@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
@@ -0,0 +1,107 @@
1
+ import type { OcxConfig } from "../../types";
2
+ import { getConfigPath, initializePersistedConfigIfMissing, mutatePersistedConfig } from "../../config";
3
+ import { mergeConfigDefaults, readConfigFileSnapshot, validateConfigCandidate } from "../../config/diagnostics";
4
+ import { adoptPersistedCodexAccountSelection, claudeCodeBaselineArmed } from "../../config/live-reconcile";
5
+ import { deleteConfigTopLevelKey, fileBackedConfigPath, recordFileBackedConfig } from "../../config/rebase-provenance";
6
+ import { MAIN_CODEX_ACCOUNT_ID, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
7
+ import { isCodexAccountPaused } from "../account-pause";
8
+ import { readCodexAccountRecord } from "../account-store";
9
+ import { codexAccountPinDrainReason, resetCodexRoutingForManualSelection } from "../routing";
10
+ import { jsonResponse } from "./http";
11
+
12
+ class SelectionRejected extends Error {
13
+ constructor(message: string, readonly status: number) { super(message); }
14
+ }
15
+
16
+ function validateSelection(config: OcxConfig, accountId: string | null): void {
17
+ if (accountId === null) return;
18
+ if (accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(config.codexAccounts)) {
19
+ throw new SelectionRejected("Remove the legacy __main__ pool row before selecting the Desktop account", 409);
20
+ }
21
+ if (isCodexAccountPaused(config, accountId)) throw new SelectionRejected("Account is paused", 409);
22
+ if (accountId === MAIN_CODEX_ACCOUNT_ID) return;
23
+ if (!isValidCodexAccountId(accountId)) throw new SelectionRejected("Invalid account id format", 400);
24
+ if (!(config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === accountId)) {
25
+ throw new SelectionRejected("Account not found", 400);
26
+ }
27
+ if (readCodexAccountRecord(accountId)?.codexValidationPending) {
28
+ throw new SelectionRejected("Account validation is pending. Refresh quota after recovery to validate it.", 409);
29
+ }
30
+ }
31
+
32
+ function applySelection(config: OcxConfig, accountId: string | null): boolean {
33
+ const changed = config.activeCodexAccountId !== (accountId ?? undefined)
34
+ || config.activeCodexAccountPinned !== (accountId ?? undefined);
35
+ if (accountId === null) {
36
+ deleteConfigTopLevelKey(config, "activeCodexAccountId");
37
+ deleteConfigTopLevelKey(config, "activeCodexAccountPinned");
38
+ } else {
39
+ config.activeCodexAccountId = accountId;
40
+ config.activeCodexAccountPinned = accountId;
41
+ }
42
+ return changed;
43
+ }
44
+
45
+ /** A manual selection is an explicit command, even when equal to the live merge baseline. */
46
+ export function persistCodexAccountSelection(config: OcxConfig, accountId: string | null): Response {
47
+ const unknown = () => jsonResponse({
48
+ error: "Account selection could not be confirmed; reload settings before retrying",
49
+ code: "account_selection_unavailable",
50
+ }, 409);
51
+ const configPath = getConfigPath();
52
+ const sourcePath = fileBackedConfigPath(config);
53
+ if (sourcePath !== undefined && sourcePath !== configPath) return unknown();
54
+ let committed: OcxConfig;
55
+ let warning = false;
56
+ try {
57
+ // Do not activate a newly disk-only account that the running proxy has not adopted.
58
+ validateSelection(config, accountId);
59
+ const before = readConfigFileSnapshot();
60
+ if (before.diagnostics.source === "default" && !fileBackedConfigPath(config) && !claudeCodeBaselineArmed(config)) {
61
+ // Preserve transient/initial callers without recreating a deleted live configuration.
62
+ // The create-only owner cannot replace a competing writer's newly published config.
63
+ const normalized = validateConfigCandidate(mergeConfigDefaults(config));
64
+ if (!normalized.ok) return unknown();
65
+ const initial = normalized.config;
66
+ applySelection(initial, accountId);
67
+ initializePersistedConfigIfMissing(initial);
68
+ }
69
+ const result = mutatePersistedConfig(persisted => {
70
+ // This callback is re-run under the mutation lock immediately before publication.
71
+ // A removed or paused account must not be resurrected by a stale UI snapshot.
72
+ validateSelection(config, accountId);
73
+ validateSelection(persisted, accountId);
74
+ return { changed: applySelection(persisted, accountId), value: persisted };
75
+ });
76
+ if (result.status === "unavailable") return unknown();
77
+ committed = result.value;
78
+ } catch (error) {
79
+ if (error instanceof SelectionRejected) return jsonResponse({ error: error.message }, error.status);
80
+ // Atomic publication may succeed before generation/bookkeeping fails. Confirm the exact
81
+ // durable selection instead of rolling live state back behind an already published write.
82
+ const snapshot = readConfigFileSnapshot();
83
+ if (snapshot.raw === undefined) return unknown();
84
+ try {
85
+ const strict = validateConfigCandidate(JSON.parse(snapshot.raw.replace(/^\uFEFF/, "")));
86
+ if (!strict.ok) return unknown();
87
+ validateSelection(config, accountId);
88
+ validateSelection(strict.config, accountId);
89
+ if (strict.config.activeCodexAccountId !== (accountId ?? undefined)
90
+ || strict.config.activeCodexAccountPinned !== (accountId ?? undefined)) return unknown();
91
+ committed = strict.config;
92
+ warning = true;
93
+ } catch { return unknown(); }
94
+ }
95
+ adoptPersistedCodexAccountSelection(config, committed);
96
+ recordFileBackedConfig(config, configPath);
97
+ // No failed save may clear affinity, spend the manual preference, or move the RR cursor.
98
+ resetCodexRoutingForManualSelection(accountId ?? MAIN_CODEX_ACCOUNT_ID);
99
+ const pinDrainReason = accountId === null ? undefined : codexAccountPinDrainReason(config, accountId);
100
+ return jsonResponse({
101
+ ok: true,
102
+ activeCodexAccountId: config.activeCodexAccountId ?? null,
103
+ appliesImmediately: true,
104
+ ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
105
+ ...(warning ? { warning: "config_bookkeeping_failed" } : {}),
106
+ });
107
+ }
@@ -1,3 +1,4 @@
1
+ import { persistCodexAccountSelection } from "./account-selection";
1
2
  import { CODEX_ACCOUNT_LOG_LABEL_RE, codexAccountLogLabel } from "../account-label";
2
3
  import { poolQuotaHistoryIdentity, readCodexAccountRecord } from "../account-store";
3
4
  import { estimateCodexQuotaCapacity, insufficientCodexCapacity } from "../quota-capacity";
@@ -7,13 +8,13 @@ import { getAccountQuotaHistory, listAccountQuotas } from "../quota";
7
8
  import { deleteCodexAccount } from "../account-lifecycle";
8
9
  import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
9
10
  import { setAllCodexAccountsCreditsAfterLimit, setCodexAccountCreditsAfterLimit } from "../account-credit-use";
10
- import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPin, setCodexAccountPriority } from "../account-priority";
11
- import { codexAccountPinDrainReason, codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
11
+ import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPriority } from "../account-priority";
12
+ import { codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned } from "../routing";
12
13
  import { DEFAULT_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, parseCodexAccountPoolStrategy, parseAccountPriority } from "../pool-rotation";
13
14
  import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
14
15
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
15
16
  import type { OcxConfig } from "../../types";
16
- import { CODEX_ACCOUNT_ID_RE, hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
17
+ import { CODEX_ACCOUNT_ID_RE, isSelectableCodexPoolAccount, isValidCodexAccountId } from "../account-id";
17
18
  import { isCodexResetCreditOperationId } from "../reset-credit-recovery";
18
19
  import { listCodexAuthAccounts, selectFallbackAfterPause, pauseExhaustedCodexAccounts } from "./account-list";
19
20
  import { jsonResponse, manualImportDisabledResponse } from "./http";
@@ -247,53 +248,13 @@ export async function handleCodexAuthAPI(
247
248
  }
248
249
 
249
250
  if (url.pathname === "/api/codex-auth/active" && req.method === "PUT") {
250
- let body: { accountId: string | null };
251
- try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
252
- const runtimeConfig = getRuntimeConfig(config);
253
- const targetAccountId = body.accountId ?? MAIN_CODEX_ACCOUNT_ID;
254
- if (body.accountId === MAIN_CODEX_ACCOUNT_ID && hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
255
- return jsonResponse({ error: "Remove the legacy __main__ pool row before selecting the Desktop account" }, 409);
256
- }
257
- if (body.accountId != null && isCodexAccountPaused(runtimeConfig, targetAccountId)) {
258
- return jsonResponse({ error: "Account is paused" }, 409);
259
- }
260
- if (body.accountId != null && body.accountId !== MAIN_CODEX_ACCOUNT_ID) {
261
- if (!isValidCodexAccountId(body.accountId)) return jsonResponse({ error: "Invalid account id format" }, 400);
262
- const exists = (runtimeConfig.codexAccounts ?? [])
263
- .some(account => isSelectableCodexPoolAccount(account) && account.id === body.accountId);
264
- if (!exists) return jsonResponse({ error: "Account not found" }, 400);
265
- if (readCodexAccountRecord(body.accountId)?.codexValidationPending) {
266
- return jsonResponse({ error: "Account validation is pending. Refresh quota after recovery to validate it." }, 409);
267
- }
251
+ let body: unknown;
252
+ try { body = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
253
+ if (!body || typeof body !== "object" || Array.isArray(body)
254
+ || !("accountId" in body) || (body.accountId !== null && typeof body.accountId !== "string")) {
255
+ return jsonResponse({ error: "accountId must be an account id or null" }, 400);
268
256
  }
269
- runtimeConfig.activeCodexAccountId = body.accountId ?? undefined;
270
- // "Use this account now" outranks selection order until the account is spent:
271
- // persisted here rather than in resetCodexRoutingForManualSelection, which is
272
- // runtime state only. A null id clears the selection instead of making one, so it
273
- // must release the pin rather than record one: pinning the `targetAccountId`
274
- // fallback would leave a pin that no effective active account matches, which
275
- // `isEffectiveCodexAccountPinned` reports as unpinned while the tier filter still
276
- // honours it as a ceiling — invisibly capping the pool at the main account's tier.
277
- if (body.accountId == null) clearCodexAccountPin(runtimeConfig);
278
- else setCodexAccountPin(runtimeConfig, targetAccountId);
279
- resetCodexRoutingForManualSelection(targetAccountId);
280
- saveRuntimeConfig(config, runtimeConfig);
281
- // A pin this route accepts can still be dropped by the very next resolve, and saying
282
- // nothing about that is what made the setting look ignored (#4521). The checks above
283
- // refuse an account that cannot be selected at all; this reports the one remaining
284
- // outcome they do not cover, from the same predicate routing releases on, so the two
285
- // cannot drift. Absent means the pin survives — additive for existing clients.
286
- // `appliesImmediately` is unchanged: it answers whether thread affinity was cleared,
287
- // not whether the pin is durable.
288
- const pinDrainReason = body.accountId == null
289
- ? undefined
290
- : codexAccountPinDrainReason(runtimeConfig, targetAccountId);
291
- return jsonResponse({
292
- ok: true,
293
- activeCodexAccountId: body.accountId,
294
- appliesImmediately: true,
295
- ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
296
- });
257
+ return persistCodexAccountSelection(getRuntimeConfig(config), body.accountId);
297
258
  }
298
259
 
299
260
  if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
@@ -81,11 +81,13 @@ import {
81
81
  import {
82
82
  captureMainAccountIdentityGeneration,
83
83
  captureMainQuotaWriter,
84
+ captureMainQuotaDispatch,
84
85
  getObservedMainQuotaIdentityKey,
85
86
  isMainQuotaWriterLive,
86
87
  matchesMainQuotaCredential,
87
88
  observeMainQuotaCredential,
88
89
  type MainQuotaWriter,
90
+ type MainQuotaDispatch,
89
91
  } from "./main-account-cache";
90
92
  import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported, isCodexReserveRequestEligible } from "./loopback-target";
91
93
  import type { DataPlaneAdmission } from "../server/auth-cors";
@@ -275,7 +277,11 @@ export function previewCodexPoolLineage(
275
277
  }
276
278
 
277
279
  export type CodexAuthContext =
278
- | { kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization }
280
+ | {
281
+ kind: "main"; accountId: null; reserveAuthorization?: MainReserveAuthorization;
282
+ /** Captured for the observed main credential actually selected for upstream. */
283
+ mainQuotaDispatch?: MainQuotaDispatch;
284
+ }
279
285
  | {
280
286
  kind: "pool";
281
287
  accountId: string;
@@ -1635,6 +1641,14 @@ export class CodexMainSubstitutionUnavailableError extends Error {
1635
1641
  }
1636
1642
  }
1637
1643
 
1644
+ /** Dispatch identity comes only from an owned observation of the selected credential. */
1645
+ function selectedMainQuotaDispatch(selected: Headers): MainQuotaDispatch | undefined {
1646
+ const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
1647
+ if (!bearer) return undefined;
1648
+ const accountId = selected.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
1649
+ return captureMainQuotaDispatch(bearer, accountId, captureConfigGeneration());
1650
+ }
1651
+
1638
1652
  /**
1639
1653
  * Build the upstream auth headers for one Codex turn.
1640
1654
  *
@@ -1654,6 +1668,7 @@ export function materializeCodexUpstreamAuth(
1654
1668
  ctx: CodexAuthContext,
1655
1669
  options: CodexAuthMaterializationOptions = {},
1656
1670
  ): Headers {
1671
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1657
1672
  const selected = new Headers();
1658
1673
  for (const name of FORWARD_HEADERS) {
1659
1674
  const value = headers.get(name);
@@ -1695,10 +1710,12 @@ export function materializeCodexUpstreamAuth(
1695
1710
  observeSelectedMainCredential(stored, writer);
1696
1711
  assertMainAccountPolicy(options.config);
1697
1712
  assertMaterializedReserve(selected, ctx, options);
1713
+ ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1698
1714
  return selected;
1699
1715
  }
1700
1716
  if (callerMatchesObservedMain(selected)) assertMainAccountPolicy(options.config);
1701
1717
  assertMaterializedReserve(selected, ctx, options);
1718
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1702
1719
  return selected;
1703
1720
  }
1704
1721
 
@@ -1749,6 +1766,7 @@ export async function materializeCodexUpstreamAuthAsync(
1749
1766
  ctx: CodexAuthContext,
1750
1767
  options: CodexAuthMaterializationOptions = {},
1751
1768
  ): Promise<Headers> {
1769
+ if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
1752
1770
  if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
1753
1771
  return materializeReserveUpstreamAuth(headers, ctx, options);
1754
1772
  }
@@ -1776,6 +1794,7 @@ export async function materializeCodexUpstreamAuthAsync(
1776
1794
  assertMainAccountPolicy(options.config);
1777
1795
  // An opt-in enabled during token refresh must not turn a proof-less context into Reserve.
1778
1796
  assertMaterializedReserve(selected, ctx, options);
1797
+ ctx.mainQuotaDispatch = selectedMainQuotaDispatch(selected);
1779
1798
  return selected;
1780
1799
  }
1781
1800
 
@@ -60,6 +60,7 @@ export interface StartupHealth {
60
60
  platform: NodeJS.Platform;
61
61
  diagnosticStale: boolean;
62
62
  recommendedCommand: string | null;
63
+ recommendedAction?: string | null;
63
64
  commands: {
64
65
  installService: string;
65
66
  repairService: string;
@@ -91,7 +92,8 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
91
92
  // An arbitrary localhost gateway has an independent lifecycle that OCX cannot repair.
92
93
  const ownsLocalRouting = inputs.routingKind === "opencodex-local";
93
94
  const desktopEffective = (inputs.platform === "darwin" || inputs.platform === "linux") && !inputs.diagnosticStale
94
- && inputs.desktop?.owned === true && inputs.desktop.loginEnabled
95
+ && inputs.desktop !== undefined && (inputs.desktop.owned || inputs.desktop.supervisor !== undefined)
96
+ && inputs.desktop.loginEnabled
95
97
  && inputs.desktop.running && inputs.desktop.viable;
96
98
  const protection: StartupProtection = ownsLocalRouting && inputs.serviceViable
97
99
  ? "service"
@@ -105,7 +107,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
105
107
  : rebootSafe
106
108
  ? "protected"
107
109
  : "at-risk";
108
- const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop?.owned)
110
+ const recommendedCommand = status !== "at-risk" || (ownsLocalRouting && inputs.desktop !== undefined)
109
111
  ? null
110
112
  : inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
111
113
  ? COMMANDS.restoreNative
@@ -117,6 +119,11 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
117
119
  // gets the registering command.
118
120
  ? (inputs.serviceInstalled && !inputs.serviceConflict ? COMMANDS.repairService : COMMANDS.installService)
119
121
  : COMMANDS.restoreNative;
122
+ const recommendedAction = status === "at-risk" && ownsLocalRouting && inputs.desktop !== undefined
123
+ ? inputs.diagnosticStale || inputs.desktop.loginEnabled
124
+ ? "Reopen OpenCodex and check Start at Login."
125
+ : "Turn on Start at Login in the OpenCodex menu so the desktop app starts this proxy after a restart."
126
+ : null;
120
127
  return {
121
128
  ...inputs,
122
129
  diagnosticStale: inputs.diagnosticStale ?? false,
@@ -127,6 +134,7 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
127
134
  protection,
128
135
  shimCoverage,
129
136
  recommendedCommand,
137
+ recommendedAction,
130
138
  commands: { ...COMMANDS },
131
139
  };
132
140
  }
@@ -211,7 +219,8 @@ function classifyStartupHealthSummary(health: StartupHealth): string {
211
219
  const command = health.recommendedCommand ?? health.commands.restoreNative;
212
220
  if (health.routingKind === "unknown") return `AT RISK after restart (Codex routing could not be verified; run '${command}')`;
213
221
  if (health.routingKind === "custom-local") return `AT RISK after restart (custom local gateway lifecycle is not managed by opencodex; run '${command}')`;
214
- if (health.desktop?.owned) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
222
+ if (health.desktop?.supervisor && !health.desktop.loginEnabled && !health.diagnosticStale) return "AT RISK after restart (OpenCodex Desktop runs this proxy, but its Start at Login could not be verified; turn it on in the OpenCodex menu)";
223
+ if (health.desktop) return "AT RISK after restart (desktop startup could not be verified; reopen OpenCodex and check Start at Login)";
215
224
  if (health.shimCoverage === "cli-only") return `AT RISK for Codex Desktop after restart (launcher shim covers CLI scripts only; run '${command}')`;
216
225
  if (health.serviceConflict) return `AT RISK after restart (background service managers conflict; run '${command}')`;
217
226
  if (health.serviceStale) return `AT RISK after restart (background service files are stale; run '${command}')`;
@@ -48,6 +48,7 @@ import { NATIVE_RESERVE_MODEL } from "./native-models";
48
48
  import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
49
49
  import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
50
50
  import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
51
+ import { preserveOperatorUltraFastTiers } from "./operator-tiers";
51
52
 
52
53
  export interface ObservedCatalogEntryBuildInput {
53
54
  readonly template: RawEntry | null;
@@ -470,6 +471,8 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
470
471
  }
471
472
 
472
473
  export interface ObservedCatalogMergeInput {
474
+ /** Preserve operator-supplied Ultra Fast only from the exact persisted routed row. */
475
+ readonly ultraFastTier?: boolean;
473
476
  readonly catalogModels: readonly RawEntry[];
474
477
  readonly baselineCatalogModels: readonly RawEntry[];
475
478
  readonly routedEntries: readonly RawEntry[];
@@ -510,6 +513,7 @@ export interface ObservedCatalogMergeInput {
510
513
  * accidentally fall back to process-ambient catalog discovery or merge-policy warnings.
511
514
  */
512
515
  export function mergeCatalogEntriesFromObservedState({
516
+ ultraFastTier = false,
513
517
  catalogModels,
514
518
  baselineCatalogModels,
515
519
  routedEntries,
@@ -849,6 +853,20 @@ export function mergeCatalogEntriesFromObservedState({
849
853
  }
850
854
  return false;
851
855
  });
856
+ // First occurrence wins, matching enforceCatalogSlugUniqueness: a later duplicate row of the
857
+ // same slug is what the writer drops, so it must not decide which declaration survives.
858
+ const operatorRows = new Map<string, RawEntry | undefined>();
859
+ for (const entry of detachedCatalogModels) {
860
+ if (typeof entry.slug !== "string" || operatorRows.has(entry.slug)) continue;
861
+ operatorRows.set(entry.slug, trustedAccountBoundNativeCatalogSlug(entry) === undefined
862
+ && !isNativeAliasCatalogEntry(entry) && entry.owned_by !== COMBO_NAMESPACE ? entry : undefined);
863
+ }
864
+ for (const entry of finalRoutedEntries) {
865
+ if (typeof entry.slug !== "string" || !entry.slug.includes("/")
866
+ || isNativeAliasCatalogEntry(entry) || isExactComboCatalogEntry(entry, exactComboSlugs)
867
+ || trustedAccountBoundNativeCatalogSlug(entry) !== undefined) continue;
868
+ preserveOperatorUltraFastTiers(entry, operatorRows.get(entry.slug), ultraFastTier);
869
+ }
852
870
  const finalRoutedEntrySet = new Set(finalRoutedEntries);
853
871
  const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
854
872
  if (!finalRoutedEntrySet.has(entry)) return false;