@yansigit/opencodex 2.33.0 → 2.35.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 (196) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +162 -0
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { existsSync, readdirSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { hasOwnProvider } from "../config";
10
+ import { hasOwnProvider, resolveSubagentCandidates } from "../config";
11
11
  import { isRateLimitOrQuotaFailureMessage } from "../lib/errors";
12
12
  import type { OcxParsedRequest, OcxConfig } from "../types";
13
13
  import { slugsEquivalent } from "../providers/slug-codec";
@@ -15,12 +15,12 @@ import { CODEX_HOME, getCodexHome } from "./paths";
15
15
  import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
16
16
  import {
17
17
  canAcquireCodexQuotaProbeLease,
18
+ canAcquireCodexQuotaScopeProbeLease,
18
19
  codexQuotaScopeForModel,
19
20
  computeCodexUsageScore,
20
21
  getCodexQuotaHealthSnapshot,
21
22
  getEffectiveActiveCodexAccountId,
22
23
  getPoolAccountPlan,
23
- isCodexAccountInCooldown,
24
24
  } from "./routing";
25
25
  import {
26
26
  isCodexAccountUsable,
@@ -38,6 +38,7 @@ import {
38
38
  import { routeModel, type RouteResult } from "../router";
39
39
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
40
40
  import { codexAccountNamespaceForModel } from "./account-namespace-match";
41
+ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
41
42
  import {
42
43
  getUpstreamHostHealth,
43
44
  normalizeUpstreamHostCircuitThreshold,
@@ -48,6 +49,15 @@ export const DEFAULT_SUBAGENT_MODEL_FALLBACK_POLL_MS = 60_000;
48
49
  const CODEX_FORWARD_ORIGIN = new URL(CODEX_FORWARD_BASE_URL).origin.toLowerCase();
49
50
 
50
51
  type SubagentQuotaPrimeFn = (config: OcxConfig, reason: string) => Promise<void>;
52
+ /** Side-effect-free Pool account preview for one resolved fallback candidate. */
53
+ export type SubagentPoolAccountPreview = (
54
+ modelId: string | undefined,
55
+ now: number,
56
+ modelEligibleAccountIds?: ReadonlySet<string>,
57
+ ) => string | null;
58
+ export type SubagentModelEligibleAccountIds = (
59
+ modelId: string | undefined,
60
+ ) => ReadonlySet<string> | undefined;
51
61
  let subagentQuotaPrimeForTests: SubagentQuotaPrimeFn | null = null;
52
62
  let quotaPrimeInFlight: Promise<void> | null = null;
53
63
 
@@ -177,8 +187,15 @@ function resolveRouteFallbackAccountId(
177
187
  route: RouteResult | null,
178
188
  config: OcxConfig,
179
189
  accountId?: string | null,
190
+ now = Date.now(),
191
+ poolAccountPreview?: SubagentPoolAccountPreview,
192
+ modelEligibleAccountIds?: ReadonlySet<string>,
180
193
  ): string | null {
181
- return route?.codexAccountId ?? resolvePoolFallbackAccountId(config, accountId);
194
+ if (route?.codexAccountId !== undefined) return route.codexAccountId;
195
+ if (route && isPoolCodexRoute(route) && poolAccountPreview) {
196
+ return poolAccountPreview(route.modelId, now, modelEligibleAccountIds);
197
+ }
198
+ return resolvePoolFallbackAccountId(config, accountId);
182
199
  }
183
200
 
184
201
  function isRoutableFallbackModel(model: string, config: OcxConfig): boolean {
@@ -237,33 +254,53 @@ export function isSubagentModelUnavailable(
237
254
  accountId?: string | null,
238
255
  now = Date.now(),
239
256
  accountUsabilityOptions?: CodexAccountUsabilityOptions,
257
+ poolAccountPreview?: SubagentPoolAccountPreview,
258
+ modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
240
259
  ): boolean {
241
260
  if (isDisabledFallbackModel(model, config)) return true;
242
261
  if (!isRoutableFallbackModel(model, config)) return true;
243
262
  const route = tryRouteFallbackModel(config, model);
244
263
  if (!route || route.provider.disabled === true) return true;
245
- if (isModelHealthBlocked(model, config, accountId, now)) return true;
264
+ const modelEligibleAccountIds = modelEligibleAccountIdsForModel?.(route.modelId);
265
+ const candidateAccountUsabilityOptions = modelEligibleAccountIds !== undefined
266
+ ? {
267
+ ...accountUsabilityOptions,
268
+ modelEligibleAccountIds,
269
+ }
270
+ : accountUsabilityOptions;
271
+ const resolvedAccountId = resolveRouteFallbackAccountId(
272
+ route,
273
+ config,
274
+ accountId,
275
+ now,
276
+ poolAccountPreview,
277
+ candidateAccountUsabilityOptions?.modelEligibleAccountIds,
278
+ );
279
+ if (isModelHealthBlocked(model, config, resolvedAccountId, now)) return true;
246
280
  if (!isPoolCodexRoute(route)) return false;
247
281
 
248
282
  // Pool candidates need a usable account. Derive requirement from the resolved
249
283
  // route (canonical openai defaults to pool even when codexAccountMode is omitted).
250
- const resolvedAccountId = resolveRouteFallbackAccountId(route, config, accountId);
251
284
  if (!resolvedAccountId) return true;
252
285
  if (isCodexAccountPaused(config, resolvedAccountId)) return true;
253
- if (!isCodexAccountUsable(config, resolvedAccountId, accountUsabilityOptions)) return true;
286
+ if (!isCodexAccountUsable(config, resolvedAccountId, candidateAccountUsabilityOptions)) return true;
254
287
  if (route.codexAccountId !== undefined) {
255
288
  // An account-qualified route is pinned and cannot consume Pool's recovery-probe
256
289
  // escape hatch. Honor both account-wide and model-scoped cooldowns so fallback
257
290
  // advances instead of selecting a candidate that exact auth will reject.
258
291
  const quotaScope = codexQuotaScopeForModel(route.modelId);
259
292
  if (getCodexQuotaHealthSnapshot(resolvedAccountId, quotaScope, now) !== null) return true;
260
- } else if (
261
- isCodexAccountInCooldown(resolvedAccountId, now)
262
- && !canAcquireCodexQuotaProbeLease(resolvedAccountId, now)
263
- ) {
264
- return true;
293
+ } else {
294
+ const quotaScope = codexQuotaScopeForModel(route.modelId);
295
+ const cooldown = getCodexQuotaHealthSnapshot(resolvedAccountId, quotaScope, now);
296
+ if (cooldown !== null) {
297
+ const probeAvailable = cooldown.quotaScope
298
+ ? canAcquireCodexQuotaScopeProbeLease(resolvedAccountId, cooldown.quotaScope, now)
299
+ : canAcquireCodexQuotaProbeLease(resolvedAccountId, now);
300
+ if (!probeAvailable) return true;
301
+ }
265
302
  }
266
- return isNativeModelQuotaExhausted(model, config, accountId, now);
303
+ return isNativeModelQuotaExhausted(model, config, resolvedAccountId, now);
267
304
  }
268
305
 
269
306
  export function selectAvailableSubagentModel(
@@ -275,8 +312,11 @@ export function selectAvailableSubagentModel(
275
312
  nativeFallbackOnly = false,
276
313
  accountUsabilityOptions?: CodexAccountUsabilityOptions,
277
314
  trailingFallback: readonly string[] = [],
315
+ poolAccountPreview?: SubagentPoolAccountPreview,
316
+ modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
317
+ resolvedChain?: readonly string[],
278
318
  ): { model: string; rewritten: boolean; skipped: string[] } {
279
- const chain = normalizedChain(primary, config, extraFallback, trailingFallback);
319
+ const chain = resolvedChain ?? normalizedChain(primary, config, extraFallback, trailingFallback);
280
320
  const skipped: string[] = [];
281
321
  for (const candidate of chain) {
282
322
  if (nativeFallbackOnly) {
@@ -286,7 +326,15 @@ export function selectAvailableSubagentModel(
286
326
  continue;
287
327
  }
288
328
  }
289
- if (isSubagentModelUnavailable(candidate, config, accountId, now, accountUsabilityOptions)) {
329
+ if (isSubagentModelUnavailable(
330
+ candidate,
331
+ config,
332
+ accountId,
333
+ now,
334
+ accountUsabilityOptions,
335
+ poolAccountPreview,
336
+ modelEligibleAccountIdsForModel,
337
+ )) {
290
338
  skipped.push(candidate);
291
339
  continue;
292
340
  }
@@ -295,6 +343,71 @@ export function selectAvailableSubagentModel(
295
343
  return { model: primary, rewritten: false, skipped };
296
344
  }
297
345
 
346
+ function isSubagentCandidateFailureMessage(message: string): boolean {
347
+ const normalized = String(message ?? "").trim();
348
+ if (!normalized) return false;
349
+ const lower = normalized.toLowerCase();
350
+ // Explicit client/transport errors must win over broad 5xx/network matches.
351
+ if (lower.includes("connection refused")) return false;
352
+ if (
353
+ lower.includes("invalid_request_error")
354
+ || lower.includes("invalid request")
355
+ || lower.includes("missing field")
356
+ ) return false;
357
+ const numericStatus = Number(normalized);
358
+ if (Number.isInteger(numericStatus) && numericStatus >= 500 && numericStatus < 600) return true;
359
+ if (isRateLimitOrQuotaFailureMessage(normalized)) return true;
360
+ if (lower.includes("stream closed before response.completed")) return true;
361
+ if (lower.includes("upstream json response stalled before completing")) return true;
362
+ if (lower.includes("etimedout") || lower.includes("timed out") || lower.includes("timeout")) return true;
363
+ if (lower.includes("fetch failed") || lower.includes("network error")) return true;
364
+ if (lower.includes("provider error 503") || lower.includes("service unavailable")) return true;
365
+ if (lower.includes("internal server error")) return true;
366
+ // Connection-refused is a legacy transport signal and is intentionally not a cooldown.
367
+ if (/provider error 5\d\d/.test(lower)) return true;
368
+ return false;
369
+ }
370
+
371
+ export function resolveSubagentSpawnRoleFromHeaders(headers: Headers): string | undefined {
372
+ const headerRole = headers.get("x-codex-agent-role")?.trim();
373
+ if (headerRole) return headerRole;
374
+ const turnMeta = headers.get("x-codex-turn-metadata");
375
+ if (!turnMeta) return undefined;
376
+ try {
377
+ const parsed = JSON.parse(turnMeta) as { agent_role?: unknown };
378
+ const role = parsed.agent_role;
379
+ return typeof role === "string" && role.trim() !== "" ? role.trim() : undefined;
380
+ } catch {
381
+ return undefined;
382
+ }
383
+ }
384
+
385
+ export function selectAvailableSubagentCandidate(
386
+ candidates: readonly string[],
387
+ config: OcxConfig,
388
+ accountId?: string | null,
389
+ now = Date.now(),
390
+ nativeFallbackOnly = false,
391
+ accountUsabilityOptions?: CodexAccountUsabilityOptions,
392
+ ): { model: string | null; skipped: string[] } {
393
+ const skipped: string[] = [];
394
+ for (const candidate of candidates) {
395
+ if (nativeFallbackOnly) {
396
+ const route = tryRouteFallbackModel(config, candidate);
397
+ if (!route || !isCanonicalOpenAiForwardProvider(route.provider)) {
398
+ skipped.push(candidate);
399
+ continue;
400
+ }
401
+ }
402
+ if (isSubagentModelUnavailable(candidate, config, accountId, now, accountUsabilityOptions)) {
403
+ skipped.push(candidate);
404
+ continue;
405
+ }
406
+ return { model: candidate, skipped };
407
+ }
408
+ return { model: null, skipped };
409
+ }
410
+
298
411
  export function noteSubagentModelFailure(
299
412
  model: string,
300
413
  message: string,
@@ -304,7 +417,7 @@ export function noteSubagentModelFailure(
304
417
  ttlMs?: number,
305
418
  ): void {
306
419
  const interval = ttlMs ?? DEFAULT_SUBAGENT_MODEL_FALLBACK_POLL_MS;
307
- if (!isRateLimitOrQuotaFailureMessage(message)) return;
420
+ if (!isSubagentCandidateFailureMessage(message)) return;
308
421
  const route = tryRouteFallbackModel(config, model);
309
422
  const poolScoped = !!route && isPoolCodexRoute(route);
310
423
  modelHealth.set(
@@ -507,6 +620,8 @@ export function recordSubagentQuotaFailureForThreadSpawn(
507
620
  noteSubagentModelFailure(model, String(message), config, accountId, now, pollIntervalMs(config));
508
621
  }
509
622
 
623
+ export const recordSubagentFailureForThreadSpawn = recordSubagentQuotaFailureForThreadSpawn;
624
+
510
625
  export function applySubagentModelFallback(
511
626
  parsed: OcxParsedRequest,
512
627
  headers: Headers,
@@ -515,27 +630,52 @@ export function applySubagentModelFallback(
515
630
  now = Date.now(),
516
631
  nativeFallbackOnly = false,
517
632
  accountUsabilityOptions?: CodexAccountUsabilityOptions,
633
+ poolAccountPreview?: SubagentPoolAccountPreview,
634
+ modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
635
+ resolvedFallbackChain?: readonly string[] | null,
518
636
  ): { from?: string; to?: string; skipped?: string[] } | null {
519
637
  if (!isThreadSpawnRequest(headers)) return null;
520
- const tomlRoleFallback = resolveAgentModelFallbackForPrimary(
521
- parsed.modelId,
522
- getCodexHome(),
523
- config.codexAccountNamespaces,
524
- );
525
- // Config-keyed chains are the supported per-role home (#1190); TOML `model_fallback`
526
- // stays readable for backwards compatibility with homes written before Codex 0.146.
527
- const configuredFallback = resolveConfiguredModelFallbackForPrimary(parsed.modelId, config);
528
- const globalFallback = config.subagentModelFallback ?? [];
529
- if (globalFallback.length === 0 && configuredFallback.length === 0 && tomlRoleFallback.length === 0) return null;
638
+
639
+ if (config.subagentCandidates !== undefined) {
640
+ const role = resolveSubagentSpawnRoleFromHeaders(headers);
641
+ const candidates = resolveSubagentCandidates(config, role ?? parsed.modelId);
642
+ if (candidates.length > 0) {
643
+ const requested = parsed.modelId;
644
+ const selection = selectAvailableSubagentCandidate(
645
+ candidates,
646
+ config,
647
+ accountId,
648
+ now,
649
+ nativeFallbackOnly,
650
+ accountUsabilityOptions,
651
+ );
652
+ if (selection.model && !slugsEquivalent(selection.model, requested)) {
653
+ rewriteParsedModel(parsed, selection.model);
654
+ return { from: requested, to: selection.model, skipped: selection.skipped };
655
+ }
656
+ if (selection.skipped.length > 0) {
657
+ return { from: requested, to: requested, skipped: selection.skipped };
658
+ }
659
+ return null;
660
+ }
661
+ }
662
+
663
+ const fallbackChain = resolvedFallbackChain === undefined
664
+ ? resolveSubagentFallbackChain(parsed, config)
665
+ : resolvedFallbackChain;
666
+ if (!fallbackChain) return null;
530
667
  const selection = selectAvailableSubagentModel(
531
668
  parsed.modelId,
532
669
  config,
533
- configuredFallback,
670
+ [],
534
671
  accountId,
535
672
  now,
536
673
  nativeFallbackOnly,
537
674
  accountUsabilityOptions,
538
- tomlRoleFallback,
675
+ [],
676
+ poolAccountPreview,
677
+ modelEligibleAccountIdsForModel,
678
+ fallbackChain,
539
679
  );
540
680
  if (!selection.rewritten) return selection.skipped.length > 0
541
681
  ? { from: parsed.modelId, to: parsed.modelId, skipped: selection.skipped }
@@ -545,6 +685,37 @@ export function applySubagentModelFallback(
545
685
  return { from, to: selection.model, skipped: selection.skipped };
546
686
  }
547
687
 
688
+ /** Resolve the effective fallback chain once for one logical spawn request. */
689
+ export function resolveSubagentFallbackChain(
690
+ parsed: OcxParsedRequest,
691
+ config: OcxConfig,
692
+ ): readonly string[] | null {
693
+ const tomlRoleFallback = resolveAgentModelFallbackForPrimary(
694
+ parsed.modelId,
695
+ getCodexHome(),
696
+ config.codexAccountNamespaces,
697
+ );
698
+ // Config-keyed chains are the supported per-role home (#1190); TOML `model_fallback`
699
+ // stays readable for backwards compatibility with homes written before Codex 0.146.
700
+ const configuredFallback = resolveConfiguredModelFallbackForPrimary(parsed.modelId, config);
701
+ const globalFallback = config.subagentModelFallback ?? [];
702
+ if (globalFallback.length === 0 && configuredFallback.length === 0 && tomlRoleFallback.length === 0) return null;
703
+ return normalizedChain(parsed.modelId, config, configuredFallback, tomlRoleFallback);
704
+ }
705
+
706
+ /** Whether the effective fallback chain crosses an account-gated native Pool model. */
707
+ export function subagentFallbackNeedsModelEntitlements(
708
+ fallbackChain: readonly string[] | null,
709
+ config: OcxConfig,
710
+ ): boolean {
711
+ return fallbackChain?.some((candidate) => {
712
+ const route = tryRouteFallbackModel(config, candidate);
713
+ return !!route
714
+ && isPoolCodexRoute(route)
715
+ && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId);
716
+ }) === true;
717
+ }
718
+
548
719
  export function subagentFallbackGuidanceText(config: OcxConfig): string {
549
720
  const chain = config.subagentModelFallback ?? [];
550
721
  if (chain.length === 0) return "";
@@ -10,7 +10,17 @@
10
10
  * Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md §1.
11
11
  */
12
12
  import { randomUUID } from "node:crypto";
13
- import { chmodSync, lstatSync, realpathSync } from "node:fs";
13
+ import {
14
+ chmodSync,
15
+ closeSync,
16
+ fsyncSync,
17
+ linkSync,
18
+ lstatSync,
19
+ openSync,
20
+ realpathSync,
21
+ unlinkSync,
22
+ } from "node:fs";
23
+ import { basename, dirname, join } from "node:path";
14
24
 
15
25
  import { Database } from "bun:sqlite";
16
26
 
@@ -38,7 +48,7 @@ import {
38
48
  } from "./user-identity";
39
49
 
40
50
  export const CODEX_COORDINATOR_SCHEMA_VERSION = 1;
41
- const DURABLE_HISTORY_STATUSES = new Set(["converged", "pending", "running", "blocked", "unknown"]);
51
+ const DURABLE_HISTORY_STATUSES = new Set(["adoption-pending", "converged", "pending", "running", "blocked", "unknown"]);
42
52
  const DURABLE_HISTORY_REASONS = new Set([
43
53
  "db-busy",
44
54
  "permission",
@@ -66,7 +76,7 @@ const CREATE_TRANSITION_TABLE = `
66
76
  history_pending_rows INTEGER,
67
77
  history_backup_entries INTEGER,
68
78
  updated_at TEXT NOT NULL,
69
- CHECK (history_status IN ('converged', 'pending', 'running', 'blocked', 'unknown')),
79
+ CHECK (history_status IN ('adoption-pending', 'converged', 'pending', 'running', 'blocked', 'unknown')),
70
80
  CHECK (history_reason IS NULL OR history_reason IN
71
81
  ('db-busy', 'permission', 'unreadable', 'schema', 'timeout',
72
82
  'shutdown-cancelled', 'worker-died', 'overtaken', 'record-write-failed')),
@@ -75,14 +85,20 @@ const CREATE_TRANSITION_TABLE = `
75
85
  CHECK ((native_generation = 0 AND current_tx_id IS NULL)
76
86
  OR (native_generation > 0 AND length(trim(current_tx_id)) > 0)),
77
87
  CHECK ((native_generation = 0
88
+ AND history_status = 'unknown'
78
89
  AND history_tx_id IS NULL
79
90
  AND history_direction IS NULL
80
91
  AND history_authority_snapshot_id IS NULL)
92
+ OR (native_generation = 0
93
+ AND history_status = 'adoption-pending'
94
+ AND length(trim(history_tx_id)) > 0
95
+ AND history_direction IS NOT NULL
96
+ AND length(trim(history_authority_snapshot_id)) > 0)
81
97
  OR (native_generation > 0
82
98
  AND history_tx_id = current_tx_id
83
99
  AND history_direction IS NOT NULL
84
100
  AND length(trim(history_authority_snapshot_id)) > 0)),
85
- CHECK (native_generation > 0 OR
101
+ CHECK (native_generation > 0 OR history_status = 'adoption-pending' OR
86
102
  (history_status = 'unknown'
87
103
  AND history_reason IS NULL
88
104
  AND history_attempts = 0
@@ -100,6 +116,15 @@ const INITIALIZE_TRANSITION_ROW = `
100
116
  history_backup_entries, updated_at
101
117
  ) VALUES (1, 0, NULL, 'unknown', NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, ?)`;
102
118
 
119
+ const INITIALIZE_ADOPTION_ROW = `
120
+ INSERT INTO codex_transition_state (
121
+ singleton, native_generation, current_tx_id,
122
+ history_status, history_reason, history_attempts,
123
+ history_next_retry_at, history_tx_id, history_direction,
124
+ history_authority_snapshot_id, history_pending_rows,
125
+ history_backup_entries, updated_at
126
+ ) VALUES (1, 0, NULL, 'adoption-pending', NULL, 0, NULL, ?, ?, ?, NULL, NULL, ?)`;
127
+
103
128
  const SELECT_TRANSITION_ROW = `
104
129
  SELECT native_generation, current_tx_id,
105
130
  history_status, history_reason, history_attempts,
@@ -210,8 +235,15 @@ function rowToState(row: TransitionRow | null): CodexTransitionState {
210
235
 
211
236
  const generation = row.native_generation;
212
237
  if (generation === 0) {
213
- if (row.current_tx_id !== null || row.history_tx_id !== null
214
- || row.history_direction !== null || row.history_authority_snapshot_id !== null) {
238
+ const ordinaryInitial = row.history_status === "unknown"
239
+ && row.history_tx_id === null
240
+ && row.history_direction === null
241
+ && row.history_authority_snapshot_id === null;
242
+ const adoptionInitial = row.history_status === "adoption-pending"
243
+ && Boolean(row.history_tx_id?.trim())
244
+ && (row.history_direction === "apply" || row.history_direction === "remove")
245
+ && Boolean(row.history_authority_snapshot_id?.trim());
246
+ if (row.current_tx_id !== null || (!ordinaryInitial && !adoptionInitial)) {
215
247
  throw new CodexCoordinatorTransactionError("The initial coordinator row contains transition metadata.");
216
248
  }
217
249
  } else if (!row.current_tx_id?.trim()
@@ -234,13 +266,73 @@ function rowToState(row: TransitionRow | null): CodexTransitionState {
234
266
  nativeGeneration: generation,
235
267
  currentTxId: row.current_tx_id,
236
268
  history,
237
- historySchedule: generation === 0 ? null : {
269
+ historySchedule: generation === 0 && row.history_status !== "adoption-pending" ? null : {
238
270
  direction: row.history_direction as "apply" | "remove",
239
271
  authoritySnapshotId: row.history_authority_snapshot_id as string,
240
272
  },
241
273
  };
242
274
  }
243
275
 
276
+ export type CodexCoordinatorAdoptionCheckpoint = "temp-created" | "temp-committed" | "published";
277
+
278
+ export interface CodexCoordinatorAdoptionOptions {
279
+ readonly direction: "apply" | "remove";
280
+ readonly onCheckpoint?: (checkpoint: CodexCoordinatorAdoptionCheckpoint) => void;
281
+ }
282
+
283
+ function fsyncPath(path: string): void {
284
+ const fd = openSync(path, "r");
285
+ try { fsyncSync(fd); } finally { closeSync(fd); }
286
+ }
287
+
288
+ function publishAdoptionDatabase(
289
+ finalDatabasePath: string,
290
+ options: CodexCoordinatorAdoptionOptions,
291
+ ): void {
292
+ const parent = dirname(finalDatabasePath);
293
+ const tempPath = join(parent, `.${basename(finalDatabasePath)}.adopt-${process.pid}-${randomUUID()}.tmp`);
294
+ let tempPresent = false;
295
+ try {
296
+ const fd = openSync(tempPath, "wx", 0o600);
297
+ closeSync(fd);
298
+ tempPresent = true;
299
+ options.onCheckpoint?.("temp-created");
300
+
301
+ const temp = new Database(tempPath, { readwrite: true, create: false });
302
+ try {
303
+ temp.exec("PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE");
304
+ temp.exec(CREATE_TRANSITION_TABLE);
305
+ temp.query(INITIALIZE_ADOPTION_ROW).run(
306
+ randomUUID(),
307
+ options.direction,
308
+ randomUUID(),
309
+ new Date().toISOString(),
310
+ );
311
+ temp.exec(`PRAGMA user_version = ${CODEX_COORDINATOR_SCHEMA_VERSION}; COMMIT`);
312
+ readCodexCoordinatorState(temp);
313
+ } catch (error) {
314
+ try { temp.exec("ROLLBACK"); } catch { /* commit may already have completed */ }
315
+ throw error;
316
+ } finally {
317
+ temp.close();
318
+ }
319
+ fsyncPath(tempPath);
320
+ options.onCheckpoint?.("temp-committed");
321
+
322
+ linkSync(tempPath, finalDatabasePath);
323
+ options.onCheckpoint?.("published");
324
+ if (process.platform !== "win32") fsyncPath(parent);
325
+ } catch (error) {
326
+ if (errorCode(error) !== "EEXIST") throw error;
327
+ } finally {
328
+ if (tempPresent) {
329
+ try { unlinkSync(tempPath); } catch (error) {
330
+ if (errorCode(error) !== "ENOENT") throw error;
331
+ }
332
+ }
333
+ }
334
+ }
335
+
244
336
  export function readCodexCoordinatorState(database: Database): CodexTransitionState {
245
337
  const row = database.query<TransitionRow, []>(SELECT_TRANSITION_ROW).get();
246
338
  return rowToState(row);
@@ -346,7 +438,10 @@ function createCapability(
346
438
  };
347
439
  }
348
440
 
349
- export function openCodexCoordinatorTransaction(finalDatabasePath: string): CodexCoordinatorTransactionController {
441
+ export function openCodexCoordinatorTransaction(
442
+ finalDatabasePath: string,
443
+ adoption?: CodexCoordinatorAdoptionOptions,
444
+ ): CodexCoordinatorTransactionController {
350
445
  let database: Database | undefined;
351
446
  let transactionOpen = false;
352
447
  let closed = false;
@@ -389,6 +484,10 @@ export function openCodexCoordinatorTransaction(finalDatabasePath: string): Code
389
484
  if (errorCode(cause) !== "ENOENT") throw cause;
390
485
  databaseWasAbsent = true;
391
486
  }
487
+ if (databaseWasAbsent && adoption) {
488
+ publishAdoptionDatabase(finalDatabasePath, adoption);
489
+ databaseWasAbsent = false;
490
+ }
392
491
  database = new Database(finalDatabasePath, { create: true });
393
492
  if (databaseWasAbsent) {
394
493
  try { chmodSync(finalDatabasePath, 0o600); } catch { /* Windows applies ACLs in WP11. */ }
@@ -203,6 +203,16 @@ export function comboConfigIssues(
203
203
  }
204
204
 
205
205
  const body = raw as Record<string, unknown>;
206
+ if (typeof body.alias === "string") {
207
+ const alias = body.alias.toLowerCase();
208
+ for (const [providerName, provider] of Object.entries(providers)) {
209
+ if (provider.alias?.toLowerCase() === alias) {
210
+ issues.push({ path: ["alias"], message: `alias "${body.alias}" is already used by provider "${providerName}"` });
211
+ }
212
+ const model = Object.entries(provider.modelAliases ?? {}).find(([, value]) => value.toLowerCase() === alias);
213
+ if (model) issues.push({ path: ["alias"], message: `alias "${body.alias}" is already used by model "${providerName}/${model[0]}"` });
214
+ }
215
+ }
206
216
  if (body.strategy !== undefined
207
217
  && body.strategy !== "failover"
208
218
  && body.strategy !== "round-robin") {
@@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
9
9
  export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
10
10
  schemaVersion: 1,
11
11
  id: "openai.codex-forward.gpt-5-6-sol.responses",
12
- version: "1.0.0",
12
+ version: "1.2.0",
13
13
  subject: {
14
14
  providerId: "openai",
15
15
  baseUrl: "https://chatgpt.com/backend-api/codex",
@@ -69,6 +69,38 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
69
69
  limitation: "The field is removed before dispatch.",
70
70
  evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["metadata-removed"] }],
71
71
  },
72
+ {
73
+ id: "system-input-messages",
74
+ feature: "request.input.system_messages",
75
+ disposition: "translated",
76
+ summary: "Text-only input system messages are folded into top-level instructions.",
77
+ limitation: "The destination does not accept system-role input items; multimodal system messages stay unchanged rather than being dropped.",
78
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["system-message-folded", "system-message-removed"] }],
79
+ },
80
+ {
81
+ id: "truncation",
82
+ feature: "request.truncation",
83
+ disposition: "unsupported",
84
+ summary: "The ChatGPT Codex forward route does not receive truncation.",
85
+ limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
86
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["truncation-removed"] }],
87
+ },
88
+ {
89
+ id: "prompt-cache-breakpoint",
90
+ feature: "request.prompt_cache_breakpoint",
91
+ disposition: "unsupported",
92
+ summary: "Client-only prompt cache breakpoint markers do not reach the forward backend.",
93
+ limitation: "Markers are removed recursively from input within bounded depth and node budgets.",
94
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-breakpoint-removed"] }],
95
+ },
96
+ {
97
+ id: "unstored-item-reference",
98
+ feature: "continuation.item_reference",
99
+ disposition: "degraded",
100
+ summary: "Unpersisted item references are omitted from store-false continuations.",
101
+ limitation: "The referenced item cannot exist at the backend when store is false; tool call_id pairs remain unchanged.",
102
+ evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["unstored-item-reference-removed"] }],
103
+ },
72
104
  {
73
105
  id: "prompt-cache-retention",
74
106
  feature: "request.prompt_cache_retention",
@@ -0,0 +1,21 @@
1
+ export interface AutonomousRemediationConfig {
2
+ enabled: boolean;
3
+ instanceId?: string;
4
+ threshold: number;
5
+ rollingWindowMs: number;
6
+ }
7
+
8
+ const DEFAULT_THRESHOLD = 3;
9
+ const DEFAULT_WINDOW_MS = 86_400_000;
10
+
11
+ export function resolveAutonomousRemediationConfig(value: unknown): AutonomousRemediationConfig {
12
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
13
+ return { enabled: false, instanceId: undefined, threshold: DEFAULT_THRESHOLD, rollingWindowMs: DEFAULT_WINDOW_MS };
14
+ }
15
+ const input = value as Record<string, unknown>;
16
+ const enabled = input.enabled === true;
17
+ const instanceId = typeof input.instanceId === "string" && input.instanceId.trim() ? input.instanceId.trim() : undefined;
18
+ const threshold = typeof input.threshold === "number" && Number.isInteger(input.threshold) && input.threshold > 0 ? input.threshold : DEFAULT_THRESHOLD;
19
+ const rollingWindowMs = typeof input.rollingWindowMs === "number" && Number.isInteger(input.rollingWindowMs) && input.rollingWindowMs > 0 ? input.rollingWindowMs : DEFAULT_WINDOW_MS;
20
+ return { enabled, instanceId, threshold, rollingWindowMs };
21
+ }
@@ -212,3 +212,17 @@ export function modelAdapterRecordConfigError(
212
212
  }
213
213
  return null;
214
214
  }
215
+
216
+ export function wsUpstreamConfigError(value: unknown): string | null {
217
+ if (value === undefined || value === null) return null;
218
+ if (typeof value !== "boolean") return "wsUpstream must be a boolean";
219
+ return null;
220
+ }
221
+
222
+ export function maxWsFrameBytesConfigError(value: unknown): string | null {
223
+ if (value === undefined || value === null) return null;
224
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
225
+ return "maxWsFrameBytes must be a positive finite integer";
226
+ }
227
+ return null;
228
+ }