@bitkyc08/opencodex 2.88.0 → 2.89.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 (205) hide show
  1. package/README.md +11 -1
  2. package/bin/ocx.mjs +15 -4
  3. package/gui/dist/assets/App-BnRSujOq.js +58 -0
  4. package/gui/dist/assets/App-dnkJ85te.css +1 -0
  5. package/gui/dist/assets/{Tray-B5bC7AWL.js → Tray-7z1COnmz.js} +1 -1
  6. package/gui/dist/assets/{index-B5bsvxor.js → index-uchD-ngf.js} +13 -13
  7. package/gui/dist/assets/{quota-summary-D8y2wTsB.js → quota-summary-BY_10IEz.js} +1 -1
  8. package/gui/dist/index.html +1 -1
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/cache-retention.ts +117 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -4
  12. package/src/adapters/anthropic.ts +10 -3
  13. package/src/adapters/coding-agent/protocol.ts +6 -0
  14. package/src/adapters/google-http.ts +3 -2
  15. package/src/adapters/google-vertex-endpoint.ts +67 -0
  16. package/src/adapters/google.ts +35 -29
  17. package/src/adapters/openai-responses/compaction-search-history.ts +27 -0
  18. package/src/adapters/openai-responses/passthrough.ts +17 -12
  19. package/src/adapters/openai-responses/reasoning.ts +68 -1
  20. package/src/adapters/registry.ts +1 -1
  21. package/src/adapters/upstream-http-error.ts +3 -2
  22. package/src/adapters/zed.ts +7 -4
  23. package/src/bridge/internal.ts +11 -1
  24. package/src/bridge/response-json.ts +2 -2
  25. package/src/bridge/sse.ts +10 -4
  26. package/src/claude/inbound.ts +12 -26
  27. package/src/claude/outbound.ts +5 -0
  28. package/src/cli/account-api.ts +13 -1
  29. package/src/cli/account-extended.ts +17 -5
  30. package/src/cli/account-key-quota.ts +4 -1
  31. package/src/cli/account.ts +1 -1
  32. package/src/cli/capabilities-agents-routing.ts +45 -8
  33. package/src/cli/capabilities-base.ts +5 -5
  34. package/src/cli/capabilities-provider-models.ts +5 -4
  35. package/src/cli/combo-result.ts +8 -2
  36. package/src/cli/combo.ts +17 -0
  37. package/src/cli/help.ts +1 -1
  38. package/src/cli/message-command.ts +4 -1
  39. package/src/cli/message-remote-args.ts +69 -0
  40. package/src/cli/message-remote-command.ts +77 -0
  41. package/src/cli/provider-runtime.ts +69 -1
  42. package/src/cli/registry.ts +3 -3
  43. package/src/codex/account-pause.ts +6 -1
  44. package/src/codex/account-runtime-state.ts +5 -0
  45. package/src/codex/account-store.ts +30 -3
  46. package/src/codex/auth-api/account-list.ts +3 -0
  47. package/src/codex/auth-api/main-account-probe.ts +3 -2
  48. package/src/codex/auth-context.ts +9 -4
  49. package/src/codex/catalog/aggregation.ts +15 -0
  50. package/src/codex/catalog/auto-review.ts +2 -1
  51. package/src/codex/catalog/build-entries.ts +17 -7
  52. package/src/codex/catalog/derive-entry.ts +3 -1
  53. package/src/codex/catalog/metadata.ts +2 -0
  54. package/src/codex/catalog/model-visibility.ts +0 -2
  55. package/src/codex/catalog/ownership.ts +71 -0
  56. package/src/codex/catalog/parsing.ts +12 -3
  57. package/src/codex/catalog/retained-sync.ts +2 -1
  58. package/src/codex/catalog/routed-removal.ts +4 -4
  59. package/src/codex/convergence.ts +2 -1
  60. package/src/codex/low-quota-desktop-notifier.ts +125 -0
  61. package/src/codex/low-quota-protection.ts +57 -31
  62. package/src/codex/main-account-cache.ts +77 -3
  63. package/src/codex/main-account.ts +6 -2
  64. package/src/codex/native-residue.ts +2 -3
  65. package/src/codex/quota.ts +26 -2
  66. package/src/combos/failover.ts +2 -1
  67. package/src/combos/jev-model-backend.ts +30 -7
  68. package/src/combos/jev-model-request.ts +20 -0
  69. package/src/combos/jev-quota-activation.ts +153 -0
  70. package/src/combos/jev-quota-config.ts +62 -0
  71. package/src/combos/jev-quota-route.ts +26 -0
  72. package/src/combos/jev-quota.ts +88 -0
  73. package/src/combos/jev.ts +25 -12
  74. package/src/combos/types.ts +27 -0
  75. package/src/compatibility/index.ts +3 -0
  76. package/src/compatibility/openai-responses.ts +8 -1
  77. package/src/compatibility/opengateway-responses.ts +61 -0
  78. package/src/config/account-auto-pause-revisions.ts +136 -0
  79. package/src/config/diagnostics.ts +13 -0
  80. package/src/config/live-reconcile.ts +9 -0
  81. package/src/config/persist-unlocked.ts +10 -1
  82. package/src/config/removed-opencode-free.ts +135 -0
  83. package/src/config/schema/account-auto-pause.ts +14 -0
  84. package/src/config/schema/config-schema.ts +15 -0
  85. package/src/config/schema/leaf-validators.ts +7 -0
  86. package/src/config.ts +10 -3
  87. package/src/generated/compatibility-version.json +344 -144
  88. package/src/images/loop.ts +32 -9
  89. package/src/lib/account-auto-pause-response.ts +16 -0
  90. package/src/lib/bun-runtime.ts +18 -1
  91. package/src/lib/bun-transpiler-cache-scope.d.mts +8 -0
  92. package/src/lib/bun-transpiler-cache-scope.mjs +68 -0
  93. package/src/lib/decision-quota-sink.ts +76 -0
  94. package/src/lib/state-store-registrations.ts +25 -0
  95. package/src/link/ssh-argv.ts +3 -0
  96. package/src/messaging/envelope.ts +6 -3
  97. package/src/messaging/remote-auth.ts +83 -0
  98. package/src/messaging/remote-bridge.ts +270 -0
  99. package/src/messaging/remote-contract.ts +108 -0
  100. package/src/messaging/remote-enrollment-recovery.ts +59 -0
  101. package/src/messaging/remote-enrollment.ts +244 -0
  102. package/src/messaging/remote-envelope.ts +40 -0
  103. package/src/messaging/remote-files.ts +117 -0
  104. package/src/messaging/remote-owner.ts +169 -0
  105. package/src/messaging/remote-ports.ts +56 -0
  106. package/src/messaging/remote-process.ts +131 -0
  107. package/src/messaging/remote-replies.ts +98 -0
  108. package/src/messaging/remote-rpc-admission.ts +32 -0
  109. package/src/messaging/remote-send.ts +72 -0
  110. package/src/messaging/remote-store.ts +111 -0
  111. package/src/messaging/remote-tunnels.ts +65 -0
  112. package/src/messaging/rpc.ts +13 -1
  113. package/src/oauth/account-auto-pause-policy.ts +66 -0
  114. package/src/oauth/account-auto-pause.ts +171 -0
  115. package/src/oauth/account-quota-rank.ts +2 -1
  116. package/src/oauth/anthropic-account-refusal.ts +6 -0
  117. package/src/oauth/anthropic-model-family.ts +8 -0
  118. package/src/oauth/anthropic-model-quota.ts +2 -6
  119. package/src/oauth/api-key-pool-preserve.ts +37 -0
  120. package/src/oauth/index.ts +5 -35
  121. package/src/oauth/store.ts +35 -8
  122. package/src/oauth/types.ts +2 -0
  123. package/src/plugins/upstream-hooks.ts +14 -1
  124. package/src/providers/api-key-pause.ts +32 -0
  125. package/src/providers/api-key-resolve.ts +10 -4
  126. package/src/providers/api-key-selection.ts +6 -1
  127. package/src/providers/api-keys.ts +8 -4
  128. package/src/providers/claude-cli-usage.ts +5 -0
  129. package/src/providers/key-failover.ts +11 -11
  130. package/src/providers/model-rename-fields.ts +1 -0
  131. package/src/providers/opencode-zen-rate-limit.ts +6 -72
  132. package/src/providers/quota/account-cache.ts +15 -1
  133. package/src/providers/quota/anthropic-account-quota.ts +2 -1
  134. package/src/providers/quota/anthropic-cooldown-recovery.ts +22 -4
  135. package/src/providers/quota/anthropic-decision-windows.ts +47 -0
  136. package/src/providers/quota/antigravity.ts +39 -6
  137. package/src/providers/quota/report-cache.ts +14 -0
  138. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  139. package/src/providers/quota-decision-publication.ts +77 -0
  140. package/src/providers/quota-decision-snapshot.ts +145 -0
  141. package/src/providers/quota-routing-cache.ts +3 -0
  142. package/src/providers/quota-types.ts +6 -1
  143. package/src/providers/quota.ts +3 -2
  144. package/src/providers/registry/entries-extended.ts +8 -52
  145. package/src/providers/registry/model-seeds.ts +1 -1
  146. package/src/providers/registry.ts +1 -2
  147. package/src/providers/stale-vision-classification-migration.ts +1 -1
  148. package/src/responses/parser.ts +18 -10
  149. package/src/responses/web-search-replay.ts +138 -0
  150. package/src/routing/compatibility/behavior.ts +4 -1
  151. package/src/server/auth-cors.ts +7 -0
  152. package/src/server/background-lifecycle.ts +2 -1
  153. package/src/server/chat-native.ts +28 -13
  154. package/src/server/claude-messages.ts +2 -1
  155. package/src/server/index/optional-listeners.ts +6 -0
  156. package/src/server/management/combo-routes.ts +17 -2
  157. package/src/server/management/oauth-account-routes.ts +10 -1
  158. package/src/server/management/provider-completion-test.ts +289 -0
  159. package/src/server/management/provider-routes.ts +9 -1
  160. package/src/server/management/route-registry.ts +2 -0
  161. package/src/server/messages-native.ts +18 -4
  162. package/src/server/messages-shared.ts +4 -1
  163. package/src/server/request-log.ts +7 -2
  164. package/src/server/responses/account-auto-pause-recovery.ts +26 -0
  165. package/src/server/responses/adapter-dispatch.ts +77 -28
  166. package/src/server/responses/combo-session-recall.ts +25 -0
  167. package/src/server/responses/combo-stream-preflight.ts +3 -1
  168. package/src/server/responses/combo-tool-loop-pin.ts +70 -0
  169. package/src/server/responses/compact.ts +8 -5
  170. package/src/server/responses/context-overflow.ts +141 -2
  171. package/src/server/responses/core-codex-account.ts +7 -1
  172. package/src/server/responses/core-combo.ts +31 -5
  173. package/src/server/responses/core.ts +12 -9
  174. package/src/server/responses/empty-completion-guard.ts +2 -0
  175. package/src/server/responses/fetch-helpers.ts +35 -4
  176. package/src/server/responses/jev-model-invoke.ts +13 -14
  177. package/src/server/responses/native-steering-log.ts +2 -2
  178. package/src/server/responses/passthrough-delivery.ts +3 -2
  179. package/src/server/responses/passthrough-dispatch.ts +30 -33
  180. package/src/server/responses/passthrough-observations.ts +38 -0
  181. package/src/server/responses/request-compression.ts +98 -0
  182. package/src/server/responses/request-transport.ts +32 -3
  183. package/src/server/responses/run-turn-execution.ts +2 -0
  184. package/src/server/responses/sidecar-execution.ts +16 -3
  185. package/src/server/responses/terminal-guard.ts +2 -0
  186. package/src/server/startup-action-control.ts +2 -2
  187. package/src/server/startup-health-cache.ts +2 -2
  188. package/src/server/windows-tray-control.ts +2 -2
  189. package/src/service/definition.ts +9 -1
  190. package/src/types/account-auto-pause.ts +15 -0
  191. package/src/types/config.ts +23 -1
  192. package/src/types/provider.ts +9 -5
  193. package/src/types/request.ts +20 -2
  194. package/src/types.ts +1 -0
  195. package/src/usage/cost.ts +17 -1
  196. package/src/usage/expected-prices.ts +3 -1
  197. package/src/usage/jev-stats.ts +4 -0
  198. package/src/usage/log.ts +2 -0
  199. package/src/usage/summary.ts +7 -0
  200. package/src/web-search/loop.ts +37 -11
  201. package/src/web-search/progress-stream.ts +4 -1
  202. package/src/web-search/run-turn-loop.ts +9 -3
  203. package/src/web-search/sidecar-search-replay.ts +96 -0
  204. package/gui/dist/assets/App-CHC89EXQ.js +0 -58
  205. package/gui/dist/assets/App-wT3mXqFz.css +0 -1
@@ -16,9 +16,9 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
16
16
  },
17
17
  {
18
18
  name: "message",
19
- usage: "ocx message <sessions|send> [options]",
20
- summary: "Discover loaded local Codex sessions and submit one queued peer message.",
21
- details: ["Local Unix transport only; starts no daemon. queued is submission, not processing; never replay an unknown send."],
19
+ usage: "ocx message <sessions|send|enable|disable|status|hosts|serve> [options]",
20
+ summary: "Discover loaded Codex sessions and queue messages locally or through explicit SSH peers.",
21
+ details: ["Remote listeners/tunnels require explicit foreground serve; no daemon start. queued is submission, not processing; never replay an unknown send."],
22
22
  },
23
23
  {
24
24
  name: "chatgpt",
@@ -1,3 +1,5 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { AccountPauseDetails } from "../types/account-auto-pause";
1
3
  import type { OcxConfig } from "../types";
2
4
  import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
3
5
 
@@ -7,7 +9,9 @@ export function isCodexAccountPaused(config: OcxConfig, accountId: string): bool
7
9
  }
8
10
 
9
11
  /** Persist the account's pool eligibility without changing credentials or runtime health. */
10
- export function setCodexAccountPaused(config: OcxConfig, accountId: string, paused: boolean): void {
12
+ export function setCodexAccountPaused(config: OcxConfig, accountId: string, paused: boolean, details?: AccountPauseDetails): void {
13
+ config.codexAccountPauseState = { ...config.codexAccountPauseState,
14
+ [accountId]: { revision: randomUUID(), ...(paused && details ? { details } : {}) } };
11
15
  const pausedIds = new Set(config.pausedCodexAccountIds ?? []);
12
16
  if (paused) pausedIds.add(accountId);
13
17
  else pausedIds.delete(accountId);
@@ -18,4 +22,5 @@ export function setCodexAccountPaused(config: OcxConfig, accountId: string, paus
18
22
 
19
23
  export function forgetCodexAccountPause(config: OcxConfig, accountId: string): void {
20
24
  setCodexAccountPaused(config, accountId, false);
25
+ if (config.codexAccountPauseState) delete config.codexAccountPauseState[accountId];
21
26
  }
@@ -1,3 +1,4 @@
1
+ import { setDecisionAccountUsable } from "../lib/decision-quota-sink";
1
2
  import { captureConfigGeneration, type GenerationContext } from "../lib/state-store-sweeper";
2
3
  import { isCodexAccountGenerationLive } from "./account-store";
3
4
 
@@ -39,12 +40,14 @@ export function clearMainRefreshGrantRejection(key: string): void {
39
40
  }
40
41
 
41
42
 
43
+ /** Quarantine a Codex account (or one credential generation) after an auth failure, and mark its advisory decision-quota row unusable so routing evidence cannot treat it as healthy. */
42
44
  export function markAccountNeedsReauth(
43
45
  id: string,
44
46
  writerGeneration = captureConfigGeneration(),
45
47
  credentialGeneration?: number,
46
48
  ): void {
47
49
  if (writerGeneration < lastReconciledGeneration && !liveAccountIds.has(id)) return;
50
+ setDecisionAccountUsable(id === "__main__" ? "codex-main" : "codex", id, false, credentialGeneration);
48
51
  // An account-wide mark supersedes a generation-scoped one: it is the stronger claim.
49
52
  if (credentialGeneration === undefined || !reauthAccounts.has(id)) {
50
53
  reauthAccounts.set(id, credentialGeneration);
@@ -81,6 +84,7 @@ export function isAccountNeedsReauth(id: string): boolean {
81
84
  return true;
82
85
  }
83
86
 
87
+ /** Lift a reauth quarantine only when the proof matches the quarantined credential generation, and restore the advisory decision-quota row's usability. */
84
88
  export function clearAccountNeedsReauth(id: string, credentialGeneration?: number): void {
85
89
  // A model response proves only the credential it used. Keep account-wide
86
90
  // quarantine and evidence from another generation intact.
@@ -88,4 +92,5 @@ export function clearAccountNeedsReauth(id: string, credentialGeneration?: numbe
88
92
  && (reauthAccounts.get(id) !== credentialGeneration
89
93
  || !isCodexAccountGenerationLive(id, credentialGeneration))) return;
90
94
  reauthAccounts.delete(id);
95
+ setDecisionAccountUsable(id === "__main__" ? "codex-main" : "codex", id, true, credentialGeneration);
91
96
  }
@@ -14,6 +14,7 @@ import {
14
14
  hardenExistingSecret,
15
15
  withConfigMutationLockSync,
16
16
  } from "../config";
17
+ import { notifyCodexStoreLoaded } from "../lib/decision-quota-sink";
17
18
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
18
19
  import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
19
20
  import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
@@ -113,30 +114,56 @@ function normalizeRecord(value: CodexAccountCredentials | CodexAccountCredential
113
114
  return undefined;
114
115
  }
115
116
 
117
+ /**
118
+ * Observe-only read for the JEV quota activation owner: the same normalization as the loader,
119
+ * without hardening, invalid-file backup or slot notification. Absent or invalid stores read as empty.
120
+ */
121
+ export function peekCodexAccountRecordStore(): Record<string, CodexAccountCredentialRecord> {
122
+ const path = codexAccountsPath();
123
+ if (!existsSync(path)) return {};
124
+ const normalized: Record<string, CodexAccountCredentialRecord> = {};
125
+ try {
126
+ const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
127
+ for (const [id, value] of Object.entries(raw)) {
128
+ const record = normalizeRecord(value);
129
+ if (record) normalized[id] = record;
130
+ }
131
+ } catch {
132
+ return {};
133
+ }
134
+ return normalized;
135
+ }
136
+
137
+ /** Read the Codex account record store from disk with hardened permissions, and hand it to the opt-in decision-quota slot (a no-op unless JEV quota signals are active). */
116
138
  function loadCodexAccountRecordStore(): CodexAccountStore {
117
139
  const path = codexAccountsPath();
118
140
  hardenConfigDir();
119
141
  hardenExistingSecret(path);
120
- if (!existsSync(path)) return {};
142
+ if (!existsSync(path)) { notifyCodexStoreLoaded({}); return {}; }
143
+ const normalized: CodexAccountStore = {};
121
144
  try {
122
145
  const raw = JSON.parse(readFileSync(path, "utf-8")) as RawCodexAccountStore;
123
- const normalized: CodexAccountStore = {};
124
146
  for (const [id, value] of Object.entries(raw)) {
125
147
  const record = normalizeRecord(value);
126
148
  if (record) normalized[id] = record;
127
149
  }
128
- return normalized;
129
150
  } catch {
130
151
  backupInvalidConfig(path);
152
+ notifyCodexStoreLoaded({});
131
153
  return {};
132
154
  }
155
+ // Outside the parse try: roster publication is advisory and must never route a loaded store into the invalid-config backup path.
156
+ notifyCodexStoreLoaded(normalized);
157
+ return normalized;
133
158
  }
134
159
 
160
+ /** Atomically write the Codex account store, then hand it to the opt-in decision-quota slot so active advisory evidence tracks the persisted credentials. */
135
161
  function persist(store: CodexAccountStore): void {
136
162
  const dir = getConfigDir();
137
163
  assertNotRealHomeUnderTest(dir);
138
164
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
139
165
  atomicWriteFile(codexAccountsPath(), JSON.stringify(store, null, 2) + "\n");
166
+ notifyCodexStoreLoaded(store);
140
167
  }
141
168
 
142
169
  function persistCredentialMutation(store: CodexAccountStore): void {
@@ -141,6 +141,7 @@ export function poolAccountDto(
141
141
  logLabel: codexAccountLogLabel(account),
142
142
  isMain: false,
143
143
  paused,
144
+ ...(config.codexAccountPauseState?.[account.id]?.details ? { pauseDetails: config.codexAccountPauseState[account.id]!.details } : {}),
144
145
  priority,
145
146
  autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(config, account.id),
146
147
  creditsAfterLimit: codexAccountUsesCreditsAfterLimit(config, account.id),
@@ -167,6 +168,7 @@ export interface CodexAuthAccountDto {
167
168
  logLabel?: string;
168
169
  isMain: boolean;
169
170
  paused: boolean;
171
+ pauseDetails?: import("../../types/account-auto-pause").AccountPauseDetails;
170
172
  /** Selection order; higher is used earlier. Always present, 0 when unset. */
171
173
  priority: number;
172
174
  /** Null inherits the global usage-switch threshold; 0 disables it for this account. */
@@ -403,6 +405,7 @@ export async function listCodexAuthAccountsSnapshot(
403
405
  logLabel: "main",
404
406
  isMain: true,
405
407
  paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
408
+ ...(runtimeConfig.codexAccountPauseState?.[MAIN_CODEX_ACCOUNT_ID]?.details ? { pauseDetails: runtimeConfig.codexAccountPauseState[MAIN_CODEX_ACCOUNT_ID]!.details } : {}),
406
409
  mainAccountHardLock: { ...getMainAccountHardLockStatus(runtimeConfig),
407
410
  externalUsage: getMainAccountExternalUsageWarning(getObservedMainQuotaIdentityKey()) },
408
411
  priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
@@ -6,7 +6,7 @@ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../.
6
6
  import { providerCodexAccountMode } from "../../providers/registry";
7
7
  import { isSelectableCodexPoolAccount } from "../account-id";
8
8
  import type { OcxConfig } from "../../types";
9
- import { parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
9
+ import { isValidWhamHistoryObservation, parseMainPolicyUsageQuota, parseUsageQuota, setAccountQuotaFromParsed } from "../quota";
10
10
  import type { StoredAccountQuota, WhamUsageResponse } from "../quota";
11
11
  import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
12
12
  import { getMainChatgptAccountId, readCodexTokensResult } from "../auth-collision";
@@ -278,6 +278,7 @@ export async function fetchMainAccountInfoWhileOwned(
278
278
  let quotaPhase: "request" | "body" | "decode" | "publish" = "request";
279
279
  let quotaRefreshGeneration = captureMainAccountIdentityGeneration();
280
280
  const readState: { owner?: CodexUsageOwner<MainAccountInfoFetchResult>; usable: boolean } = { usable: false };
281
+ /** Perform one single-flight main-account usage read: classify auth failures and publish parsed quota and decision evidence only while the dispatch and credential are still current. */
281
282
  const read = async (): Promise<MainAccountInfoFetchResult> => {
282
283
  try {
283
284
  let dispatchSequence = 0;
@@ -398,7 +399,7 @@ export async function fetchMainAccountInfoWhileOwned(
398
399
  // score and auto-switch the main account exactly like a pool account (Option A).
399
400
  setMainAccountPlan(result.plan);
400
401
  if (result.quota) {
401
- setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota);
402
+ setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, result.quota, writerGeneration, mainQuotaWriter, policyQuota, undefined, false, false, isValidWhamHistoryObservation(usage) ? quota : null);
402
403
  }
403
404
  publishQuotaDispatch(dispatchSequence);
404
405
  readState.usable = quota !== null;
@@ -69,7 +69,7 @@ import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
69
69
  import { maskAccountId } from "../lib/privacy";
70
70
  import { formatErrorResponse, formatUsageLimitReachedResponse } from "../bridge";
71
71
  import { nextCodexPoolRecoveryAt } from "./routing/pool-recovery";
72
- import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
72
+ import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, isValidWhamHistoryObservation, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
73
73
  import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "./account-credit-use";
74
74
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
75
75
  import { FORWARD_HEADERS } from "../adapters/openai-responses";
@@ -665,6 +665,7 @@ function assertReserveAdmission(config: CodexAuthPolicyConfig): void {
665
665
  }
666
666
  }
667
667
 
668
+ /** Authorize a reserve request for the main credential, re-checking admission and writer liveness, and publish the ordinary quota observed during the capability read. */
668
669
  async function authorizeReserveCredential(
669
670
  token: { accessToken: string; chatgptAccountId: string },
670
671
  writer: MainQuotaWriter | undefined,
@@ -681,9 +682,11 @@ async function authorizeReserveCredential(
681
682
  const authorization = isMainReserveAuthorizationLive(existing, token) ? existing
682
683
  : await getMainReserveAuthorization({
683
684
  token, writer, signal,
685
+ /** Publish the ordinary usage observed during the reserve capability read: parse it once and reuse that observation as the advisory decision evidence when it is a valid history observation. */
684
686
  observeOrdinaryQuota(data, capturedWriter) {
685
- setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, parseUsageQuota(data), writerGeneration,
686
- capturedWriter, parseMainPolicyUsageQuota(data));
687
+ const parsed = parseUsageQuota(data);
688
+ setAccountQuotaFromParsed(MAIN_CODEX_ACCOUNT_ID, parsed, writerGeneration,
689
+ capturedWriter, parseMainPolicyUsageQuota(data), undefined, false, false, isValidWhamHistoryObservation(data) ? parsed : null);
687
690
  },
688
691
  });
689
692
  // The capability read also publishes ordinary quota. A new 99% reading or cooldown wins.
@@ -769,8 +772,10 @@ export function createCodexAuthDispatchGuard(
769
772
  terminalHelper = false,
770
773
  ): ((headers: Headers) => void) | undefined {
771
774
  const reserveGuard = createCodexReserveDispatchGuard(ctx, config, modelId, admission, terminalHelper);
772
- if (ctx.kind !== "pool") return reserveGuard;
775
+ if (ctx.kind !== "pool" && ctx.kind !== "main-pool") return reserveGuard;
773
776
  return headers => {
777
+ if (config.pausedCodexAccountIds?.includes(ctx.accountId)) throw new CodexPoolAuthenticationError("Selected Codex account is paused; resume it in account settings");
778
+ if (ctx.kind === "main-pool") { reserveGuard?.(headers); return; }
774
779
  assertPoolAccountCredits(ctx.accountId, poolContextCreditHoldResetAt(ctx, config));
775
780
  reserveGuard?.(headers);
776
781
  };
@@ -195,6 +195,7 @@ export function deriveComboCatalogModel(
195
195
  ...(combo.alias ? { alias: combo.alias } : {}),
196
196
  ...(combo.nativeAlias ? { nativeAlias: true } : {}),
197
197
  ...(combo.displayName ? { displayName: combo.displayName } : {}),
198
+ ...(combo.description ? { description: combo.description } : {}),
198
199
  ...(defaultReasoningEffort ? { defaultReasoningEffort } : {}),
199
200
  ...(members.every(member => member.parallelToolCalls === true)
200
201
  ? { parallelToolCalls: true }
@@ -401,6 +402,20 @@ export function exactComboCatalogSlugs(
401
402
  }));
402
403
  }
403
404
 
405
+ /** Current combo row descriptions by public catalog slug, for retained-row re-projection (#7229). */
406
+ export function comboCatalogDescriptions(
407
+ config: Pick<OcxConfig, "combos" | "disabledModels">,
408
+ ): Map<string, string> {
409
+ const current = exactComboCatalogSlugs(config);
410
+ return new Map(listComboIds(config).flatMap(id => {
411
+ const raw = config.combos?.[id];
412
+ const alias = typeof raw?.alias === "string" ? raw.alias.trim() : "";
413
+ const publicSlug = alias || comboModelId(id);
414
+ const description = typeof raw?.description === "string" ? raw.description.trim() : "";
415
+ return description && current.has(publicSlug) ? [[publicSlug, description] as const] : [];
416
+ }));
417
+ }
418
+
404
419
  export function normalizedOpenAiApiSignature(model: CatalogModel): string {
405
420
  const normalized = {
406
421
  provider: model.provider,
@@ -5,6 +5,7 @@ import { encodeRoutedModelId } from "../../providers/slug-codec";
5
5
  import { canonicalAutoReviewModelKey, isValidAutoReviewModel as isValidAutoReviewTarget } from "../../config/provider-validation";
6
6
  import { readConfiguredAutoReviewModel } from "./parsing";
7
7
  import type { RawEntry } from "./parsing";
8
+ import { hasOcxRoutedAuthorship } from "./ownership";
8
9
  import { configuredCatalogEntry } from "./subagent-roster";
9
10
 
10
11
  const AUTO_REVIEW_ROOT_MARKER = "opencodex_auto_review_root";
@@ -38,7 +39,7 @@ function isRoutedCatalogEntry(entry: RawEntry): boolean {
38
39
  if (isCodexControlPlaneModel(entry.slug)) return false;
39
40
  const slug = typeof entry.slug === "string" ? entry.slug : "";
40
41
  return slug.includes("/")
41
- || (typeof entry.description === "string" && entry.description.startsWith("Routed via opencodex → "));
42
+ || hasOcxRoutedAuthorship(entry);
42
43
  }
43
44
 
44
45
  /** Restore an owned native value, retaining provenance to avoid legacy reclassification. */
@@ -4,6 +4,7 @@ import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasonin
4
4
  import { clearModelCache } from "../model-cache";
5
5
  import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
6
6
  import { COMBO_NAMESPACE } from "../../combos";
7
+ import { hasOcxComboAuthorship, hasOcxRoutedAuthorship, reapplyRetainedComboDescription, ROUTED_DESCRIPTION_PREFIX } from "./ownership";
7
8
  import {
8
9
  CODEX_CUSTOM_MODEL_CATALOG_KIND,
9
10
  CODEX_PROVIDER_MODEL_CATALOG_KIND,
@@ -218,7 +219,7 @@ export function buildCatalogEntriesFromObservedState({
218
219
  const routed = deriveEntry(
219
220
  template,
220
221
  slug,
221
- `Routed via opencodex → ${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
222
+ `${ROUTED_DESCRIPTION_PREFIX}${nativeAlias.provider} (${nativeAlias.owned_by ?? nativeAlias.provider}).`,
222
223
  5,
223
224
  nativeAlias,
224
225
  exactComboSlugs,
@@ -276,7 +277,7 @@ export function buildCatalogEntriesFromObservedState({
276
277
  const e = deriveEntry(
277
278
  template,
278
279
  slug,
279
- `Routed via opencodex → ${m.provider} (${m.owned_by ?? m.provider}).`,
280
+ `${ROUTED_DESCRIPTION_PREFIX}${m.provider} (${m.owned_by ?? m.provider}).`,
280
281
  5,
281
282
  m,
282
283
  exactComboSlugs,
@@ -380,15 +381,15 @@ export function orderForModelPicker(
380
381
  * True when an existing catalog row was authored by OpenCodex routing (#855).
381
382
  * Every generated routed row — current full-slug form, the June–July 2026
382
383
  * provider-name form, and legacy combo aliases — carries the stable
383
- * description prefix `Routed via opencodex → `; foreign rows from Cursor or
384
+ * description prefix `Routed via opencodex → `, or — when a combo overrides its description
385
+ * (#7229) — the `opencodex_routed_origin` marker (./ownership); foreign rows from Cursor or
384
386
  * user tooling do not. `owned_by` describes upstream ownership, and `comp_hash`
385
387
  * describes history compatibility; neither is an authorship signal.
386
388
  */
387
389
  export function isOcxAuthoredRoutedEntry(entry: RawEntry): boolean {
388
390
  if (isNativeAliasCatalogEntry(entry)) return true;
389
- const desc = typeof entry.description === "string" ? entry.description : "";
390
391
  const slug = typeof entry.slug === "string" ? entry.slug : "";
391
- return slug.includes("/") && desc.startsWith("Routed via opencodex → ");
392
+ return slug.includes("/") && hasOcxRoutedAuthorship(entry);
392
393
  }
393
394
 
394
395
  function recoverableNativeSlug(entry: RawEntry): string | null {
@@ -503,6 +504,11 @@ export interface ObservedCatalogMergeInput {
503
504
  readonly multiAgentV2Enabled: boolean;
504
505
  readonly keepNativeChatGptOnV1?: boolean;
505
506
  readonly exactComboSlugs: ReadonlySet<string>;
507
+ /**
508
+ * Current combo row descriptions by public slug (#7229). When supplied, retained combo rows
509
+ * are re-projected onto it; absent means the caller has no config and leaves them unchanged.
510
+ */
511
+ readonly comboDescriptions?: ReadonlyMap<string, string>;
506
512
  readonly hasPhysicalComboProvider: boolean;
507
513
  readonly includeNativeOpenAi: boolean;
508
514
  readonly accountBoundEntries: readonly RawEntry[];
@@ -545,6 +551,7 @@ export function mergeCatalogEntriesFromObservedState({
545
551
  multiAgentV2Enabled,
546
552
  keepNativeChatGptOnV1,
547
553
  exactComboSlugs,
554
+ comboDescriptions,
548
555
  hasPhysicalComboProvider,
549
556
  includeNativeOpenAi,
550
557
  accountBoundEntries,
@@ -639,8 +646,7 @@ export function mergeCatalogEntriesFromObservedState({
639
646
  }
640
647
  const freshExactComboEntries = new Set(admittedRoutedEntries.filter(entry => (
641
648
  isExactComboCatalogEntry(entry, exactComboSlugs)
642
- && typeof entry.description === "string"
643
- && entry.description.startsWith(`Routed via opencodex → ${COMBO_NAMESPACE} (`)
649
+ && hasOcxComboAuthorship(entry)
644
650
  )));
645
651
  const rank = new Map(featured.map((slug, i) => [slug, i] as const));
646
652
  const freshEquivalentKeys = new Set(admittedRoutedEntries.flatMap(entry => (
@@ -802,6 +808,10 @@ export function mergeCatalogEntriesFromObservedState({
802
808
  delete entry[SPAWN_PRIORITY_FIELD];
803
809
  }
804
810
  const slug = String(entry.slug);
811
+ if (comboDescriptions !== undefined && exactComboSlugs.has(slug)
812
+ && (isNativeAliasCatalogEntry(entry) || (entry.owned_by === COMBO_NAMESPACE && hasOcxComboAuthorship(entry)))) {
813
+ reapplyRetainedComboDescription(entry, comboDescriptions.get(slug));
814
+ }
805
815
  if (!isOcxAuthoredRoutedEntry(entry)) continue;
806
816
  // The builder no longer copies a template's comp_hash onto routed rows (#5796), but a row
807
817
  // kept from disk may still carry one. Custom rows, Codex-forward aliases included, never
@@ -26,6 +26,7 @@ import {
26
26
  isGpt56NativeSlug,
27
27
  } from "./effort";
28
28
  import { CATALOG_INACTIVE_REASON_FIELD, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
29
+ import { applyRoutedDescription } from "./ownership";
29
30
 
30
31
  export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
31
32
  if (priority !== 9) clone.priority = priority;
@@ -128,7 +129,7 @@ export function deriveEntry(
128
129
  delete e[SPAWN_PRIORITY_FIELD];
129
130
  e.slug = slug;
130
131
  e.display_name = routedDisplayName(slug, model);
131
- e.description = desc;
132
+ applyRoutedDescription(e, desc, model);
132
133
  e.priority = priority;
133
134
  e.visibility = "list";
134
135
  if ("upgrade" in e) e.upgrade = null;
@@ -212,6 +213,7 @@ export function deriveEntry(
212
213
  : { web_search_tool_type: "text_and_image", supports_search_tool: true }
213
214
  : {}),
214
215
  };
216
+ applyRoutedDescription(entry, desc, model);
215
217
  if (isRouted) {
216
218
  applyRoutedCodexToolMode(entry, model?.codexToolMode);
217
219
  applyReasoningLevels(
@@ -38,6 +38,7 @@ import type { RawEntry } from "./parsing";
38
38
  import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled";
39
39
  import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
40
40
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
41
+ import { ROUTED_ORIGIN_FIELD } from "./ownership";
41
42
  import { RESERVE_METADATA_SOURCE_FIELD } from "./reserve";
42
43
  import {
43
44
  ACCOUNT_GATED_NATIVE_OPENAI_MODELS,
@@ -813,6 +814,7 @@ export function observedReserveCatalogSource(
813
814
  }
814
815
  source.slug = NATIVE_RESERVE_MODEL;
815
816
  delete source.opencodex_catalog_kind;
817
+ delete source[ROUTED_ORIGIN_FIELD];
816
818
  delete source[RESERVE_METADATA_SOURCE_FIELD];
817
819
  delete source[ACCOUNT_BOUND_OBSERVED_NATIVE_MARKER];
818
820
  delete source[ACCOUNT_BOUND_OBSERVED_SELECTORS_MARKER];
@@ -189,7 +189,6 @@ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds:
189
189
  );
190
190
  }
191
191
  export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
192
- if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
193
192
  // xAI /models advertises both the dated deployment and this floating alias.
194
193
  // Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id.
195
194
  if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false;
@@ -205,7 +204,6 @@ export function shouldRetainConfiguredProviderModel(
205
204
  prov?: OcxProviderConfig,
206
205
  ): boolean {
207
206
  if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
208
- if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
209
207
  if (modelInList(prov?.retainModels, modelId)) return true;
210
208
  return false;
211
209
  }
@@ -0,0 +1,71 @@
1
+ import { COMBO_NAMESPACE } from "../../combos/identifiers";
2
+ import type { CatalogModel, RawEntry } from "./parsing";
3
+
4
+ /**
5
+ * Authorship of OpenCodex-generated routed catalog rows (#855, #7229).
6
+ *
7
+ * A generated routed row normally carries the description prefix below, and that prefix is what
8
+ * removal, template selection, auto-review and residue detection have always recognized. A combo
9
+ * may replace its row description (#7229), so such a row carries a stable marker instead. Rows
10
+ * with the generated description stay byte-identical and keep prefix recognition; only an exact
11
+ * known marker value confers authorship.
12
+ */
13
+ export const ROUTED_DESCRIPTION_PREFIX = "Routed via opencodex → ";
14
+ export const ROUTED_ORIGIN_FIELD = "opencodex_routed_origin";
15
+ export const COMBO_ROUTED_ORIGIN = "combo-v1";
16
+ const COMBO_DESCRIPTION_PREFIX = `${ROUTED_DESCRIPTION_PREFIX}${COMBO_NAMESPACE} (`;
17
+
18
+ function descriptionStartsWith(entry: RawEntry, prefix: string): boolean {
19
+ return typeof entry.description === "string" && entry.description.startsWith(prefix);
20
+ }
21
+
22
+ /** True for a row whose description was overridden by a combo and stamped by OpenCodex. */
23
+ export function hasComboRoutedOriginMarker(entry: RawEntry): boolean {
24
+ return entry[ROUTED_ORIGIN_FIELD] === COMBO_ROUTED_ORIGIN;
25
+ }
26
+
27
+ /** Generated by OpenCodex routing: the stable marker, or the legacy generated description. */
28
+ export function hasOcxRoutedAuthorship(entry: RawEntry): boolean {
29
+ return hasComboRoutedOriginMarker(entry) || descriptionStartsWith(entry, ROUTED_DESCRIPTION_PREFIX);
30
+ }
31
+
32
+ /** Generated for a combo: the stable marker, or the legacy generated combo description. */
33
+ export function hasOcxComboAuthorship(entry: RawEntry): boolean {
34
+ return hasComboRoutedOriginMarker(entry) || descriptionStartsWith(entry, COMBO_DESCRIPTION_PREFIX);
35
+ }
36
+
37
+ /** The operator's combo description override, if this routed model has one. */
38
+ export function comboDescriptionOverride(model: CatalogModel | undefined): string | undefined {
39
+ if (model?.provider !== COMBO_NAMESPACE) return undefined;
40
+ const description = typeof model.description === "string" ? model.description.trim() : "";
41
+ return description || undefined;
42
+ }
43
+
44
+ /**
45
+ * Set a routed row's description and its authorship marker together. A template clone may carry
46
+ * another row's marker, so the marker is always rewritten from this model's own override.
47
+ */
48
+ export function applyRoutedDescription(entry: RawEntry, generated: string, model: CatalogModel | undefined): void {
49
+ const override = comboDescriptionOverride(model);
50
+ entry.description = override ?? generated;
51
+ if (override !== undefined) entry[ROUTED_ORIGIN_FIELD] = COMBO_ROUTED_ORIGIN;
52
+ else delete entry[ROUTED_ORIGIN_FIELD];
53
+ }
54
+
55
+ /**
56
+ * A combo row retained from the previous catalog (its fresh derivation failed, e.g. a member
57
+ * provider is temporarily unavailable) bypasses deriveEntry. Re-project the combo's CURRENT
58
+ * description onto it so clearing or changing the description in config still reaches the
59
+ * picker. A cleared override returns the row to the generated combo text without the marker;
60
+ * an unmarked row without a current override is left exactly as it was.
61
+ */
62
+ export function reapplyRetainedComboDescription(entry: RawEntry, current: string | undefined): void {
63
+ const description = current?.trim();
64
+ if (description) {
65
+ entry.description = description;
66
+ entry[ROUTED_ORIGIN_FIELD] = COMBO_ROUTED_ORIGIN;
67
+ } else if (hasComboRoutedOriginMarker(entry)) {
68
+ entry.description = `${ROUTED_DESCRIPTION_PREFIX}${COMBO_NAMESPACE} (${COMBO_NAMESPACE}).`;
69
+ delete entry[ROUTED_ORIGIN_FIELD];
70
+ }
71
+ }
@@ -43,6 +43,7 @@ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREA
43
43
  import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
44
44
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
45
45
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
46
+ import { hasComboRoutedOriginMarker, hasOcxRoutedAuthorship } from "./ownership";
46
47
  import { NATIVE_GPT6_ASTRA_MODEL } from "./native-models";
47
48
  import { recordOwnedConfigPath } from "../../lib/config-ownership";
48
49
 
@@ -95,6 +96,12 @@ export interface CatalogModel {
95
96
  * which carries no CatalogModel, so a configured displayName can never override a native name.
96
97
  */
97
98
  displayName?: string;
99
+ /**
100
+ * Display-only Codex catalog `description` override, honored for combo rows only (#7229).
101
+ * Unset keeps the generated "Routed via opencodex → …" text; a set value stamps the row with
102
+ * the ownership marker in ./ownership so removal and template selection still recognize it.
103
+ */
104
+ description?: string;
98
105
  owned_by?: string;
99
106
  reasoningEfforts?: string[];
100
107
  /** Exact CCA family evidence, retained through the in-memory discovery cache. */
@@ -297,7 +304,7 @@ export function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null
297
304
  && "base_instructions" in m
298
305
  && m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
299
306
  && m.owned_by !== COMBO_NAMESPACE
300
- && !(typeof m.description === "string" && m.description.startsWith("Routed via opencodex → ")),
307
+ && !hasOcxRoutedAuthorship(m),
301
308
  ) ?? null;
302
309
  }
303
310
 
@@ -325,7 +332,7 @@ export function findSupportedNativeTemplate(catalog: RawCatalog | null): RawEntr
325
332
  && "base_instructions" in m
326
333
  && m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
327
334
  && m.owned_by !== COMBO_NAMESPACE
328
- && !(typeof m.description === "string" && m.description.startsWith("Routed via opencodex → ")),
335
+ && !hasOcxRoutedAuthorship(m),
329
336
  ) ?? null;
330
337
  }
331
338
 
@@ -925,7 +932,9 @@ export function readCatalogBackup(catalogPath: string): RawCatalog | null {
925
932
 
926
933
  export function catalogHasRoutedEntries(catalog: RawCatalog | null): boolean {
927
934
  return (catalog?.models ?? []).some(m => typeof m.slug === "string"
928
- && (m.slug.includes("/") || m.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND));
935
+ && (m.slug.includes("/")
936
+ || m.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND
937
+ || hasComboRoutedOriginMarker(m)));
929
938
  }
930
939
 
931
940
  export function writePristineCatalogBackup(backupPath: string, catalogPath: string, catalog: RawCatalog): void {
@@ -60,7 +60,7 @@ import { isMultiAgentV2Enabled } from "../features";
60
60
  import { clampCatalogModelsToCodexSupport } from "./effort";
61
61
  import { suppressedSyntheticMaxCatalogSlugs } from "./model-hints";
62
62
  import { filterCatalogVisibleModels, gatherRoutedModels, type CatalogGatherProviderModelOutcome } from "./provider-fetch";
63
- import { dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
63
+ import { comboCatalogDescriptions, dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
64
64
  import {
65
65
  withCatalogWriteSerialization,
66
66
  type CatalogWritePermit,
@@ -530,6 +530,7 @@ function writeRetainedCatalogSync({
530
530
  multiAgentV2Enabled,
531
531
  keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
532
532
  exactComboSlugs,
533
+ comboDescriptions: comboCatalogDescriptions(config),
533
534
  hasPhysicalComboProvider,
534
535
  includeNativeOpenAi,
535
536
  accountBoundEntries,
@@ -4,6 +4,7 @@ import type { OcxConfig } from "../../types";
4
4
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
5
5
  import { isOcxAuthoredRoutedEntry } from "./build-entries";
6
6
  import { isNativeAliasCatalogEntry } from "./metadata";
7
+ import { hasOcxRoutedAuthorship } from "./ownership";
7
8
  import type { RawCatalog, RawEntry } from "./parsing";
8
9
 
9
10
  /**
@@ -25,10 +26,9 @@ function routedNamespace(entry: RawEntry): string | null {
25
26
  if (trustedAccountBoundNativeCatalogSlug(entry) !== undefined) return null;
26
27
  if (isNativeAliasCatalogEntry(entry)) return COMBO_NAMESPACE;
27
28
  // Combo aliases may be bare or use an unrelated provider namespace. Ownership selects the
28
- // combo namespace only with the same generated-description authorship signal as routed rows.
29
- if (entry.owned_by === COMBO_NAMESPACE
30
- && typeof entry.description === "string"
31
- && entry.description.startsWith("Routed via opencodex → ")) return COMBO_NAMESPACE;
29
+ // combo namespace only with the same authorship signal as routed rows (generated description or
30
+ // the #7229 marker a custom combo description carries).
31
+ if (entry.owned_by === COMBO_NAMESPACE && hasOcxRoutedAuthorship(entry)) return COMBO_NAMESPACE;
32
32
  // Foreign rows (Cursor, user tooling) are never removed by the provider rule.
33
33
  if (!isOcxAuthoredRoutedEntry(entry)) return null;
34
34
  return entry.slug.slice(0, entry.slug.indexOf("/"));
@@ -53,7 +53,7 @@ import {
53
53
  orderForSubagents,
54
54
  } from "./catalog/sync";
55
55
  import { multiAgentV2EnabledFromConfigText } from "./features";
56
- import { enforceCatalogSlugUniqueness, exactComboCatalogSlugs } from "./catalog/aggregation";
56
+ import { comboCatalogDescriptions, enforceCatalogSlugUniqueness, exactComboCatalogSlugs } from "./catalog/aggregation";
57
57
  import {
58
58
  isNativeAliasCatalogEntry,
59
59
  accountBoundNativeOpenAiSlugs,
@@ -392,6 +392,7 @@ function prepareCatalog(
392
392
  multiAgentV2Enabled,
393
393
  keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
394
394
  exactComboSlugs,
395
+ comboDescriptions: comboCatalogDescriptions(config),
395
396
  hasPhysicalComboProvider,
396
397
  includeNativeOpenAi,
397
398
  accountBoundEntries,