@iislee/opencodex 2.31.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -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
  }
@@ -515,27 +563,27 @@ export function applySubagentModelFallback(
515
563
  now = Date.now(),
516
564
  nativeFallbackOnly = false,
517
565
  accountUsabilityOptions?: CodexAccountUsabilityOptions,
566
+ poolAccountPreview?: SubagentPoolAccountPreview,
567
+ modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
568
+ resolvedFallbackChain?: readonly string[] | null,
518
569
  ): { from?: string; to?: string; skipped?: string[] } | null {
519
570
  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;
571
+ const fallbackChain = resolvedFallbackChain === undefined
572
+ ? resolveSubagentFallbackChain(parsed, config)
573
+ : resolvedFallbackChain;
574
+ if (!fallbackChain) return null;
530
575
  const selection = selectAvailableSubagentModel(
531
576
  parsed.modelId,
532
577
  config,
533
- configuredFallback,
578
+ [],
534
579
  accountId,
535
580
  now,
536
581
  nativeFallbackOnly,
537
582
  accountUsabilityOptions,
538
- tomlRoleFallback,
583
+ [],
584
+ poolAccountPreview,
585
+ modelEligibleAccountIdsForModel,
586
+ fallbackChain,
539
587
  );
540
588
  if (!selection.rewritten) return selection.skipped.length > 0
541
589
  ? { from: parsed.modelId, to: parsed.modelId, skipped: selection.skipped }
@@ -545,6 +593,37 @@ export function applySubagentModelFallback(
545
593
  return { from, to: selection.model, skipped: selection.skipped };
546
594
  }
547
595
 
596
+ /** Resolve the effective fallback chain once for one logical spawn request. */
597
+ export function resolveSubagentFallbackChain(
598
+ parsed: OcxParsedRequest,
599
+ config: OcxConfig,
600
+ ): readonly string[] | null {
601
+ const tomlRoleFallback = resolveAgentModelFallbackForPrimary(
602
+ parsed.modelId,
603
+ getCodexHome(),
604
+ config.codexAccountNamespaces,
605
+ );
606
+ // Config-keyed chains are the supported per-role home (#1190); TOML `model_fallback`
607
+ // stays readable for backwards compatibility with homes written before Codex 0.146.
608
+ const configuredFallback = resolveConfiguredModelFallbackForPrimary(parsed.modelId, config);
609
+ const globalFallback = config.subagentModelFallback ?? [];
610
+ if (globalFallback.length === 0 && configuredFallback.length === 0 && tomlRoleFallback.length === 0) return null;
611
+ return normalizedChain(parsed.modelId, config, configuredFallback, tomlRoleFallback);
612
+ }
613
+
614
+ /** Whether the effective fallback chain crosses an account-gated native Pool model. */
615
+ export function subagentFallbackNeedsModelEntitlements(
616
+ fallbackChain: readonly string[] | null,
617
+ config: OcxConfig,
618
+ ): boolean {
619
+ return fallbackChain?.some((candidate) => {
620
+ const route = tryRouteFallbackModel(config, candidate);
621
+ return !!route
622
+ && isPoolCodexRoute(route)
623
+ && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId);
624
+ }) === true;
625
+ }
626
+
548
627
  export function subagentFallbackGuidanceText(config: OcxConfig): string {
549
628
  const chain = config.subagentModelFallback ?? [];
550
629
  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,82 @@ 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 fsyncFile(path: string): void {
284
+ // Writable fd on purpose. Windows FlushFileBuffers needs GENERIC_WRITE;
285
+ // openSync(path, "r") is GENERIC_READ only and fails with EPERM (measured on
286
+ // Windows 11 25H2 / bun 1.3.14). prompt-journal.ts uses the same "r+" open.
287
+ const fd = openSync(path, "r+");
288
+ try { fsyncSync(fd); } finally { closeSync(fd); }
289
+ }
290
+
291
+ function fsyncDir(path: string): void {
292
+ // POSIX directory fsync is O_RDONLY. O_RDWR ("r+") is EISDIR.
293
+ const fd = openSync(path, "r");
294
+ try { fsyncSync(fd); } finally { closeSync(fd); }
295
+ }
296
+
297
+ function publishAdoptionDatabase(
298
+ finalDatabasePath: string,
299
+ options: CodexCoordinatorAdoptionOptions,
300
+ ): void {
301
+ const parent = dirname(finalDatabasePath);
302
+ const tempPath = join(parent, `.${basename(finalDatabasePath)}.adopt-${process.pid}-${randomUUID()}.tmp`);
303
+ let tempPresent = false;
304
+ try {
305
+ const fd = openSync(tempPath, "wx", 0o600);
306
+ closeSync(fd);
307
+ tempPresent = true;
308
+ options.onCheckpoint?.("temp-created");
309
+
310
+ const temp = new Database(tempPath, { readwrite: true, create: false });
311
+ try {
312
+ temp.exec("PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE");
313
+ temp.exec(CREATE_TRANSITION_TABLE);
314
+ temp.query(INITIALIZE_ADOPTION_ROW).run(
315
+ randomUUID(),
316
+ options.direction,
317
+ randomUUID(),
318
+ new Date().toISOString(),
319
+ );
320
+ temp.exec(`PRAGMA user_version = ${CODEX_COORDINATOR_SCHEMA_VERSION}; COMMIT`);
321
+ readCodexCoordinatorState(temp);
322
+ } catch (error) {
323
+ try { temp.exec("ROLLBACK"); } catch { /* commit may already have completed */ }
324
+ throw error;
325
+ } finally {
326
+ temp.close();
327
+ }
328
+ fsyncFile(tempPath);
329
+ options.onCheckpoint?.("temp-committed");
330
+
331
+ linkSync(tempPath, finalDatabasePath);
332
+ options.onCheckpoint?.("published");
333
+ if (process.platform !== "win32") fsyncDir(parent);
334
+ } catch (error) {
335
+ if (errorCode(error) !== "EEXIST") throw error;
336
+ } finally {
337
+ if (tempPresent) {
338
+ try { unlinkSync(tempPath); } catch (error) {
339
+ if (errorCode(error) !== "ENOENT") throw error;
340
+ }
341
+ }
342
+ }
343
+ }
344
+
244
345
  export function readCodexCoordinatorState(database: Database): CodexTransitionState {
245
346
  const row = database.query<TransitionRow, []>(SELECT_TRANSITION_ROW).get();
246
347
  return rowToState(row);
@@ -346,7 +447,10 @@ function createCapability(
346
447
  };
347
448
  }
348
449
 
349
- export function openCodexCoordinatorTransaction(finalDatabasePath: string): CodexCoordinatorTransactionController {
450
+ export function openCodexCoordinatorTransaction(
451
+ finalDatabasePath: string,
452
+ adoption?: CodexCoordinatorAdoptionOptions,
453
+ ): CodexCoordinatorTransactionController {
350
454
  let database: Database | undefined;
351
455
  let transactionOpen = false;
352
456
  let closed = false;
@@ -389,6 +493,10 @@ export function openCodexCoordinatorTransaction(finalDatabasePath: string): Code
389
493
  if (errorCode(cause) !== "ENOENT") throw cause;
390
494
  databaseWasAbsent = true;
391
495
  }
496
+ if (databaseWasAbsent && adoption) {
497
+ publishAdoptionDatabase(finalDatabasePath, adoption);
498
+ databaseWasAbsent = false;
499
+ }
392
500
  database = new Database(finalDatabasePath, { create: true });
393
501
  if (databaseWasAbsent) {
394
502
  try { chmodSync(finalDatabasePath, 0o600); } catch { /* Windows applies ACLs in WP11. */ }
@@ -109,6 +109,28 @@ export function clearComboTargetCooldowns(comboId?: string): void {
109
109
 
110
110
  export type ComboFailureDecision = "hop" | "stop";
111
111
 
112
+ function isModelLifecycleGone(
113
+ status: number,
114
+ message: string,
115
+ code?: string | null,
116
+ ): boolean {
117
+ if (status !== 410) return false;
118
+ const normalizedCode = code?.trim().toLowerCase().replaceAll("-", "_");
119
+ if ([
120
+ "model_deprecated",
121
+ "model_end_of_life",
122
+ "model_eol",
123
+ "model_not_found",
124
+ "model_retired",
125
+ ].includes(normalizedCode ?? "")) return true;
126
+ const text = message.toLowerCase();
127
+ return /\bmodel\b/.test(text) && (
128
+ /\bend[ -]of[ -]life\b/.test(text)
129
+ || /\bno longer available\b/.test(text)
130
+ || /\b(?:deprecated|retired|retirement|sunset|decommissioned)\b/.test(text)
131
+ );
132
+ }
133
+
112
134
  export function comboFailureDecision(
113
135
  status: number,
114
136
  message: string,
@@ -119,6 +141,11 @@ export function comboFailureDecision(
119
141
  // Cyber policy is a hard non-retryable refusal — honor structured code even when
120
142
  // classificationText was truncated before the JSON code field.
121
143
  if (isCyberPolicyCode(options?.code)) return "stop";
144
+ // HTTP 410 is normally terminal. A model-specific lifecycle verdict is target-local,
145
+ // however: another provider/model in the declared combo can still serve the request.
146
+ // Require structured lifecycle code or explicit model+lifecycle prose so unrelated
147
+ // application-level 410 responses remain fail-closed.
148
+ if (isModelLifecycleGone(status, message, options?.code)) return "hop";
122
149
  const error = classifyError(status, "upstream_error", message);
123
150
  if (isCyberPolicyCode(error.code)) return "stop";
124
151
  // A local input-admission refusal (#1524) says "this candidate cannot fit the request",
@@ -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") {
@@ -0,0 +1,26 @@
1
+ import { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
2
+ import type { CompatibilityManifestV1 } from "./manifest";
3
+
4
+ export {
5
+ COMPATIBILITY_DISPOSITIONS,
6
+ COMPATIBILITY_MANIFEST_SCHEMA_VERSION,
7
+ compatibilityManifestIssues,
8
+ defineCompatibilityManifest,
9
+ } from "./manifest";
10
+ export type {
11
+ CompatibilityClaimV1,
12
+ CompatibilityDisposition,
13
+ CompatibilityEvidenceKind,
14
+ CompatibilityEvidenceRefV1,
15
+ CompatibilityManifestV1,
16
+ CompatibilitySubjectV1,
17
+ } from "./manifest";
18
+ export { OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST } from "./openai-responses";
19
+
20
+ export const COMPATIBILITY_MANIFESTS: readonly CompatibilityManifestV1[] = Object.freeze([
21
+ OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST,
22
+ ]);
23
+
24
+ export function getCompatibilityManifest(id: string): CompatibilityManifestV1 | undefined {
25
+ return COMPATIBILITY_MANIFESTS.find(manifest => manifest.id === id);
26
+ }