@bitkyc08/opencodex 2.81.0 → 2.83.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 (272) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ba8uG696.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-IImbNPRy.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-BHRZdJuv.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DiJO6sYY.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/cursor/catalog.ts +2 -2
  11. package/src/adapters/cursor/effort-map.ts +1 -1
  12. package/src/adapters/devin/live-models.ts +2 -2
  13. package/src/adapters/google-truncation.ts +22 -0
  14. package/src/adapters/google.ts +7 -3
  15. package/src/adapters/ollama-native.ts +18 -14
  16. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  17. package/src/adapters/openai-responses/passthrough.ts +11 -4
  18. package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
  19. package/src/adapters/openai-responses/reasoning.ts +11 -1
  20. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  21. package/src/bridge/errors.ts +7 -1
  22. package/src/claude/agents-inject.ts +14 -3
  23. package/src/claude/context-windows.ts +22 -20
  24. package/src/claude/desktop-3p.ts +88 -12
  25. package/src/claude/desktop-picker.ts +1 -1
  26. package/src/claude/desktop-profile.ts +2 -1
  27. package/src/claude/gateway-cache.ts +18 -1
  28. package/src/claude/inbound.ts +77 -3
  29. package/src/claude/intercept/cli-catalog.ts +3 -0
  30. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  31. package/src/claude/intercept/picker-models.ts +14 -6
  32. package/src/claude/intercept/settings.ts +3 -0
  33. package/src/claude/long-context.ts +66 -0
  34. package/src/claude/model-info.ts +19 -16
  35. package/src/claude/outbound.ts +94 -9
  36. package/src/claude/subagent-model.ts +40 -11
  37. package/src/cli/account-auth.ts +20 -12
  38. package/src/cli/agent.ts +9 -5
  39. package/src/cli/aside-profile-recovery.ts +33 -0
  40. package/src/cli/capabilities-base.ts +5 -5
  41. package/src/cli/capabilities-integrations.ts +2 -2
  42. package/src/cli/cli-path-diagnostics.ts +132 -0
  43. package/src/cli/desktop-runtime-guidance.ts +65 -0
  44. package/src/cli/dispatch.ts +5 -0
  45. package/src/cli/doctor.ts +16 -3
  46. package/src/cli/index.ts +14 -9
  47. package/src/cli/integration-aside-sync.ts +2 -1
  48. package/src/cli/integrations.ts +16 -3
  49. package/src/cli/resolve.ts +26 -3
  50. package/src/cli/root.ts +4 -2
  51. package/src/cli/status.ts +64 -12
  52. package/src/cli/update-restart-child.ts +4 -2
  53. package/src/cli/update-restart-home.ts +28 -10
  54. package/src/cli/update-restart-service-record.ts +116 -0
  55. package/src/cli/update-restart-supervision.ts +96 -0
  56. package/src/cli/update-restart.ts +59 -13
  57. package/src/cli/v2.ts +27 -20
  58. package/src/cli/version-skew-notice.ts +77 -0
  59. package/src/clients/config-export/model-metadata.ts +16 -0
  60. package/src/clients/config-export/zcode-store.ts +46 -11
  61. package/src/clients/config-export/zcode.ts +3 -5
  62. package/src/clients/config-export.ts +1 -1
  63. package/src/clients/lazycodex.ts +1 -1
  64. package/src/clients/omo-role-models.ts +32 -7
  65. package/src/codex/auth-api/account-selection.ts +107 -0
  66. package/src/codex/auth-api/pool-mode-gate.ts +4 -5
  67. package/src/codex/auth-api/routes.ts +10 -49
  68. package/src/codex/auth-context.ts +28 -4
  69. package/src/codex/autostart-health.ts +12 -3
  70. package/src/codex/catalog/build-entries.ts +18 -0
  71. package/src/codex/catalog/bundled.ts +155 -26
  72. package/src/codex/catalog/combo-member.ts +3 -2
  73. package/src/codex/catalog/effort.ts +26 -5
  74. package/src/codex/catalog/gather-capture.ts +5 -0
  75. package/src/codex/catalog/model-hints.ts +15 -8
  76. package/src/codex/catalog/model-visibility.ts +3 -1
  77. package/src/codex/catalog/native-models.ts +1 -1
  78. package/src/codex/catalog/operator-tiers.ts +38 -0
  79. package/src/codex/catalog/provider-models.ts +68 -35
  80. package/src/codex/catalog/retained-sync.ts +7 -5
  81. package/src/codex/catalog/routed-gather.ts +15 -1
  82. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  83. package/src/codex/catalog-auto-refresh.ts +19 -0
  84. package/src/codex/catalog.ts +1 -1
  85. package/src/codex/codex-home-owner.ts +3 -0
  86. package/src/codex/config-write-lock.ts +333 -0
  87. package/src/codex/convergence.ts +2 -1
  88. package/src/codex/desktop-app/darwin.ts +1 -1
  89. package/src/codex/desktop-app/handoff.ts +1 -1
  90. package/src/codex/desktop-app/linux.ts +1 -1
  91. package/src/codex/desktop-app/lock.ts +1 -1
  92. package/src/codex/desktop-app/types.ts +1 -1
  93. package/src/codex/desktop-app/windows.ts +1 -1
  94. package/src/codex/desktop-app-restart.ts +1 -1
  95. package/src/codex/features.ts +257 -200
  96. package/src/codex/inject/config-write-section.ts +34 -0
  97. package/src/codex/inject/multi-agent-v2.ts +14 -6
  98. package/src/codex/inject/remove.ts +36 -11
  99. package/src/codex/inject/restore.ts +85 -20
  100. package/src/codex/inject-coordination.ts +15 -6
  101. package/src/codex/inject.ts +62 -20
  102. package/src/codex/journal.ts +62 -17
  103. package/src/codex/main-account-cache.ts +43 -0
  104. package/src/codex/main-account.ts +6 -7
  105. package/src/codex/paths.ts +2 -0
  106. package/src/codex/prepared-home.ts +18 -0
  107. package/src/codex/prompt-journal.ts +25 -12
  108. package/src/codex/prompt-layers.ts +49 -26
  109. package/src/codex/prompt-lock-claim.ts +134 -0
  110. package/src/codex/prompt-lock-evidence.ts +26 -0
  111. package/src/codex/prompt-lock-io.ts +16 -0
  112. package/src/codex/prompt-lock-owner.ts +130 -0
  113. package/src/codex/prompt-lock.ts +84 -53
  114. package/src/codex/routing/failure-window.ts +146 -0
  115. package/src/codex/routing/selection.ts +4 -1
  116. package/src/codex/routing.ts +13 -17
  117. package/src/codex/runtime.ts +106 -18
  118. package/src/codex/shim.ts +45 -19
  119. package/src/combos/jev-service-exchange.ts +281 -0
  120. package/src/combos/jev.ts +34 -220
  121. package/src/config/atomic-write.ts +1 -1
  122. package/src/config/codex-ws-reuse-setting.ts +10 -0
  123. package/src/config/live-reconcile.ts +17 -0
  124. package/src/config/load-degrade.ts +8 -0
  125. package/src/config/rebase-provenance.ts +11 -0
  126. package/src/config/schema/config-schema.ts +4 -0
  127. package/src/generated/compatibility-version.json +375 -227
  128. package/src/generated/model-metadata.ts +2 -2
  129. package/src/grok/inject.ts +276 -206
  130. package/src/grok/reset-coupon-ledger.ts +223 -57
  131. package/src/grok/reset-coupons.ts +13 -6
  132. package/src/grok/sync.ts +2 -1
  133. package/src/images/loop.ts +29 -11
  134. package/src/integrations/aside-profiles.ts +10 -3
  135. package/src/integrations/catalog-refresh.ts +18 -2
  136. package/src/integrations/cursor-detect.ts +1 -1
  137. package/src/integrations/writer.ts +9 -3
  138. package/src/lib/bun-path-runtime.d.mts +32 -0
  139. package/src/lib/bun-path-runtime.mjs +95 -0
  140. package/src/lib/bun-runtime-preflight.ts +140 -0
  141. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  142. package/src/lib/desktop-cli-handoff.mjs +92 -0
  143. package/src/lib/desktop-cli-record.d.mts +29 -0
  144. package/src/lib/desktop-cli-record.mjs +102 -0
  145. package/src/lib/errors.ts +25 -0
  146. package/src/lib/redact.ts +2 -2
  147. package/src/lib/request-execution-budget.ts +8 -0
  148. package/src/lib/synced-state-location.ts +46 -6
  149. package/src/lib/test-home-guard.ts +87 -1
  150. package/src/lib/translator-budget.ts +17 -4
  151. package/src/lib/windows-atomic-replace.ts +38 -16
  152. package/src/lib/windows-elevation.ts +6 -0
  153. package/src/lib/windows-secret-acl.ts +92 -25
  154. package/src/lib/winsw.ts +2 -3
  155. package/src/oauth/anthropic-account-refusal.ts +39 -1
  156. package/src/oauth/anthropic-routing.ts +1 -1
  157. package/src/oauth/anthropic-send-ownership.ts +3 -3
  158. package/src/oauth/chatgpt.ts +113 -41
  159. package/src/oauth/index.ts +7 -3
  160. package/src/oauth/meta-muse-device.ts +1 -1
  161. package/src/oauth/store.ts +1 -1
  162. package/src/oauth/types.ts +1 -1
  163. package/src/providers/anthropic-fast.ts +1 -1
  164. package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
  165. package/src/providers/azure-model-metadata.ts +115 -0
  166. package/src/providers/derive.ts +21 -0
  167. package/src/providers/devin-cli-authmode-migration.ts +1 -1
  168. package/src/providers/devin-provider-merge-migration.ts +1 -1
  169. package/src/providers/github-copilot-auto.ts +169 -0
  170. package/src/providers/model-presets.ts +1 -1
  171. package/src/providers/muse-subscription-usage.ts +1 -1
  172. package/src/providers/quota/vendor-probes-key.ts +127 -17
  173. package/src/providers/quota.ts +1 -1
  174. package/src/providers/registry/entries-core.ts +6 -6
  175. package/src/providers/registry/entries-extended.ts +4 -0
  176. package/src/providers/registry/model-seeds.ts +3 -3
  177. package/src/providers/xai-fast-model.ts +1 -1
  178. package/src/responses/reasoning-envelope.ts +29 -1
  179. package/src/responses/reasoning-replay-cache.ts +17 -1
  180. package/src/responses/spill-store.ts +15 -2
  181. package/src/responses/state/metrics.ts +8 -0
  182. package/src/responses/state/spill-failure.ts +12 -0
  183. package/src/responses/state/spill-queue.ts +12 -5
  184. package/src/responses/state.ts +18 -10
  185. package/src/responses/task-input.ts +17 -0
  186. package/src/server/background-lifecycle.ts +27 -0
  187. package/src/server/chat-completions.ts +1 -0
  188. package/src/server/chat-native.ts +28 -12
  189. package/src/server/claude-messages.ts +43 -6
  190. package/src/server/inbound-body-admission.ts +25 -7
  191. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  192. package/src/server/index/link-listener.ts +30 -17
  193. package/src/server/index/serve-options.ts +18 -4
  194. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  195. package/src/server/index/startup-warnings.ts +19 -0
  196. package/src/server/index.ts +2 -1
  197. package/src/server/lifecycle.ts +31 -4
  198. package/src/server/management/agent-settings-routes.ts +93 -32
  199. package/src/server/management/codex-agent-role-routes.ts +4 -2
  200. package/src/server/management/codex-prompt-routes.ts +1 -0
  201. package/src/server/management/codex-role-auto-assign.ts +13 -2
  202. package/src/server/management/config-routes.ts +59 -8
  203. package/src/server/management/context.ts +2 -2
  204. package/src/server/management/grok-coupon-routes.ts +277 -52
  205. package/src/server/management/link-routes.ts +42 -20
  206. package/src/server/management/route-registry.ts +2 -2
  207. package/src/server/management/shared.ts +3 -3
  208. package/src/server/messages-native-oauth.ts +9 -8
  209. package/src/server/messages-native.ts +49 -13
  210. package/src/server/messages-response-headers.ts +14 -0
  211. package/src/server/relay-eager.ts +2 -0
  212. package/src/server/relay.ts +18 -7
  213. package/src/server/request-log-conversation.ts +48 -5
  214. package/src/server/request-log-failure-attribution.ts +3 -0
  215. package/src/server/request-log-terminal-status.ts +186 -0
  216. package/src/server/request-log.ts +15 -89
  217. package/src/server/responses/adapter-continuation.ts +11 -3
  218. package/src/server/responses/adapter-dispatch.ts +99 -19
  219. package/src/server/responses/codex-ws-exchange.ts +17 -2
  220. package/src/server/responses/codex-ws-pool.ts +100 -13
  221. package/src/server/responses/codex-ws-wire.ts +9 -0
  222. package/src/server/responses/combo-requested-effort.ts +3 -1
  223. package/src/server/responses/compact.ts +3 -2
  224. package/src/server/responses/core-codex-account.ts +22 -1
  225. package/src/server/responses/core-combo-native.ts +20 -3
  226. package/src/server/responses/core-combo.ts +22 -14
  227. package/src/server/responses/core-lifetime.ts +3 -36
  228. package/src/server/responses/core-opaque-recovery.ts +5 -5
  229. package/src/server/responses/core-options.ts +5 -0
  230. package/src/server/responses/core-replay.ts +73 -0
  231. package/src/server/responses/core.ts +9 -9
  232. package/src/server/responses/empty-completion-guard.ts +18 -2
  233. package/src/server/responses/fetch-helpers.ts +26 -3
  234. package/src/server/responses/jev-model-invoke.ts +1 -1
  235. package/src/server/responses/passthrough-delivery.ts +24 -2
  236. package/src/server/responses/passthrough-dispatch.ts +134 -62
  237. package/src/server/responses/passthrough-execution.ts +3 -3
  238. package/src/server/responses/request-prepare.ts +4 -0
  239. package/src/server/responses/request-send-budget.ts +112 -49
  240. package/src/server/responses/request-transport.ts +125 -5
  241. package/src/server/responses/response-effects.ts +1 -0
  242. package/src/server/responses/run-turn-execution.ts +15 -5
  243. package/src/server/responses/sidecar-execution.ts +30 -16
  244. package/src/server/responses/sidecar-send-budget.ts +58 -0
  245. package/src/server/responses/spend-storage-error.ts +33 -0
  246. package/src/server/responses/ws-upstream.ts +19 -6
  247. package/src/server/startup-health-cache.ts +2 -1
  248. package/src/server/workflow-refusal.ts +3 -0
  249. package/src/service/cli.ts +20 -4
  250. package/src/service/desktop-command-guard.ts +32 -0
  251. package/src/service/desktop-startup.ts +66 -40
  252. package/src/service/desktop-supervision.d.mts +26 -0
  253. package/src/service/desktop-supervision.mjs +140 -0
  254. package/src/service/managing-cli.ts +1 -0
  255. package/src/service/orchestration.ts +46 -20
  256. package/src/service/repair.ts +25 -9
  257. package/src/service/state.ts +4 -3
  258. package/src/service/systemd.ts +17 -8
  259. package/src/service/windows-ops.ts +61 -22
  260. package/src/storage/policy-job.ts +5 -1
  261. package/src/storage/policy-worker.ts +3 -1
  262. package/src/storage/policy.ts +9 -0
  263. package/src/types/config.ts +21 -0
  264. package/src/types/request.ts +10 -0
  265. package/src/types/wire.ts +1 -0
  266. package/src/update/index.ts +57 -9
  267. package/src/update/restart-ownership.ts +10 -3
  268. package/src/update/runtime-ownership.d.mts +6 -0
  269. package/src/update/runtime-ownership.mjs +23 -7
  270. package/src/usage/expected-prices.ts +1 -1
  271. package/src/web-search/loop.ts +5 -2
  272. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -21,6 +21,7 @@ import {
21
21
  } from "./live-sideband";
22
22
  import {
23
23
  withRequestLogId,
24
+ withMessagesRequestLogId,
24
25
  } from "./startup-warnings";
25
26
 
26
27
  import { remoteWorkspaceEnabled } from "../../remote-control/workspace-activation";
@@ -118,6 +119,7 @@ import { handleChatCompletions } from "../chat-completions";
118
119
  import { anthropicErrorResponse } from "../../claude/outbound";
119
120
  import {
120
121
  buildDesktop3pRegistry,
122
+ desktop3pModelOptions,
121
123
  generateDesktop3pModels,
122
124
  } from "../../claude/desktop-3p";
123
125
  import { buildDesktopDiscoveryInputs } from "../../claude/desktop-discovery-inputs";
@@ -1024,6 +1026,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1024
1026
  const models = config.claudeCode?.enabled === false ? [] : generateDesktop3pModels(
1025
1027
  desktopInputs.nativeSlugs, desktopInputs.routedModels,
1026
1028
  config.claudeCode?.desktopProfile, desktopInputs.nativeContextCap,
1029
+ desktop3pModelOptions(config.claudeCode),
1027
1030
  );
1028
1031
  const response = jsonResponse({ version: 1, models }, 200, req, policy);
1029
1032
  response.headers.set("Cache-Control", "no-store");
@@ -1556,11 +1559,16 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1556
1559
  // pre-translation stream + native passthrough callbacks) — do not re-wrap the
1557
1560
  // translated Anthropic stream here.
1558
1561
  const sessionReq = withCallerSessionIdentity(req, admission);
1559
- return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => withCors(
1562
+ // Entering the handler, or a logged refusal, means the final request log owns requestId.
1563
+ let ownsRequestLog = false;
1564
+ const claimRequestLog = (work: (lease: ActiveTurnLease) => Promise<Response>) =>
1565
+ (lease: ActiveTurnLease) => { ownsRequestLog = true; return work(lease); };
1566
+ const response = await runAdmittedHttpTurn(sessionReq, policy, claimRequestLog(async turnAdmissionLease => withCors(
1560
1567
  await handleClaudeMessages(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
1561
1568
  req,
1562
1569
  policy,
1563
- ), { requestId, start, logCtx });
1570
+ )), { requestId, start, logCtx, onLogged: () => { ownsRequestLog = true; } });
1571
+ return ownsRequestLog ? withMessagesRequestLogId(response, requestId) : response;
1564
1572
  }
1565
1573
 
1566
1574
  // OpenAI Chat Completions inbound (GitHub Copilot App / OpenAI-compatible clients).
@@ -1586,8 +1594,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1586
1594
  // `policy`, not `config`: this route is now served on the unauthenticated loopback
1587
1595
  // listener too (#4236), and only the receiving listener's view produces CORS headers
1588
1596
  // that match the admission decision made above.
1589
- return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1590
- await handleChatCompletions(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
1597
+ // Promote a caller `x-session-id` before admission, as Responses and Messages do (#6520):
1598
+ // the ChatGPT bridge forwards only canonical session headers, so the raw marker is lost.
1599
+ const sessionReq = withCallerSessionIdentity(req, admission);
1600
+ return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => withCors(
1601
+ await handleChatCompletions(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }),
1591
1602
  req,
1592
1603
  policy,
1593
1604
  ), { requestId, start, logCtx });
@@ -1708,7 +1719,9 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1708
1719
  if (audioController) registerTurn(audioController, turnAdmissionLease);
1709
1720
  const acquisition = audioController
1710
1721
  ? clearableDeadline(120_000, AbortSignal.any([req.signal, audioController.signal])) : undefined;
1722
+ const detachClientAbort = turnAdmissionLease.bindAbortSignal(req.signal);
1711
1723
  const releaseAcquisition = () => {
1724
+ detachClientAbort();
1712
1725
  acquisition?.clear();
1713
1726
  if (audioController) unregisterTurn(audioController);
1714
1727
  else turnAdmissionLease.release();
@@ -1827,6 +1840,7 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1827
1840
  return withCors(formatErrorResponse(502, "upstream_error", "Audio WebSocket upgrade failed"), req, policy);
1828
1841
  }
1829
1842
  if (upgraded) {
1843
+ detachClientAbort();
1830
1844
  acquisition?.clear();
1831
1845
  finalizeLiveRequest(101);
1832
1846
  return undefined as unknown as Response;
@@ -37,7 +37,8 @@ export interface SpendLedgerServerLifecycle {
37
37
  export function acquireSpendLedgerServerLifecycle(configDir: string): SpendLedgerServerLifecycle {
38
38
  const owner: SpendLedgerOwnerLease = acquireSpendLedgerOwner(configDir);
39
39
  // Advisory: a synced state directory makes the journal's hard-link guard refuse intermittently
40
- // (#6314). Said once at startup instead of being discovered from a 502.
40
+ // (#6314), including Google Drive syncing the native Desktop or Documents folder. Said once
41
+ // at startup instead of being discovered from a 502. The nlink guard itself is unchanged.
41
42
  warnIfSyncedStateDirectory(configDir);
42
43
  // Each entry returns whatever the listener's own stop returned. Typed as void-or-promise
43
44
  // because the rollback below has to WAIT on it: declaring it `() => void` let the call site
@@ -100,6 +100,25 @@ import {
100
100
 
101
101
  const REQUEST_LOG_ID_RESPONSE_HEADER = "x-opencodex-request-id";
102
102
 
103
+ /** Messages clients persist request-id; the upstream diagnostic id has a separate namespace. */
104
+ export function withMessagesRequestLogId(response: Response, requestId: string): Response {
105
+ const upstreamId = response.headers.get("request-id");
106
+ const correlated = withRequestLogId(response, requestId);
107
+ correlated.headers.delete("x-opencodex-upstream-request-id");
108
+ if (upstreamId && /^req_[A-Za-z0-9_-]{1,128}$/.test(upstreamId)) {
109
+ correlated.headers.set("x-opencodex-upstream-request-id", upstreamId);
110
+ }
111
+ correlated.headers.set("request-id", requestId);
112
+ const exposed = correlated.headers.get("Access-Control-Expose-Headers") ?? "";
113
+ const names = exposed.split(",").map(name => name.trim().toLowerCase());
114
+ const additions = ["request-id", ...(correlated.headers.has("x-opencodex-upstream-request-id")
115
+ ? ["x-opencodex-upstream-request-id"] : [])].filter(name => !names.includes(name));
116
+ if (additions.length) {
117
+ correlated.headers.set("Access-Control-Expose-Headers", [exposed, ...additions].filter(Boolean).join(", "));
118
+ }
119
+ return correlated;
120
+ }
121
+
103
122
  export function withRequestLogId(response: Response, requestId: string): Response {
104
123
  const headers = new Headers(response.headers);
105
124
  headers.set(REQUEST_LOG_ID_RESPONSE_HEADER, requestId);
@@ -502,7 +502,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
502
502
  work: (lease: ActiveTurnLease) => Promise<Response>,
503
503
  refusalLog?: WorkflowRefusalLog,
504
504
  ): Promise<Response> {
505
- const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
505
+ const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers), req.signal);
506
506
  if (!lease) return serverBusyResponse(req, "active turns", policy);
507
507
  // Root, lane, and the refusal that follows from them, all live in ./workflow-refusal.
508
508
  let workflow: ReturnType<typeof admitHttpWorkflowTurn>;
@@ -750,6 +750,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
750
750
  Object.defineProperty(server, "stop", {
751
751
  configurable: true,
752
752
  value: async (closeActiveConnections?: boolean): Promise<void> => {
753
+ backgroundLifecycle?.revokeClientFanout();
753
754
  remoteWorkspaceStopping = true;
754
755
  liveCallBindings.clear();
755
756
  // Disarm the package-tree restart timer before teardown can schedule another restart.
@@ -36,6 +36,7 @@ export const SESSION_LANE_ID_BYTES = 32;
36
36
  const turnGate = createAdmissionGate("active_turns", MAX_ACTIVE_TURNS);
37
37
  export interface ActiveTurnLease extends AdmissionLease {
38
38
  attach(lease: AdmissionLease): void;
39
+ bindAbortSignal(signal: AbortSignal): () => void;
39
40
  bindAbortController(ac: AbortController): void;
40
41
  beginCodexAccountSelection(): CodexAccountSelectionAdmission;
41
42
  isTransferred(): boolean;
@@ -169,7 +170,7 @@ export function resetLifecycleDrainStateForTests(): void {
169
170
  serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
170
171
  releaseServerStartupLifecycleImpl = releaseNativeMainStartupLifecycle;
171
172
  }
172
- export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
173
+ export function tryAdmitTurn(sessionLaneId?: string, clientSignal?: AbortSignal): ActiveTurnLease | null {
173
174
  if (isDraining()) return null;
174
175
  const opaqueSessionLaneId = sessionLaneId
175
176
  ? createHash("sha256").update(sessionLaneId).digest("hex").slice(0, SESSION_LANE_ID_BYTES)
@@ -192,6 +193,8 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
192
193
  }
193
194
  const controllers = new Set<AbortController>();
194
195
  const attachedLeases = new Set<AdmissionLease>();
196
+ const abortListeners = new Map<AbortSignal, () => void>();
197
+ let cancellationReason: unknown;
195
198
  let active = true;
196
199
  let transferred = false;
197
200
  let nativeMainClaimed = false;
@@ -200,22 +203,40 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
200
203
  if (!active) attachedLease.release();
201
204
  else attachedLeases.add(attachedLease);
202
205
  },
206
+ bindAbortSignal(signal) {
207
+ if (!active || abortListeners.has(signal)) return () => {};
208
+ const onAbort = () => {
209
+ cancellationReason = signal.reason;
210
+ const boundControllers = [...controllers];
211
+ lease.release();
212
+ for (const controller of boundControllers) controller.abort(cancellationReason);
213
+ };
214
+ abortListeners.set(signal, onAbort);
215
+ signal.addEventListener("abort", onAbort, { once: true });
216
+ if (signal.aborted) onAbort();
217
+ return () => {
218
+ if (abortListeners.get(signal) !== onAbort) return;
219
+ signal.removeEventListener("abort", onAbort);
220
+ abortListeners.delete(signal);
221
+ };
222
+ },
203
223
  bindAbortController(ac) {
204
224
  knownTurnControllers.add(ac);
205
225
  if (!active) {
206
- ac.abort(new Error("turn already settled"));
226
+ ac.abort(cancellationReason ?? new Error("turn already settled"));
207
227
  return;
208
228
  }
209
229
  transferred = true;
210
230
  controllers.add(ac);
211
231
  activeTurns.set(ac, lease);
232
+ lease.bindAbortSignal(ac.signal);
212
233
  },
213
234
  beginCodexAccountSelection() {
214
235
  const mainProfileDraining = nativeMainDrainOwners.size > 0;
215
- let selectionActive = !mainProfileDraining;
236
+ let selectionActive = active && !mainProfileDraining;
216
237
  let released = false;
217
238
  if (selectionActive) nativeMainSelections += 1;
218
- return {
239
+ const selection: CodexAccountSelectionAdmission = {
219
240
  mainProfileDraining,
220
241
  claimMainProfile() {
221
242
  if (released || mainProfileDraining || !active) return false;
@@ -228,18 +249,23 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
228
249
  release() {
229
250
  if (released) return;
230
251
  released = true;
252
+ attachedLeases.delete(selection);
231
253
  if (selectionActive) {
232
254
  selectionActive = false;
233
255
  nativeMainSelections = Math.max(0, nativeMainSelections - 1);
234
256
  }
235
257
  },
236
258
  };
259
+ lease.attach(selection);
260
+ return selection;
237
261
  },
238
262
  isTransferred() { return transferred; },
239
263
  release() {
240
264
  if (!active) return;
241
265
  active = false;
242
266
  admittedTurns.delete(lease);
267
+ for (const [signal, listener] of abortListeners) signal.removeEventListener("abort", listener);
268
+ abortListeners.clear();
243
269
  for (const controller of controllers) {
244
270
  if (activeTurns.get(controller) === lease) activeTurns.delete(controller);
245
271
  }
@@ -258,6 +284,7 @@ export function tryAdmitTurn(sessionLaneId?: string): ActiveTurnLease | null {
258
284
  },
259
285
  };
260
286
  admittedTurns.add(lease);
287
+ if (clientSignal) lease.bindAbortSignal(clientSignal);
261
288
  return lease;
262
289
  }
263
290
  export function codexAccountSelectionForTurn(
@@ -3,6 +3,9 @@ import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-sta
3
3
  import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { readFileSync } from "node:fs";
6
+ import { dirname } from "node:path";
7
+ import { prepareCodexHome } from "../../codex/prepared-home";
8
+ import { codexHomeIsAbsent } from "../../codex/codex-home-owner";
6
9
  import type { CatalogModel } from "../../codex/catalog";
7
10
  import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
8
11
  import { mergeModelPinnedEfforts, modelPinnedEffortsConfigError } from "../../config/provider-validation";
@@ -400,6 +403,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
400
403
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
401
404
  getMultiAgentModeHintText, probeCodexSupportsModeHint, setAgentsEnabled, setAgentsMaxDepth,
402
405
  setSubagentDeveloperInstructions, setMultiAgentModeHintText, MODE_HINT_UNSUPPORTED_ERROR,
406
+ activeCodexConfigPath,
403
407
  } = await import("../../codex/features");
404
408
  // Probe the capability before any combined-request mutation. The scalar writer
405
409
  // repeats this check, but doing it here prevents an earlier flag/mode/agents
@@ -419,17 +423,37 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
419
423
  const requestedFlag = wantsFlag
420
424
  ? body.enabled as boolean
421
425
  : modeFlag ?? (wantsKeepNative && hybridPinActive ? false : undefined);
426
+ /*
427
+ * One held section for every config.toml write this PUT makes — the
428
+ * feature transition plus each scalar edit. Individually-locked writers
429
+ * would let a foreign write land between them and leave a half-applied
430
+ * combination on disk; the lock is released before the post-write readers
431
+ * and catalog converge below.
432
+ */
433
+ const needsCodexConfigWrites = requestedFlag !== undefined || wantsThreads
434
+ || wantsAgentsEnabled || wantsMaxDepth || wantsSubagentInstructions || wantsModeHintText;
435
+ if (needsCodexConfigWrites && codexHomeIsAbsent(dirname(activeCodexConfigPath()))) {
436
+ return jsonResponse({ error: `config.toml not readable at ${activeCodexConfigPath()}` }, 502);
437
+ }
438
+ const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage } = await import("../../codex/config-write-lock");
439
+ const configWriteLock = needsCodexConfigWrites ? await acquireConfigWriteLock(activeCodexConfigPath()) : null;
440
+ if (configWriteLock !== null && !configWriteLock.ok) {
441
+ return jsonResponse({ error: configWriteLockFailureMessage(configWriteLock), retryable: configWriteLock.error === "locked" }, 502);
442
+ }
443
+ const heldWriteLock = configWriteLock !== null && configWriteLock.ok ? configWriteLock.handle : undefined;
444
+ try {
422
445
  if (requestedFlag !== undefined || wantsThreads) {
423
446
  const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
424
447
  let toggle = deps.toggleCodexMultiAgentV2;
425
448
  if (!toggle) {
426
449
  const { runCodexFeaturesCommand } = await import("../../cli/v2");
427
- toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable");
450
+ toggle = (enabled, env, validate) => runCodexFeaturesCommand(enabled ? "enable" : "disable", "multi_agent_v2", env, validate);
428
451
  }
429
452
  const result = transitionMultiAgentV2(targetFlag, toggle, {
430
453
  ...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
454
+ heldConfigWriteLock: heldWriteLock,
431
455
  });
432
- if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}` }, 502);
456
+ if (!result.ok) return jsonResponse({ error: `multi_agent_v2 transition failed: ${result.error}`, ...(result.retryable === false ? { retryable: false } : {}) }, 502);
433
457
  if (result.changed && result.threadLimit !== null) warnings.push(`Thread limit ${result.threadLimit} preserved for ${targetFlag ? "v2" : "v1"}.`);
434
458
  }
435
459
  if (wantsMode) {
@@ -465,10 +489,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
465
489
  // asserts this route file contains no direct write primitive, and matches on the
466
490
  // symbol name even inside a comment.
467
491
  const scalarWrites: Array<{ field: string; run: () => { ok: true; changed: boolean } | { ok: false; error: string } }> = [];
468
- if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
469
- if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
470
- if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
471
- if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null) });
492
+ if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null, undefined, heldWriteLock) });
493
+ if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null, undefined, heldWriteLock) });
494
+ if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null, undefined, heldWriteLock) });
495
+ if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null, undefined, heldWriteLock) });
472
496
  const landed: string[] = [];
473
497
  for (const write of scalarWrites) {
474
498
  try {
@@ -482,6 +506,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
482
506
  return jsonResponse({ error: `writing ${write.field} failed: ${message}${landed.length > 0 ? ` (already applied: ${landed.join(", ")})` : ""}` }, 502);
483
507
  }
484
508
  }
509
+ } finally {
510
+ if (heldWriteLock) releaseConfigWriteLock(heldWriteLock);
511
+ }
485
512
  // Derived from fresh post-write readers (readConfigText is uncached): upstream
486
513
  // lets an enabled multi_agent_v2 feature override [agents].enabled = false, so
487
514
  // warn rather than reject — silently accepting would imply multi-agent is off.
@@ -533,35 +560,59 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
533
560
  }
534
561
  const body = parsedBody as { enabled?: unknown };
535
562
  if (typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
536
- const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
563
+ const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, activeCodexConfigPath } = await import("../../codex/features");
537
564
  const before = isDefaultModeRequestUserInputEnabled();
538
565
  let toggle = deps.toggleDefaultModeRequestUserInput;
539
566
  if (!toggle) {
540
- const { runCodexFeaturesCommand } = await import("../../cli/v2");
541
- toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable", DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY);
542
- }
543
- let toggleError: string | null = null;
544
- try {
545
- toggle(body.enabled);
546
- } catch (error) {
547
- const err = error as { stderr?: unknown; message?: string };
548
- const raw = err.stderr;
549
- const stderrText = typeof raw === "string"
550
- ? raw.trim()
551
- : raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
552
- toggleError = stderrText || (err.message ?? String(error));
553
- }
554
- const enabled = isDefaultModeRequestUserInputEnabled();
555
- if (toggleError !== null || enabled !== body.enabled) {
556
- const reason = toggleError
557
- ?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
558
- return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
567
+ try {
568
+ const { codexFeaturesInvocation, runCodexFeaturesCommand } = await import("../../cli/v2");
569
+ const action = body.enabled ? "enable" : "disable";
570
+ const invocation = codexFeaturesInvocation(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, process.platform, { requireAvailable: true });
571
+ toggle = (_enabled, env, validate) => runCodexFeaturesCommand(action, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY, env, validate, invocation);
572
+ } catch (error) {
573
+ return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${error instanceof Error ? error.message : String(error)}` }, 502);
574
+ }
559
575
  }
560
- const warnings: string[] = [];
561
- if (enabled !== before) {
562
- warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
576
+ // The `codex features` subprocess rewrites config.toml itself — run it
577
+ // under the shared write lock so it cannot interleave with an opencodex
578
+ // scalar edit or injection mid-write on either side.
579
+ const { acquireConfigWriteLock, releaseConfigWriteLock, configWriteLockFailureMessage, runConfigWriteChild, ConfigWriteDestinationChanged } = await import("../../codex/config-write-lock");
580
+ const configPath = activeCodexConfigPath();
581
+ // Preserve native home creation only after executable resolution succeeds.
582
+ prepareCodexHome(dirname(configPath));
583
+ {
584
+ const configLock = await acquireConfigWriteLock(configPath);
585
+ if (!configLock.ok) {
586
+ return jsonResponse({ error: configWriteLockFailureMessage(configLock), retryable: configLock.error === "locked" }, 502);
587
+ }
588
+ let toggleError: string | null = null;
589
+ let destinationChanged = false;
590
+ try {
591
+ runConfigWriteChild(configPath, configLock.handle, (env, validate) => toggle!(body.enabled as boolean, env, validate));
592
+ } catch (error) {
593
+ destinationChanged = error instanceof ConfigWriteDestinationChanged;
594
+ const err = error as { stderr?: unknown; message?: string };
595
+ const raw = err.stderr;
596
+ const stderrText = typeof raw === "string"
597
+ ? raw.trim()
598
+ : raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
599
+ toggleError = stderrText || (err.message ?? String(error));
600
+ } finally {
601
+ releaseConfigWriteLock(configLock.handle);
602
+ }
603
+ if (destinationChanged) return jsonResponse({ error: toggleError, retryable: false }, 502);
604
+ const enabled = isDefaultModeRequestUserInputEnabled();
605
+ if (toggleError !== null || enabled !== body.enabled) {
606
+ const reason = toggleError
607
+ ?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
608
+ return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
609
+ }
610
+ const warnings: string[] = [];
611
+ if (enabled !== before) {
612
+ warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
613
+ }
614
+ return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
563
615
  }
564
- return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
565
616
  }
566
617
 
567
618
  // Subagent prompt injection model: single native or routed model whose info is
@@ -1421,6 +1472,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1421
1472
  alwaysEnableEffort: config.claudeCode?.alwaysEnableEffort === true,
1422
1473
  autoContext: config.claudeCode?.autoContext !== false,
1423
1474
  autoCompactWindow: config.claudeCode?.autoCompactWindow ?? null,
1475
+ contextAccounting: config.claudeCode?.contextAccounting === "200k" ? "200k" : "1m",
1424
1476
  blockedSkills: config.claudeCode?.blockedSkills ?? null,
1425
1477
  injectAgents: config.claudeCode?.injectAgents !== false,
1426
1478
  sidecarPools: {
@@ -1457,7 +1509,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1457
1509
  return prototype === Object.prototype || prototype === null;
1458
1510
  };
1459
1511
  if (!isPlainObject(parsedBody)) return jsonResponse({ error: "body must be an object" }, 400);
1460
- const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
1512
+ const body = parsedBody as { enabled?: unknown; cliFirstParty?: unknown; authMode?: unknown; model?: unknown; smallFastModel?: unknown; modelMap?: unknown; classifierModel?: unknown; classifierFallbacks?: unknown; systemEnv?: unknown; fastMode?: unknown; maxContextTokens?: unknown; alwaysEnableEffort?: unknown; tierModels?: unknown; autoContext?: unknown; autoCompactWindow?: unknown; contextAccounting?: unknown; blockedSkills?: unknown; injectAgents?: unknown; webSearchSidecar?: unknown; visionSidecar?: unknown };
1461
1513
  if (body.cliFirstParty !== undefined && typeof body.cliFirstParty !== "boolean")
1462
1514
  return jsonResponse({ error: "cliFirstParty must be a boolean" }, 400);
1463
1515
  if (body.cliFirstParty !== undefined && Object.keys(body).length !== 1)
@@ -1727,6 +1779,15 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1727
1779
  if (body.autoContext) delete next.autoContext;
1728
1780
  else next.autoContext = false;
1729
1781
  }
1782
+ if (body.contextAccounting !== undefined) {
1783
+ // Sparse like autoContext (devlog/_plan/261009_claude_1m_default/030): "1m" is the default
1784
+ // and drops the key; "200k" is the only stored value.
1785
+ if (body.contextAccounting !== "1m" && body.contextAccounting !== "200k") {
1786
+ return jsonResponse({ error: "contextAccounting must be \"1m\" or \"200k\"" }, 400);
1787
+ }
1788
+ if (body.contextAccounting === "200k") next.contextAccounting = "200k";
1789
+ else delete next.contextAccounting;
1790
+ }
1730
1791
  if (body.injectAgents !== undefined) {
1731
1792
  // Default-on boolean (devlog 260712 070): true = drop the key, false = store.
1732
1793
  if (typeof body.injectAgents !== "boolean") return jsonResponse({ error: "injectAgents must be a boolean" }, 400);
@@ -1841,7 +1902,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1841
1902
  // levers feed the same injection, so a changed or cleared slot must not linger in
1842
1903
  // launchd until the next restart.
1843
1904
  const systemEnvInputs = ["systemEnv", "authMode", "model", "smallFastModel", "tierModels",
1844
- "maxContextTokens", "alwaysEnableEffort", "autoContext", "autoCompactWindow"] as const;
1905
+ "maxContextTokens", "alwaysEnableEffort", "autoContext", "autoCompactWindow", "contextAccounting"] as const;
1845
1906
  if (systemEnvInputs.some(field => body[field] !== undefined)) {
1846
1907
  try {
1847
1908
  await applySystemEnvToggle(config, config.port);
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Per-role model for omo (Codex / LazyCodex): `$CODEX_HOME/agents/<role>.toml` plus LazyCodex's
3
- * `codex.agents.<role>.model` mirror in omo.jsonc. Both halves exist only when LazyCodex is
3
+ * `"[codex]".agents.<role>` model and reasoning mirror in omo.jsonc. Both halves exist only when LazyCodex is
4
4
  * detected; Pi-based and OpenCode-based omo are never read here.
5
5
  *
6
6
  * Loaded on demand from `src/server/management-api.ts`, like the quota-reset handler, so a
@@ -40,6 +40,7 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
40
40
  omoJsonc: { state: omoState.state },
41
41
  roles: roles.listCodexAgentRoleModels(codexHome).map(entry => ({
42
42
  ...entry,
43
+ effort: roles.readCodexAgentRoleEffort(entry.role, codexHome),
43
44
  omoJsoncModel: omoModels[entry.role] ?? null,
44
45
  })),
45
46
  }, 200, req, config);
@@ -114,7 +115,8 @@ export async function handleCodexAgentRoleRoutes(ctx: ManagementContext): Promis
114
115
  // The role TOML is what Codex obeys, so its write stands even when the omo mirror cannot follow.
115
116
  let omoStatus: ReturnType<typeof omo.writeOmoRoleModel> | "write_failed";
116
117
  try {
117
- omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath());
118
+ const reasoning = effort === undefined ? undefined : omo.omoReasoningFor(effort);
119
+ omoStatus = omo.writeOmoRoleModel(role, model, omo.omoJsoncPath(), reasoning);
118
120
  } catch {
119
121
  omoStatus = "write_failed";
120
122
  }
@@ -83,6 +83,7 @@ const WRITE_ERROR_STATUS: Record<WriteError, number> = {
83
83
  write_failed: 500,
84
84
  recovery_required: 409,
85
85
  locked: 409,
86
+ unsafe: 422,
86
87
  };
87
88
 
88
89
  /** Read-only view for the route test that asserts every mapping is a client error. */
@@ -58,6 +58,10 @@ async function modelCandidates(slugs: readonly string[], models: readonly Catalo
58
58
  ]);
59
59
  const nativeSlugs = catalog.listCatalogNativeSlugs();
60
60
  const routed = new Map(models.map(model => [catalog.catalogModelSlug(model), model]));
61
+ // A routed model that declares no levels still gets the ladder Codex shows for it, the same one
62
+ // the Models page lists, so its proposal carries an effort instead of none. An explicit empty
63
+ // ladder means the model takes no effort control, and the written catalog lists none for it.
64
+ const written = catalog.catalogModelLadders(slugs);
61
65
  const unitPrice = (provider: string, modelId: string): number | null => {
62
66
  const cost = resolveMatchedPrice(provider, modelId)?.cost4;
63
67
  return cost ? cost.input + cost.output : null;
@@ -65,11 +69,18 @@ async function modelCandidates(slugs: readonly string[], models: readonly Catalo
65
69
  return slugs.map(slug => {
66
70
  const row = routed.get(slug);
67
71
  if (row) {
72
+ const ladder = written.get(slug);
73
+ const efforts = Array.isArray(row.reasoningEfforts) ? row.reasoningEfforts : ladder?.efforts ?? [];
74
+ // A row without its own default keeps the one Codex shows, when that rung is on the ladder.
75
+ const shownDefault = ladder?.defaultEffort !== undefined && efforts.includes(ladder.defaultEffort)
76
+ ? ladder.defaultEffort
77
+ : undefined;
78
+ const defaultEffort = row.defaultReasoningEffort ?? shownDefault;
68
79
  return {
69
80
  model: slug,
70
81
  unitPrice: unitPrice(row.provider, row.id),
71
- efforts: row.reasoningEfforts ?? [],
72
- ...(row.defaultReasoningEffort ? { defaultEffort: row.defaultReasoningEffort } : {}),
82
+ efforts,
83
+ ...(defaultEffort ? { defaultEffort } : {}),
73
84
  };
74
85
  }
75
86
  if (nativeSlugs.includes(slug)) {