@bitkyc08/opencodex 2.81.0 → 2.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/bin/ocx.mjs +166 -73
  2. package/gui/dist/assets/App-Ds_qL7o8.js +54 -0
  3. package/gui/dist/assets/{Tray-EwAOsP0S.js → Tray-BqHF5Trd.js} +1 -1
  4. package/gui/dist/assets/{index-Brk6F3fn.css → index-BAbcj51J.css} +1 -1
  5. package/gui/dist/assets/{index-RsoaAxXY.js → index-D3FBCNiP.js} +13 -13
  6. package/gui/dist/assets/{quota-summary-D2lN8tDm.js → quota-summary-DTBC8n7-.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic.ts +1 -1
  10. package/src/adapters/openai-responses/compaction-search-history.ts +109 -0
  11. package/src/adapters/openai-responses/passthrough.ts +9 -4
  12. package/src/adapters/openai-responses/tool-output-recovery.ts +20 -0
  13. package/src/claude/agents-inject.ts +14 -3
  14. package/src/claude/context-windows.ts +19 -17
  15. package/src/claude/desktop-3p.ts +88 -12
  16. package/src/claude/desktop-profile.ts +2 -1
  17. package/src/claude/gateway-cache.ts +18 -1
  18. package/src/claude/inbound.ts +77 -3
  19. package/src/claude/intercept/cli-catalog.ts +3 -0
  20. package/src/claude/intercept/picker-ca-persistence.ts +7 -6
  21. package/src/claude/intercept/picker-models.ts +14 -6
  22. package/src/claude/intercept/settings.ts +3 -0
  23. package/src/claude/long-context.ts +66 -0
  24. package/src/claude/model-info.ts +19 -16
  25. package/src/claude/outbound.ts +94 -9
  26. package/src/claude/subagent-model.ts +40 -11
  27. package/src/cli/agent.ts +9 -5
  28. package/src/cli/aside-profile-recovery.ts +33 -0
  29. package/src/cli/capabilities-base.ts +5 -5
  30. package/src/cli/capabilities-integrations.ts +2 -2
  31. package/src/cli/cli-path-diagnostics.ts +132 -0
  32. package/src/cli/desktop-runtime-guidance.ts +65 -0
  33. package/src/cli/dispatch.ts +5 -0
  34. package/src/cli/doctor.ts +16 -3
  35. package/src/cli/index.ts +14 -9
  36. package/src/cli/integration-aside-sync.ts +2 -1
  37. package/src/cli/integrations.ts +16 -3
  38. package/src/cli/resolve.ts +25 -2
  39. package/src/cli/root.ts +4 -2
  40. package/src/cli/status.ts +64 -12
  41. package/src/cli/update-restart-child.ts +4 -2
  42. package/src/cli/update-restart-home.ts +28 -10
  43. package/src/cli/update-restart-service-record.ts +116 -0
  44. package/src/cli/update-restart-supervision.ts +96 -0
  45. package/src/cli/update-restart.ts +59 -13
  46. package/src/cli/v2.ts +27 -20
  47. package/src/cli/version-skew-notice.ts +77 -0
  48. package/src/clients/lazycodex.ts +1 -1
  49. package/src/clients/omo-role-models.ts +32 -7
  50. package/src/codex/auth-api/account-selection.ts +107 -0
  51. package/src/codex/auth-api/routes.ts +10 -49
  52. package/src/codex/auth-context.ts +20 -1
  53. package/src/codex/autostart-health.ts +12 -3
  54. package/src/codex/catalog/build-entries.ts +18 -0
  55. package/src/codex/catalog/bundled.ts +155 -26
  56. package/src/codex/catalog/combo-member.ts +3 -2
  57. package/src/codex/catalog/effort.ts +26 -5
  58. package/src/codex/catalog/gather-capture.ts +5 -0
  59. package/src/codex/catalog/model-hints.ts +15 -8
  60. package/src/codex/catalog/model-visibility.ts +3 -1
  61. package/src/codex/catalog/operator-tiers.ts +38 -0
  62. package/src/codex/catalog/provider-models.ts +43 -35
  63. package/src/codex/catalog/retained-sync.ts +7 -5
  64. package/src/codex/catalog/routed-gather.ts +15 -1
  65. package/src/codex/catalog-auto-refresh-sources.ts +3 -4
  66. package/src/codex/catalog-auto-refresh.ts +19 -0
  67. package/src/codex/catalog.ts +1 -1
  68. package/src/codex/codex-home-owner.ts +3 -0
  69. package/src/codex/config-write-lock.ts +333 -0
  70. package/src/codex/convergence.ts +2 -1
  71. package/src/codex/features.ts +257 -200
  72. package/src/codex/inject/config-write-section.ts +34 -0
  73. package/src/codex/inject/multi-agent-v2.ts +14 -6
  74. package/src/codex/inject/remove.ts +36 -11
  75. package/src/codex/inject/restore.ts +85 -20
  76. package/src/codex/inject-coordination.ts +15 -6
  77. package/src/codex/inject.ts +62 -20
  78. package/src/codex/journal.ts +62 -17
  79. package/src/codex/main-account-cache.ts +43 -0
  80. package/src/codex/paths.ts +2 -0
  81. package/src/codex/prepared-home.ts +18 -0
  82. package/src/codex/prompt-journal.ts +25 -12
  83. package/src/codex/prompt-layers.ts +49 -26
  84. package/src/codex/prompt-lock-claim.ts +134 -0
  85. package/src/codex/prompt-lock-evidence.ts +26 -0
  86. package/src/codex/prompt-lock-io.ts +16 -0
  87. package/src/codex/prompt-lock-owner.ts +130 -0
  88. package/src/codex/prompt-lock.ts +84 -53
  89. package/src/codex/routing/failure-window.ts +146 -0
  90. package/src/codex/routing/selection.ts +4 -1
  91. package/src/codex/routing.ts +13 -17
  92. package/src/codex/runtime.ts +106 -18
  93. package/src/codex/shim.ts +45 -19
  94. package/src/combos/jev-service-exchange.ts +281 -0
  95. package/src/combos/jev.ts +34 -220
  96. package/src/config/atomic-write.ts +1 -1
  97. package/src/config/codex-ws-reuse-setting.ts +10 -0
  98. package/src/config/live-reconcile.ts +17 -0
  99. package/src/config/load-degrade.ts +8 -0
  100. package/src/config/rebase-provenance.ts +11 -0
  101. package/src/config/schema/config-schema.ts +4 -0
  102. package/src/generated/compatibility-version.json +303 -171
  103. package/src/generated/model-metadata.ts +2 -2
  104. package/src/grok/inject.ts +50 -4
  105. package/src/grok/sync.ts +2 -1
  106. package/src/images/loop.ts +29 -11
  107. package/src/integrations/aside-profiles.ts +10 -3
  108. package/src/integrations/catalog-refresh.ts +18 -2
  109. package/src/integrations/writer.ts +9 -3
  110. package/src/lib/bun-path-runtime.d.mts +32 -0
  111. package/src/lib/bun-path-runtime.mjs +95 -0
  112. package/src/lib/bun-runtime-preflight.ts +140 -0
  113. package/src/lib/desktop-cli-handoff.d.mts +20 -0
  114. package/src/lib/desktop-cli-handoff.mjs +92 -0
  115. package/src/lib/desktop-cli-record.d.mts +29 -0
  116. package/src/lib/desktop-cli-record.mjs +102 -0
  117. package/src/lib/request-execution-budget.ts +8 -0
  118. package/src/lib/synced-state-location.ts +46 -6
  119. package/src/lib/test-home-guard.ts +87 -1
  120. package/src/lib/translator-budget.ts +17 -4
  121. package/src/lib/windows-atomic-replace.ts +38 -16
  122. package/src/lib/windows-elevation.ts +6 -0
  123. package/src/lib/windows-secret-acl.ts +92 -25
  124. package/src/lib/winsw.ts +2 -3
  125. package/src/oauth/anthropic-account-refusal.ts +39 -1
  126. package/src/oauth/anthropic-routing.ts +1 -1
  127. package/src/oauth/anthropic-send-ownership.ts +3 -3
  128. package/src/oauth/chatgpt.ts +113 -41
  129. package/src/oauth/index.ts +6 -2
  130. package/src/oauth/store.ts +1 -1
  131. package/src/providers/azure-model-metadata.ts +115 -0
  132. package/src/providers/derive.ts +21 -0
  133. package/src/providers/quota/vendor-probes-key.ts +127 -17
  134. package/src/providers/quota.ts +1 -1
  135. package/src/responses/reasoning-envelope.ts +29 -1
  136. package/src/responses/reasoning-replay-cache.ts +17 -1
  137. package/src/responses/spill-store.ts +15 -2
  138. package/src/responses/state/metrics.ts +8 -0
  139. package/src/responses/state/spill-failure.ts +12 -0
  140. package/src/responses/state/spill-queue.ts +12 -5
  141. package/src/responses/state.ts +18 -10
  142. package/src/responses/task-input.ts +17 -0
  143. package/src/server/background-lifecycle.ts +27 -0
  144. package/src/server/chat-completions.ts +1 -0
  145. package/src/server/chat-native.ts +28 -12
  146. package/src/server/claude-messages.ts +43 -6
  147. package/src/server/inbound-body-admission.ts +1 -0
  148. package/src/server/index/claude-intercept-lifecycle.ts +3 -1
  149. package/src/server/index/serve-options.ts +10 -2
  150. package/src/server/index/spend-ledger-lifecycle.ts +2 -1
  151. package/src/server/index/startup-warnings.ts +19 -0
  152. package/src/server/index.ts +1 -0
  153. package/src/server/management/agent-settings-routes.ts +93 -32
  154. package/src/server/management/codex-agent-role-routes.ts +4 -2
  155. package/src/server/management/codex-prompt-routes.ts +1 -0
  156. package/src/server/management/codex-role-auto-assign.ts +13 -2
  157. package/src/server/management/config-routes.ts +59 -8
  158. package/src/server/management/context.ts +2 -2
  159. package/src/server/management/shared.ts +3 -3
  160. package/src/server/messages-native-oauth.ts +9 -8
  161. package/src/server/messages-native.ts +49 -13
  162. package/src/server/messages-response-headers.ts +14 -0
  163. package/src/server/request-log-conversation.ts +48 -5
  164. package/src/server/request-log.ts +31 -2
  165. package/src/server/responses/adapter-continuation.ts +2 -2
  166. package/src/server/responses/adapter-dispatch.ts +34 -13
  167. package/src/server/responses/codex-ws-exchange.ts +17 -2
  168. package/src/server/responses/codex-ws-pool.ts +100 -13
  169. package/src/server/responses/codex-ws-wire.ts +9 -0
  170. package/src/server/responses/combo-requested-effort.ts +3 -1
  171. package/src/server/responses/core-codex-account.ts +22 -1
  172. package/src/server/responses/core-combo-native.ts +20 -3
  173. package/src/server/responses/core-combo.ts +22 -14
  174. package/src/server/responses/core-lifetime.ts +3 -36
  175. package/src/server/responses/core-options.ts +5 -0
  176. package/src/server/responses/core-replay.ts +73 -0
  177. package/src/server/responses/core.ts +3 -3
  178. package/src/server/responses/fetch-helpers.ts +26 -3
  179. package/src/server/responses/passthrough-delivery.ts +24 -2
  180. package/src/server/responses/passthrough-dispatch.ts +19 -2
  181. package/src/server/responses/request-prepare.ts +4 -0
  182. package/src/server/responses/request-send-budget.ts +112 -49
  183. package/src/server/responses/response-effects.ts +1 -0
  184. package/src/server/responses/run-turn-execution.ts +2 -0
  185. package/src/server/responses/sidecar-execution.ts +25 -16
  186. package/src/server/responses/sidecar-send-budget.ts +58 -0
  187. package/src/server/responses/ws-upstream.ts +19 -6
  188. package/src/server/startup-health-cache.ts +2 -1
  189. package/src/server/workflow-refusal.ts +3 -0
  190. package/src/service/cli.ts +20 -4
  191. package/src/service/desktop-command-guard.ts +32 -0
  192. package/src/service/desktop-startup.ts +66 -40
  193. package/src/service/desktop-supervision.d.mts +26 -0
  194. package/src/service/desktop-supervision.mjs +140 -0
  195. package/src/service/managing-cli.ts +1 -0
  196. package/src/service/orchestration.ts +46 -20
  197. package/src/service/repair.ts +25 -9
  198. package/src/service/state.ts +4 -3
  199. package/src/service/systemd.ts +17 -8
  200. package/src/service/windows-ops.ts +61 -22
  201. package/src/storage/policy-job.ts +5 -1
  202. package/src/storage/policy-worker.ts +3 -1
  203. package/src/storage/policy.ts +9 -0
  204. package/src/types/config.ts +21 -0
  205. package/src/types/request.ts +5 -0
  206. package/src/types/wire.ts +1 -0
  207. package/src/update/index.ts +57 -9
  208. package/src/update/restart-ownership.ts +10 -3
  209. package/src/update/runtime-ownership.d.mts +6 -0
  210. package/src/update/runtime-ownership.mjs +23 -7
  211. package/src/web-search/loop.ts +5 -2
  212. package/gui/dist/assets/App-7yhoys8a.js +0 -54
@@ -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)) {
@@ -9,6 +9,7 @@ import { isAnthropicInstanceId } from "../../providers/anthropic-instance-id";
9
9
  import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
10
10
  import type { IntegrationClientId } from "../../integrations/registry";
11
11
  import { randomUUID } from "node:crypto";
12
+ import { isDeepStrictEqual } from "node:util";
12
13
  import { readFileSync } from "node:fs";
13
14
  import type { CatalogModel } from "../../codex/catalog";
14
15
  import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
@@ -182,6 +183,17 @@ interface ClientIntegrationSyncOutcome {
182
183
  readonly profileId?: number;
183
184
  }
184
185
 
186
+ /** Only the Desktop write's own applied markers may change during a background projection. */
187
+ function clientProjectionConfig(config: OcxConfig): OcxConfig {
188
+ const snapshot = structuredClone(config);
189
+ const profile = snapshot.claudeCode?.desktopProfile;
190
+ if (profile) {
191
+ delete profile.appliedFingerprint;
192
+ delete profile.appliedAt;
193
+ }
194
+ return snapshot;
195
+ }
196
+
185
197
  /**
186
198
  * Re-inject native clients that are switched ON and every file integration whose
187
199
  * OpenCodex ownership record is the operator's durable opt-in.
@@ -205,17 +217,41 @@ export async function syncEnabledClientIntegrations(
205
217
  config: OcxConfig,
206
218
  deps: Pick<ManagementContext["deps"],
207
219
  "fetchAllModels" | "refreshOwnedCatalogIntegrations" | "writeDesktop3pConfig"> = {},
220
+ options: { unattended?: { isCurrent: () => boolean } } = {},
208
221
  ): Promise<ClientIntegrationSyncOutcome[]> {
209
222
  // A sibling instance passes its OWN port here; the Grok fence and the Desktop gateway profile
210
223
  // stay on the live owner's (`src/codex/sibling-start.ts`).
211
224
  if (port === undefined || siblingOfLivePort() !== null) return [];
212
- const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled } = await import("../../codex/desired-state");
225
+ const projectionConfig = options.unattended ? clientProjectionConfig(config) : undefined;
226
+ const { claudeDesktopIntegrationEnabled, grokIntegrationEnabled, localClientSyncAllowed } = await import("../../codex/desired-state");
213
227
  const out: ClientIntegrationSyncOutcome[] = [];
228
+ const unattended = options.unattended;
229
+ const admit = () => {
230
+ if (!unattended!.isCurrent()) return false;
231
+ const fresh = loadConfig();
232
+ return localClientSyncAllowed(fresh) && isDeepStrictEqual(clientProjectionConfig(fresh), projectionConfig);
233
+ };
234
+ const stale = () => unattended !== undefined && !admit();
235
+ if (stale()) return out;
236
+ const grok = unattended ? await import("../../grok/inject") : undefined;
214
237
 
215
- if (grokIntegrationEnabled(config)) {
238
+ if (grokIntegrationEnabled(config) && (!grok || grok.grokManagedBlockPresent())) {
216
239
  try {
217
240
  const { syncGrokConfig } = await import("../../grok/sync");
218
- const r = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {});
241
+ if (stale()) return out;
242
+ const grokDeps = grok ? {
243
+ fetchAllModels: deps.fetchAllModels ?? (await import("../management-api")).fetchAllModels,
244
+ injectGrokConfig: ((...args: Parameters<typeof grok.injectGrokConfig>) =>
245
+ admit() && grokIntegrationEnabled(loadConfig()) && grok.grokManagedBlockPresent()
246
+ ? grok.injectGrokConfig(...args)
247
+ : { ok: true, changed: false, message: "Grok refresh skipped" }),
248
+ } : undefined;
249
+ const r = unattended
250
+ ? await syncGrokConfig(port, config, {
251
+ ...config.hostname ? { hostname: config.hostname } : {},
252
+ refreshOnly: { admit },
253
+ }, grokDeps)
254
+ : await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {}, grokDeps);
219
255
  out.push(r.ok
220
256
  ? { client: "grok", ok: true, changed: r.changed === true }
221
257
  : { client: "grok", ok: false, reason: r.message });
@@ -224,9 +260,11 @@ export async function syncEnabledClientIntegrations(
224
260
  }
225
261
  }
226
262
 
263
+ if (stale()) return out;
227
264
  const { observeClaudeDesktopMode, resolveClaudeDesktopMode } = await import("../../claude/desktop-first-party");
228
265
  // A first-party Desktop must never get a gateway profile written and selected by a sync.
229
- if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party") {
266
+ if (claudeDesktopIntegrationEnabled(config) && resolveClaudeDesktopMode(config, observeClaudeDesktopMode(config)) !== "first-party"
267
+ && (!unattended || !!config.claudeCode?.desktopProfile?.appliedFingerprint)) {
230
268
  try {
231
269
  const { writeDesktop3pConfig } = await import("../../claude/desktop-3p");
232
270
  const { desktopVisibleNativeSlugs, filterCatalogVisibleModels } = await import("../../codex/catalog");
@@ -240,12 +278,15 @@ export async function syncEnabledClientIntegrations(
240
278
  const latest = loadConfig();
241
279
  // Discovery awaited: the mode may have changed meanwhile. Re-resolve on the fresh read,
242
280
  // immediately before the writer, so a first-party switch during fetchAllModels still wins.
243
- if (claudeDesktopIntegrationEnabled(latest) && resolveClaudeDesktopMode(latest, observeClaudeDesktopMode(latest)) !== "first-party") {
281
+ if (stale()) return;
282
+ if (claudeDesktopIntegrationEnabled(latest) && resolveClaudeDesktopMode(latest, observeClaudeDesktopMode(latest)) !== "first-party"
283
+ && (!unattended || !!latest.claudeCode?.desktopProfile?.appliedFingerprint)) {
244
284
  const routed = filterCatalogVisibleModels(models, latest)
245
285
  .map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow }));
246
286
  const writtenProfile = latest.claudeCode?.desktopProfile;
247
287
  const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
248
- const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
288
+ // Keep the attended writer's argument count unchanged, including its lock test seam.
289
+ const writeArgs: Parameters<typeof writeDesktop3pConfig> = [
249
290
  port,
250
291
  [...desktopVisibleNativeSlugs(latest)],
251
292
  routed,
@@ -253,7 +294,13 @@ export async function syncEnabledClientIntegrations(
253
294
  "static",
254
295
  writtenProfile,
255
296
  nativeContextLimits(latest),
256
- );
297
+ ];
298
+ if (unattended) {
299
+ writeArgs[7] = undefined;
300
+ writeArgs[8] = { appliedFingerprint: writtenProfile!.appliedFingerprint!, admit };
301
+ }
302
+ const r = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(...writeArgs);
303
+ if (unattended && r.reason === "desktop_refresh_only_skipped") return;
257
304
  if (!r.written || !r.fingerprint) {
258
305
  out.push({ client: "claude-desktop", ok: false, reason: r.reason ?? "Claude Desktop write failed" });
259
306
  } else {
@@ -271,6 +318,7 @@ export async function syncEnabledClientIntegrations(
271
318
  }
272
319
  }
273
320
 
321
+ if (stale()) return out;
274
322
  const { refreshOwnedCatalogIntegrations } = await import("../../integrations/catalog-refresh");
275
323
  const refreshOwned = deps.refreshOwnedCatalogIntegrations ?? refreshOwnedCatalogIntegrations;
276
324
  out.push(...await refreshOwned({
@@ -280,7 +328,10 @@ export async function syncEnabledClientIntegrations(
280
328
  },
281
329
  config,
282
330
  port,
283
- }, ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"]));
331
+ }, unattended
332
+ ? ["mcode", "pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"]
333
+ : ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"],
334
+ ...unattended ? [{ refreshOnly: true, admit }] as const : []));
284
335
 
285
336
  return out;
286
337
  }
@@ -64,8 +64,8 @@ export interface ManagementApiDeps {
64
64
  refreshOwnedCatalogIntegrations?: typeof refreshOwnedCatalogIntegrations;
65
65
  /** Platform seam for capability projections; does not alter host-level startup behavior. */
66
66
  platform?: NodeJS.Platform;
67
- toggleCodexMultiAgentV2?: (enabled: boolean) => void;
68
- toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
67
+ toggleCodexMultiAgentV2?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
68
+ toggleDefaultModeRequestUserInput?: (enabled: boolean, env: NodeJS.ProcessEnv, validateBeforeSpawn: () => void) => void;
69
69
  createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
70
70
  /** Test-only destination for best-effort Claude agent-definition sync. */
71
71
  claudeAgentConfigDir?: string;
@@ -364,7 +364,7 @@ export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProvid
364
364
  /** Shared Desktop profile DTO builder for the management API and CLI. */
365
365
  export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxClaudeDesktopProfile) {
366
366
  const { filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
367
- const { DESKTOP_SUPPORTS_1M_THRESHOLD } = await import("../../claude/desktop-3p");
367
+ const { routeSupportsOneMillion } = await import("../../claude/long-context");
368
368
  const { reconcileDesktopProfile, renderDesktopProfile } = await import("../../claude/desktop-profile");
369
369
  const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
370
370
  const profileModels: DesktopProfileModel[] = [
@@ -413,8 +413,8 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
413
413
  ...(modelByRoute.get(route)?.contextWindow ? { contextWindow: modelByRoute.get(route)!.contextWindow } : {}),
414
414
  effortSupported: effortByRoute.get(route) ?? false,
415
415
  // Read-only view of the 1M capability the written Desktop config already emits,
416
- // derived from the SAME threshold so the dashboard chip can never disagree.
417
- supports1m: (modelByRoute.get(route)?.contextWindow ?? 0) >= DESKTOP_SUPPORTS_1M_THRESHOLD,
416
+ // derived from the SAME predicate so the dashboard chip can never disagree.
417
+ supports1m: routeSupportsOneMillion(route, modelByRoute.get(route)?.contextWindow),
418
418
  assignment: profile.assignments[route]!,
419
419
  }));
420
420
  return {
@@ -139,19 +139,20 @@ export async function resolveNativeOAuthBindingForInstance(
139
139
  const routeDecision = currentRoute(instance, config, model);
140
140
  const manualSelectionGeneration = routing.captureAnthropicManualSelectionGeneration();
141
141
  const selection = captureOAuthAccountSelection(instance);
142
- // Always ask admission first so pause/login/cooldown retains its typed local refusal.
143
- const admittedId = await routing.resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
144
- assertOwner();
145
- if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
146
- const proposed = routing.resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
147
- // A lost selection commit discards a recovery proposal: a newer manual/policy choice wins.
142
+ // A recovery candidate may replace a terminal active credential, including pool-off.
143
+ // A newer committed selection or route discards the proposal and uses normal admission.
148
144
  const recoverySelectionMatches = !!options.expectedRecoverySelection
149
145
  && selection?.accountId === options.expectedRecoverySelection.accountId
150
146
  && selection?.revision === options.expectedRecoverySelection.revision;
151
147
  const recoveryRouteMatches = options.expectedRecoveryRouteDecision === undefined
152
148
  || JSON.stringify(options.expectedRecoveryRouteDecision) === JSON.stringify(routeDecision);
153
- const accountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
154
- ? options.candidateAccountId : admittedId;
149
+ const recoveryAccountId = attempt === 0 && options.candidateAccountId && recoverySelectionMatches && recoveryRouteMatches
150
+ ? options.candidateAccountId : undefined;
151
+ const accountId = recoveryAccountId
152
+ ?? await routing.resolveAnthropicDispatchAccountId(config, sessionKey, routeDecision, model);
153
+ assertOwner();
154
+ if (!routeIsCurrent(instance, config, model, routeDecision)) continue;
155
+ const proposed = routing.resolveAnthropicAccountForSession(sessionKey, config, Date.now(), routeDecision, model);
155
156
  if (!selection) continue;
156
157
  if (!routeCandidates(routing.getEligibleAnthropicAccounts(Date.now(), model), routeDecision).includes(accountId)) {
157
158
  throw new NativeOAuthSelectionChangedError();