@bitkyc08/opencodex 2.43.0 → 2.44.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 (138) hide show
  1. package/README.md +25 -17
  2. package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
  3. package/gui/dist/assets/index-ltx3L-WS.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/command-code.ts +23 -2
  7. package/src/adapters/cursor/native-exec.ts +63 -11
  8. package/src/adapters/cursor/tool-definitions.ts +1 -1
  9. package/src/adapters/cursor/tool-schemas.ts +59 -0
  10. package/src/adapters/kiro.ts +54 -8
  11. package/src/adapters/openai-chat.ts +23 -6
  12. package/src/adapters/openai-responses.ts +3 -1
  13. package/src/adapters/opencode-go.ts +35 -0
  14. package/src/claude/agents-inject.ts +12 -5
  15. package/src/claude/desktop-3p-library.ts +89 -0
  16. package/src/claude/desktop-3p.ts +166 -95
  17. package/src/claude/desktop-discovery-inputs.ts +44 -0
  18. package/src/claude/desktop-profile.ts +1 -1
  19. package/src/claude/desktop-remote-store-artifact.ts +185 -0
  20. package/src/claude/desktop-remote-store-io.ts +91 -0
  21. package/src/claude/desktop-remote-store-state.ts +127 -0
  22. package/src/claude/desktop-remote-store.ts +269 -0
  23. package/src/claude/inbound-model-options.ts +10 -2
  24. package/src/claude/inbound-records.ts +7 -0
  25. package/src/claude/inbound.ts +1 -1
  26. package/src/claude/model-info.ts +16 -3
  27. package/src/cli/account-api.ts +7 -1
  28. package/src/cli/aside-profiles.ts +17 -0
  29. package/src/cli/capabilities.ts +39 -0
  30. package/src/cli/claude-agent-startup-sync.ts +3 -1
  31. package/src/cli/claude-desktop.ts +127 -18
  32. package/src/cli/connect.ts +27 -14
  33. package/src/cli/dispatch.ts +30 -16
  34. package/src/cli/export-command.ts +3 -1
  35. package/src/cli/index.ts +28 -16
  36. package/src/cli/integrations.ts +46 -12
  37. package/src/cli/opencode.ts +11 -4
  38. package/src/cli/runtime-api.ts +7 -2
  39. package/src/cli/uninstall-client-state.ts +78 -0
  40. package/src/client/connect.ts +476 -223
  41. package/src/client/hub-client.ts +81 -0
  42. package/src/client/hub-relay.ts +38 -6
  43. package/src/client/lifecycle-lock.ts +129 -0
  44. package/src/client/state.ts +71 -42
  45. package/src/clients/aside-profiles.ts +224 -0
  46. package/src/codex/account-lifecycle.ts +5 -9
  47. package/src/codex/auth-api.ts +50 -4
  48. package/src/codex/catalog/metadata.ts +0 -2
  49. package/src/codex/catalog/provider-fetch.ts +7 -5
  50. package/src/codex/catalog/sync.ts +88 -24
  51. package/src/codex/convergence.ts +2 -0
  52. package/src/codex/inject.ts +45 -14
  53. package/src/codex/journal.ts +6 -6
  54. package/src/codex/quota-refresh-outcome.ts +27 -0
  55. package/src/codex/shim.ts +7 -1
  56. package/src/combos/resolve.ts +10 -7
  57. package/src/config.ts +18 -5
  58. package/src/generated/compatibility-version.json +209 -105
  59. package/src/images/loop.ts +5 -2
  60. package/src/integrations/aside-profile-context.ts +270 -0
  61. package/src/integrations/aside-profile-journal.ts +229 -0
  62. package/src/integrations/aside-profiles.ts +176 -0
  63. package/src/integrations/catalog-refresh.ts +40 -0
  64. package/src/integrations/config-io.ts +16 -1
  65. package/src/integrations/owned-refresh.ts +14 -2
  66. package/src/integrations/state.ts +3 -1
  67. package/src/integrations/writer.ts +3 -1
  68. package/src/lab/fabric/producer-isolate.ts +84 -46
  69. package/src/lib/account-selection-events.ts +32 -0
  70. package/src/lib/errors.ts +9 -1
  71. package/src/lib/provider-outbound.ts +2 -45
  72. package/src/lib/proxy-env.ts +67 -0
  73. package/src/oauth/account-quota-rank.ts +9 -1
  74. package/src/oauth/anthropic-routing.ts +91 -23
  75. package/src/oauth/generic-account-failover.ts +15 -53
  76. package/src/oauth/index.ts +67 -68
  77. package/src/oauth/pool-settings-capability.ts +2 -1
  78. package/src/oauth/store.ts +91 -7
  79. package/src/oauth/types.ts +8 -0
  80. package/src/providers/api-key-selection.ts +110 -0
  81. package/src/providers/api-keys.ts +38 -31
  82. package/src/providers/context-cap.ts +27 -3
  83. package/src/providers/key-failover.ts +27 -17
  84. package/src/providers/provider-id-rewrite.ts +10 -8
  85. package/src/providers/registry.ts +1 -0
  86. package/src/responses/custom-tool-compat.ts +10 -0
  87. package/src/responses/function-call-compat.ts +173 -0
  88. package/src/responses/namespace-tool-compat.ts +27 -4
  89. package/src/responses/parser.ts +8 -0
  90. package/src/responses/task-input.ts +36 -0
  91. package/src/responses/tool-name-aliases.ts +79 -0
  92. package/src/router.ts +2 -0
  93. package/src/routing/capability.ts +10 -3
  94. package/src/routing/compatibility/assemble.ts +16 -1
  95. package/src/routing/evaluator.ts +5 -1
  96. package/src/server/auth-cors.ts +3 -0
  97. package/src/server/chat-native.ts +44 -9
  98. package/src/server/claude-messages.ts +71 -36
  99. package/src/server/grok-responses-snapshot-repair.ts +333 -0
  100. package/src/server/index.ts +26 -10
  101. package/src/server/management/account-selection-stream.ts +70 -0
  102. package/src/server/management/aside-profile-routes.ts +231 -0
  103. package/src/server/management/config-routes.ts +36 -41
  104. package/src/server/management/context.ts +3 -0
  105. package/src/server/management/integration-routes.ts +61 -3
  106. package/src/server/management/logs-usage-routes.ts +1 -0
  107. package/src/server/management/model-routes.ts +20 -9
  108. package/src/server/management/model-rows.ts +10 -2
  109. package/src/server/management/oauth-account-routes.ts +7 -0
  110. package/src/server/management/provider-routes.ts +10 -8
  111. package/src/server/management/route-registry.ts +20 -2
  112. package/src/server/management-auth.ts +16 -0
  113. package/src/server/relay-eager.ts +17 -5
  114. package/src/server/relay.ts +43 -4
  115. package/src/server/request-log.ts +29 -1
  116. package/src/server/responses/agent-task-recovery-cache.ts +11 -0
  117. package/src/server/responses/agent-task-recovery.ts +24 -4
  118. package/src/server/responses/codex-ws-pool.ts +4 -4
  119. package/src/server/responses/codex-ws-session.ts +2 -2
  120. package/src/server/responses/collaboration.ts +2 -1
  121. package/src/server/responses/combo-stream-preflight.ts +26 -7
  122. package/src/server/responses/core.ts +668 -151
  123. package/src/server/responses/fetch-helpers.ts +6 -1
  124. package/src/server/responses/ws-upstream.ts +8 -3
  125. package/src/server/responses-custom-tool-repair.ts +29 -9
  126. package/src/server/responses-function-tool-repair.ts +183 -0
  127. package/src/server/responses-snapshot-codec.ts +14 -0
  128. package/src/server/responses-snapshot-repair.ts +1 -13
  129. package/src/server/responses-undeclared-tool-guard.ts +1 -68
  130. package/src/server/sse-payload-rewrite.ts +3 -1
  131. package/src/service.ts +6 -2
  132. package/src/types/config.ts +23 -16
  133. package/src/types/provider.ts +14 -3
  134. package/src/types/request.ts +2 -0
  135. package/src/usage/log.ts +9 -0
  136. package/src/web-search/loop.ts +5 -2
  137. package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
  138. package/gui/dist/assets/index-Djowl68T.js +0 -112
@@ -88,15 +88,15 @@ import {
88
88
  export const MAX_SPAWN_AGENT_MODEL_OVERRIDES = 5;
89
89
 
90
90
  // Base for config.modelPickerOrder display priorities (#1649). modelPickerOrder is a DISPLAY-ONLY
91
- // reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but never the
92
- // spawn_agent candidate window. The window is derived from SPAWN_PRIORITY_FIELD (the natural
93
- // priority captured before the override), so display order and spawn candidates are decoupled.
91
+ // reordering of the Codex model picker: it rewrites a row's Codex-visible `priority` but not
92
+ // OpenCodex's natural-priority guidance window. Native Codex advertisements still follow the
93
+ // visible priority and can differ from that guidance window.
94
94
  export const PICKER_ORDER_PRIORITY_BASE = 1_000;
95
95
 
96
- // OpenCodex-private catalog field: the spawn_agent candidate priority a row would have WITHOUT
96
+ // OpenCodex-private catalog field: the guidance candidate priority a row would have WITHOUT
97
97
  // modelPickerOrder. Codex ignores unknown catalog fields (same as opencodex_catalog_kind), so this
98
- // is invisible to Codex; effectiveSubagentRoster reads it so a display reorder cannot change which
99
- // rows are spawn_agent candidates. Absent on rows modelPickerOrder did not move.
98
+ // is invisible to Codex; effectiveSubagentRoster reads it to keep OpenCodex guidance candidates
99
+ // independent of display order. It does not freeze native advertisements. Absent on unmoved rows.
100
100
  export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
101
101
 
102
102
  export type SpawnAgentSurface = "v1" | "v2";
@@ -154,7 +154,9 @@ export interface SubagentRosterExclusion {
154
154
  }
155
155
 
156
156
  export interface EffectiveSubagentRoster {
157
+ /** OpenCodex's natural-priority guidance projection, not captured native tool text. */
157
158
  candidates: EffectiveSubagentModel[];
159
+ /** Configured models within that projection; exact-name eligibility is a separate check. */
158
160
  advertised: EffectiveSubagentModel[];
159
161
  excluded: SubagentRosterExclusion[];
160
162
  }
@@ -191,8 +193,8 @@ export function effectiveSubagentRoster(
191
193
  .filter(({ entry }) => entry.visibility === "list")
192
194
  .filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
193
195
  .sort((left, right) => {
194
- // Spawn candidates rank by the natural priority (SPAWN_PRIORITY_FIELD when present), so a
195
- // modelPickerOrder display reorder (#1649) can never change candidate membership. Rows the
196
+ // OpenCodex guidance candidates rank by natural priority (SPAWN_PRIORITY_FIELD when present),
197
+ // so modelPickerOrder does not change this projection. Native tool advertisements differ. Rows the
196
198
  // override did not move fall back to their Codex-visible `priority`.
197
199
  const spawnPriorityOf = (entry: RawEntry): number => {
198
200
  const spawn = entry[SPAWN_PRIORITY_FIELD];
@@ -315,6 +317,8 @@ export function deriveEntry(
315
317
  contextCap?: NativeContextLimitsInput,
316
318
  ): RawEntry {
317
319
  const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
320
+ // Go exposes model-specific upstream enums; synthetic tiers mislead subagent overrides.
321
+ const preserveExactReasoning = preserveExact || model?.provider === "opencode-go";
318
322
  const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
319
323
  ? upstreamNativeEntry(model.id)
320
324
  : null;
@@ -328,6 +332,8 @@ export function deriveEntry(
328
332
  }
329
333
  if (template || codexForwardNativeCapabilityAlias) {
330
334
  const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
335
+ // A cached template may carry display-order history; each new row owns its natural rank.
336
+ delete e[SPAWN_PRIORITY_FIELD];
331
337
  e.slug = slug;
332
338
  e.display_name = routedDisplayName(slug, model);
333
339
  e.description = desc;
@@ -359,7 +365,7 @@ export function deriveEntry(
359
365
  e,
360
366
  model?.reasoningEfforts,
361
367
  model?.defaultReasoningEffort,
362
- preserveExact || codexForwardNativeCapabilityAlias !== null,
368
+ preserveExactReasoning || codexForwardNativeCapabilityAlias !== null,
363
369
  );
364
370
  // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
365
371
  // native tool/search/responses-lite contract while preserving the routed slug and wire id.
@@ -409,7 +415,7 @@ export function deriveEntry(
409
415
  };
410
416
  if (isRouted) {
411
417
  applyRoutedCodexToolMode(entry, model?.codexToolMode);
412
- applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
418
+ applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning);
413
419
  }
414
420
  else {
415
421
  applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
@@ -516,27 +522,25 @@ export function buildCatalogEntriesFromObservedState({
516
522
  // catalog stays put across rebuilds. Featured rows keep their existing 0..N-1 band; when
517
523
  // modelPickerOrder is unset the helper is a no-op and every priority below is byte-identical to
518
524
  // before. The spawn_agent candidate window is derived separately from SPAWN_PRIORITY_FIELD, so
519
- // this display reorder cannot change which rows are spawn candidates.
520
- const pickerOrder = Array.isArray(modelPickerOrder)
521
- ? modelPickerOrder.filter((id): id is string => typeof id === "string" && id.length > 0)
522
- : [];
525
+ // this display reorder does not change OpenCodex's guidance candidate calculation.
526
+ const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
523
527
  const pickerOrderRank = new Map(pickerOrder.map((slug, i) => [slug, i] as const));
524
528
  const pickerOrderActive = pickerOrder.length > 0;
525
529
  // The display band reuses the existing high priority tier (>= PICKER_ORDER_PRIORITY_BASE, the
526
530
  // same 1_000+ neighborhood account rows occupy), keeping listed rows visually after the featured
527
- // band. Candidate membership does not depend on this — see SPAWN_PRIORITY_FIELD.
531
+ // band. OpenCodex guidance membership does not depend on this — see SPAWN_PRIORITY_FIELD.
528
532
  /**
529
533
  * Priority for a non-featured routed row that is explicitly LISTED in modelPickerOrder. Listed
530
534
  * slugs sort in declared order within the high picker-order display tier
531
535
  * (>= PICKER_ORDER_PRIORITY_BASE). This sets the Codex-visible `priority` only; the caller records
532
- * the row's natural priority in SPAWN_PRIORITY_FIELD so the spawn_agent candidate window is
533
- * unchanged. Returns undefined when the feature is off or the row is not listed, so those rows
536
+ * the row's natural priority in SPAWN_PRIORITY_FIELD for OpenCodex's unchanged guidance window.
537
+ * Returns undefined when the feature is off or the row is not listed, so those rows
534
538
  * keep their original assignment (default 5 / account 1_000+) untouched.
535
539
  *
536
540
  * Scope: only the generic routed `<provider>/<model>` rows call this (see the goModels loop
537
541
  * below). Native passthrough rows and account-qualified native rows keep their own priority
538
- * logic and are intentionally not reordered here this matches the documented contract on
539
- * OcxConfig.modelPickerOrder (route native ordering through subagentModels instead).
542
+ * logic and are intentionally not reordered in this legacy builder pass. The final merge can
543
+ * apply complete ordering when the configured list includes a bare id.
540
544
  */
541
545
  const pickerOrderPriority = (slug: string, altSlug?: string): number | undefined => {
542
546
  if (!pickerOrderActive) return undefined;
@@ -658,9 +662,9 @@ export function buildCatalogEntriesFromObservedState({
658
662
  // Keep the generated account rows together in Codex's priority-sorted flat picker.
659
663
  e.priority = 1_000 + (typeof e.priority === "number" ? e.priority : 5);
660
664
  }
661
- // #1649: modelPickerOrder is a DISPLAY-ONLY override. Record the natural priority spawn_agent
662
- // must keep using, then let modelPickerOrder move only the Codex-visible `priority`. Featured
663
- // rows are never overridden (their rank is authoritative for both display and spawn).
665
+ // The legacy routed-only builder pass keeps featured ranks and records natural priority
666
+ // before changing non-featured display priority. The final complete-order pass may move
667
+ // featured display rows too; OpenCodex guidance continues to use their natural ranks.
664
668
  if (rankHit === undefined) {
665
669
  const pickerPriority = pickerOrderPriority(slug, `${m.provider}/${m.id}`);
666
670
  if (pickerPriority !== undefined) {
@@ -779,12 +783,39 @@ export const CANONICAL_NATIVE_CATALOG_CONTENT_POLICY: Readonly<
779
783
  unsupportedNativeEntries: "drop",
780
784
  });
781
785
 
786
+ function normalizeModelPickerOrder(order: unknown): string[] {
787
+ return Array.isArray(order)
788
+ ? order.filter((id): id is string => typeof id === "string" && id.trim().length > 0)
789
+ : [];
790
+ }
791
+
792
+ /** Preserve exact-id precedence while accepting the existing raw/encoded slug spellings. */
793
+ function modelPickerRank(order: readonly string[]): (slug: string) => number | undefined {
794
+ const exact = new Map(order.map((slug, index) => [slug, index]));
795
+ const equivalent = new Map(order.map((slug, index) => [slugEquivalenceKey(slug), index]));
796
+ return slug => exact.get(slug) ?? equivalent.get(slugEquivalenceKey(slug));
797
+ }
798
+
799
+ /** Complete display ordering retains natural ranks for OpenCodex's separate guidance projection. */
800
+ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly string[]): void {
801
+ const pickerOrder = normalizeModelPickerOrder(order);
802
+ if (!pickerOrder.some(slug => !slug.includes("/"))) return;
803
+ const rankOf = modelPickerRank(pickerOrder);
804
+ for (const entry of entries) {
805
+ const natural = entry[SPAWN_PRIORITY_FIELD] ?? entry.priority ?? 9;
806
+ entry[SPAWN_PRIORITY_FIELD] = natural;
807
+ entry.priority = rankOf(String(entry.slug)) ?? pickerOrder.length + Number(natural);
808
+ }
809
+ }
810
+
782
811
  export interface ObservedCatalogMergeInput {
783
812
  readonly catalogModels: readonly RawEntry[];
784
813
  readonly baselineCatalogModels: readonly RawEntry[];
785
814
  readonly routedEntries: readonly RawEntry[];
786
815
  readonly baseline: ReadonlyMap<string, number>;
787
816
  readonly featured: readonly string[];
817
+ readonly modelPickerOrder?: readonly string[];
818
+ readonly accountSelectors?: readonly string[];
788
819
  readonly wsEnabled: boolean;
789
820
  readonly template: RawEntry | null;
790
821
  readonly disabledModels: ReadonlySet<string>;
@@ -817,6 +848,8 @@ export function mergeCatalogEntriesFromObservedState({
817
848
  routedEntries,
818
849
  baseline,
819
850
  featured,
851
+ modelPickerOrder = [],
852
+ accountSelectors = [],
820
853
  wsEnabled,
821
854
  template,
822
855
  disabledModels,
@@ -975,7 +1008,9 @@ export function mergeCatalogEntriesFromObservedState({
975
1008
  finished.priority = nativePriority(slug, upstream.priority);
976
1009
  return finished;
977
1010
  }
978
- const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m.priority) });
1011
+ const preserved = normalizeServiceTiers({ ...m, priority: nativePriority(slug, m[SPAWN_PRIORITY_FIELD] ?? m.priority) });
1012
+ // Recompute spawn rank from current featured models, not a prior picker override.
1013
+ delete preserved[SPAWN_PRIORITY_FIELD];
979
1014
  // Older natives kept from disk still need the mock top tiers (max + ultra always
980
1015
  // for subagent max spawns; wire-clamped to the model's real top rung).
981
1016
  if (!isGpt56NativeSlug(slug) && slug !== NATIVE_RESERVE_MODEL) ensureUltraReasoningLevel(preserved);
@@ -1060,6 +1095,32 @@ export function mergeCatalogEntriesFromObservedState({
1060
1095
  // remain outside provider ownership and survive unless a fresh row replaces their exact slug.
1061
1096
  return !isOcxAuthoredRoutedEntry(entry);
1062
1097
  });
1098
+ // Retained rows bypass the builder. Recompute managed spawn ranks from current config
1099
+ // before either display-order mode; a saved display override is not current roster authority.
1100
+ const pickerOrder = normalizeModelPickerOrder(modelPickerOrder);
1101
+ const fullPickerOrder = pickerOrder.some(slug => !slug.includes("/"));
1102
+ const rankOf = modelPickerRank(pickerOrder);
1103
+ const featuredRankOf = modelPickerRank(featured);
1104
+ const priorityStride = Math.max(accountSelectors.length, 1);
1105
+ for (const entry of preservedRoutedEntries) {
1106
+ const natural = entry[SPAWN_PRIORITY_FIELD];
1107
+ if (typeof natural === "number") {
1108
+ entry.priority = natural;
1109
+ delete entry[SPAWN_PRIORITY_FIELD];
1110
+ }
1111
+ const slug = String(entry.slug);
1112
+ if (!isOcxAuthoredRoutedEntry(entry) || isNativeAliasCatalogEntry(entry)) continue;
1113
+ const featuredRank = featuredRankOf(slug);
1114
+ entry.priority = featuredRank !== undefined
1115
+ ? featuredRank * priorityStride
1116
+ : (accountSelectors.length > 0 ? 1_000 : 0) + 5;
1117
+ if (featuredRank !== undefined || fullPickerOrder) continue;
1118
+ const pickerIndex = rankOf(slug);
1119
+ if (pickerIndex !== undefined) {
1120
+ entry[SPAWN_PRIORITY_FIELD] = entry.priority;
1121
+ entry.priority = PICKER_ORDER_PRIORITY_BASE + pickerIndex * priorityStride;
1122
+ }
1123
+ }
1063
1124
  let finalRoutedEntries = [...admittedRoutedEntries, ...preservedRoutedEntries];
1064
1125
  finalRoutedEntries = finalRoutedEntries.filter(entry => {
1065
1126
  const slug = typeof entry.slug === "string" ? entry.slug : "";
@@ -1134,7 +1195,7 @@ export function mergeCatalogEntriesFromObservedState({
1134
1195
  // Mock-max universality (260709): preserved routed entries from disk may predate
1135
1196
  // the max rung — ensure it here so subagent max spawns validate on every
1136
1197
  // reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
1137
- if (!exactCombo && !reserveProjection) {
1198
+ if (!exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
1138
1199
  const levels = Array.isArray(e.supported_reasoning_levels)
1139
1200
  ? e.supported_reasoning_levels as Array<{ effort?: string }>
1140
1201
  : [];
@@ -1161,6 +1222,7 @@ export function mergeCatalogEntriesFromObservedState({
1161
1222
  multiAgentV2Enabled,
1162
1223
  { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
1163
1224
  );
1225
+ applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
1164
1226
  for (const entry of versionedEntries) {
1165
1227
  const kind = entry.opencodex_catalog_kind;
1166
1228
  if (trustedAccountBoundNativeCatalogSlug(entry) === undefined
@@ -1762,6 +1824,8 @@ function writeRetainedCatalogSync({
1762
1824
  }).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
1763
1825
  : [];
1764
1826
  catalog.models = mergeCatalogEntriesFromObservedState({
1827
+ modelPickerOrder,
1828
+ accountSelectors,
1765
1829
  catalogModels: catalogModelsForMerge,
1766
1830
  baselineCatalogModels: baselineCatalog?.models ?? [],
1767
1831
  routedEntries: goEntries,
@@ -342,6 +342,8 @@ function prepareCatalog(
342
342
  )),
343
343
  );
344
344
  const mergedModels = mergeCatalogEntriesFromObservedState({
345
+ modelPickerOrder,
346
+ accountSelectors,
345
347
  catalogModels,
346
348
  baselineCatalogModels,
347
349
  routedEntries,
@@ -6,6 +6,7 @@ import {
6
6
  readConfigAdmissionSnapshot,
7
7
  subagentDefaultSyncEffective,
8
8
  websocketsEnabled,
9
+ withConfigMutationLockSync,
9
10
  } from "../config";
10
11
  import { CodexWriteLockSkipped, withCodexWriteLock } from "./codex-write-lock";
11
12
  import { shouldSyncCodexOnStart } from "./desired-state";
@@ -148,6 +149,18 @@ export interface InjectCodexOptions {
148
149
  /** Explicit remote routing target. Absence preserves byte-compatible standalone output. */
149
150
  routingTarget?: CodexRoutingTarget;
150
151
  journalOwner?: { kind: "process" } | { kind: "client"; apiKeyId: string };
152
+ /** Synchronous read-only client ownership guard, evaluated at the artifact commit boundary. */
153
+ beforeClientWrite?: () => void;
154
+ }
155
+
156
+ function runClientWriteGuard(guard: InjectCodexOptions["beforeClientWrite"]): void {
157
+ const result: unknown = guard?.();
158
+ if (result !== null && (typeof result === "object" || typeof result === "function")
159
+ && typeof (result as { then?: unknown }).then === "function") {
160
+ // Reject async guards without leaving their eventual rejection unhandled.
161
+ void Promise.resolve(result).catch(() => {});
162
+ throw new Error("Connected client write guard must be synchronous");
163
+ }
151
164
  }
152
165
 
153
166
  export interface CodexRoutingTarget {
@@ -901,7 +914,14 @@ export async function injectCodexConfig(
901
914
  if (activeProvider) {
902
915
  // A launcher may have journaled before the provider manager took ownership. Never let shutdown
903
916
  // replay that stale snapshot over externally managed config.
904
- if (!options.validateOnly) removeJournal();
917
+ if (!options.validateOnly) {
918
+ if (options.beforeClientWrite) {
919
+ withConfigMutationLockSync(() => {
920
+ runClientWriteGuard(options.beforeClientWrite);
921
+ removeJournal();
922
+ });
923
+ } else removeJournal();
924
+ }
905
925
  const nativeSubagentDefaultsWarning = configuredManagedSubagentDefaults(
906
926
  config,
907
927
  )
@@ -974,8 +994,8 @@ export async function injectCodexConfig(
974
994
  // not-ours (which would make them unrestorable).
975
995
  content = stripJournaledOpenaiBaseUrl(
976
996
  content,
977
- journaledInjectedOpenaiBaseUrl(),
978
- journaledInjectedRealtimeWsBaseUrl(),
997
+ journaledInjectedOpenaiBaseUrl({ readOnly: !!options.beforeClientWrite }),
998
+ journaledInjectedRealtimeWsBaseUrl({ readOnly: !!options.beforeClientWrite }),
979
999
  );
980
1000
  if (hasOcxProviderTable(content)) {
981
1001
  content = removeOcxSection(content);
@@ -1179,17 +1199,24 @@ export async function injectCodexConfig(
1179
1199
  let transitionReceipt: { nativeGeneration: number; currentTxId: string } | undefined;
1180
1200
 
1181
1201
  if (eligibility.kind === "legacy-uncoordinated") {
1182
- // Unchanged behavior for homes the coordinator cannot yet adopt. Stated
1183
- // rather than implied: this is the boundary, and adoption is its own phase.
1184
- if (!shouldSyncCodexOnStart(loadConfig())) {
1185
- return {
1186
- success: true,
1187
- status: "skipped",
1188
- skippedReason: "desired_disabled",
1189
- message: "Codex integration is OFF; no Codex config, catalog, cache, or history was changed.",
1190
- };
1191
- }
1192
- applyNativeArtifacts();
1202
+ const applyLegacy = (): CodexInjectResult | undefined => {
1203
+ if (!shouldSyncCodexOnStart(loadConfig())) {
1204
+ return {
1205
+ success: true,
1206
+ status: "skipped",
1207
+ skippedReason: "desired_disabled",
1208
+ message: "Codex integration is OFF; no Codex config, catalog, cache, or history was changed.",
1209
+ };
1210
+ }
1211
+ runClientWriteGuard(options.beforeClientWrite);
1212
+ applyNativeArtifacts();
1213
+ };
1214
+ // Only connected guarded writes add C here. A concurrent disconnect claim
1215
+ // either follows this commit or is observed by the guard before any write.
1216
+ const skipped = options.beforeClientWrite
1217
+ ? withConfigMutationLockSync(applyLegacy)
1218
+ : applyLegacy();
1219
+ if (skipped) return skipped;
1193
1220
  } else {
1194
1221
  const coordinated = await withCodexWriteLock(
1195
1222
  {
@@ -1210,6 +1237,10 @@ export async function injectCodexConfig(
1210
1237
  if (!shouldSyncCodexOnStart(loadConfig())) {
1211
1238
  throw new CodexWriteLockSkipped("desired_disabled");
1212
1239
  }
1240
+ // N and C are held here. Reject stale client work before publishing a
1241
+ // transition or capturing preimages; rejection must not compensate over
1242
+ // a disconnect's restored files.
1243
+ runClientWriteGuard(options.beforeClientWrite);
1213
1244
  /*
1214
1245
  * Publish BEFORE touching the filesystem. `assertPublished` runs after this
1215
1246
  * callback returns and throws unless a transition was recorded, so writing
@@ -161,13 +161,13 @@ export function markJournalInjectedState(
161
161
  * survives such a rewrite, so restore can still prove the URL is ours -- and, just as
162
162
  * importantly, prove that a DIFFERENT URL is not.
163
163
  */
164
- export function journaledInjectedOpenaiBaseUrl(): string | null {
165
- return readJournal()?.injectedOpenaiBaseUrl ?? null;
164
+ export function journaledInjectedOpenaiBaseUrl(options: { readOnly?: boolean } = {}): string | null {
165
+ return readJournal(options.readOnly !== true)?.injectedOpenaiBaseUrl ?? null;
166
166
  }
167
167
 
168
168
  /** The root `experimental_realtime_ws_base_url` the last injection wrote, or null. */
169
- export function journaledInjectedRealtimeWsBaseUrl(): string | null {
170
- return readJournal()?.injectedRealtimeWsBaseUrl ?? null;
169
+ export function journaledInjectedRealtimeWsBaseUrl(options: { readOnly?: boolean } = {}): string | null {
170
+ return readJournal(options.readOnly !== true)?.injectedRealtimeWsBaseUrl ?? null;
171
171
  }
172
172
 
173
173
  /** The catalog path the last injection wrote to, or null when none was recorded. */
@@ -179,14 +179,14 @@ export function removeJournal(): void {
179
179
  try { unlinkSync(JOURNAL_PATH); } catch { /* ignore */ }
180
180
  }
181
181
 
182
- function readJournal(): Journal | null {
182
+ function readJournal(cleanInvalid = true): Journal | null {
183
183
  if (!existsSync(JOURNAL_PATH)) return null;
184
184
  try {
185
185
  const journal = JSON.parse(readFileSync(JOURNAL_PATH, "utf-8")) as Journal;
186
186
  if (journal.version !== 1) throw new Error("unknown version");
187
187
  return journal;
188
188
  } catch {
189
- removeJournal();
189
+ if (cleanInvalid) removeJournal();
190
190
  return null;
191
191
  }
192
192
  }
@@ -0,0 +1,27 @@
1
+ /** Diagnostic only: never use this outcome as quota, entitlement, or admission evidence. */
2
+ export type CodexQuotaRefreshOutcome =
3
+ | { status: "http_error"; httpStatus: number }
4
+ | { status: "ok" | "not_reported" | "timeout" | "network_error" | "invalid_response" | "internal_error" };
5
+
6
+ /** The management response is untrusted at the CLI boundary; copy only the fixed vocabulary. */
7
+ export function projectCodexQuotaRefreshOutcome(value: unknown): CodexQuotaRefreshOutcome | undefined {
8
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
9
+ const row = value as Record<string, unknown>;
10
+ if (row.status === "http_error") {
11
+ return typeof row.httpStatus === "number" && Number.isInteger(row.httpStatus)
12
+ && row.httpStatus >= 100 && row.httpStatus <= 599
13
+ ? { status: "http_error", httpStatus: row.httpStatus }
14
+ : undefined;
15
+ }
16
+ switch (row.status) {
17
+ case "ok":
18
+ case "not_reported":
19
+ case "timeout":
20
+ case "network_error":
21
+ case "invalid_response":
22
+ case "internal_error":
23
+ return { status: row.status };
24
+ default:
25
+ return undefined;
26
+ }
27
+ }
package/src/codex/shim.ts CHANGED
@@ -961,14 +961,20 @@ function unixProcessGroupAlive(groupId: number): boolean {
961
961
  }
962
962
 
963
963
  function terminateUnixProcessGroup(groupId: number): void {
964
+ let permissionError: unknown;
964
965
  try {
965
966
  process.kill(-groupId, "SIGKILL");
966
967
  } catch (error) {
967
- if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error;
968
+ const code = (error as NodeJS.ErrnoException).code;
969
+ if (code === "EPERM") permissionError = error;
970
+ else if (code !== "ESRCH") throw error;
968
971
  }
972
+ // A concurrently exiting group can briefly reject a second signal. Only
973
+ // observed disappearance clears that uncertainty; never send another signal.
969
974
  const deadline = Date.now() + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS;
970
975
  while (Date.now() < deadline && unixProcessGroupAlive(groupId)) Bun.sleepSync(10);
971
976
  if (unixProcessGroupAlive(groupId)) {
977
+ if (permissionError) throw permissionError;
972
978
  throw new Error(`Codex shim install probe process group ${groupId} did not terminate`);
973
979
  }
974
980
  }
@@ -1,6 +1,7 @@
1
1
  import type { OcxComboTarget, OcxConfig } from "../types";
2
2
  import { getCachedProviderQuota } from "../providers/quota-routing-cache";
3
3
  import type { ProviderQuota } from "../providers/quota-types";
4
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
4
5
  import { sleepWithAbort } from "../lib/upstream-retry";
5
6
  import {
6
7
  coolComboTarget,
@@ -60,9 +61,13 @@ export class NoAvailableComboTargetsError extends Error {
60
61
  }
61
62
  }
62
63
 
63
- function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget): boolean {
64
- return Object.hasOwn(config.providers, target.provider)
65
- && config.providers[target.provider]?.disabled !== true;
64
+ function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now: number): boolean {
65
+ if (!Object.hasOwn(config.providers, target.provider)) return false;
66
+ const provider = config.providers[target.provider];
67
+ if (!provider || provider.disabled === true) return false;
68
+ // Native account selection owns model-scoped quota; a provider summary cannot veto it.
69
+ return isCanonicalOpenAiForwardProvider(provider)
70
+ || !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now);
66
71
  }
67
72
 
68
73
  function quotaWindowExhausted(percent: number | undefined, resetAt: number | undefined, now: number): boolean {
@@ -159,8 +164,7 @@ export function pickComboTarget(
159
164
  const excluded = new Set(options.exclude ?? []);
160
165
  const now = options.now ?? Date.now();
161
166
  const eligible = (target: Required<OcxComboTarget>): boolean =>
162
- targetProviderIsUsable(config, target)
163
- && !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
167
+ targetProviderIsUsable(config, target, now)
164
168
  && !isComboTargetInCooldown(comboId, target, now)
165
169
  && !excluded.has(targetKey(target))
166
170
  && (options.eligible?.(target) ?? true);
@@ -338,8 +342,7 @@ export async function pickComboTargetWithWait(
338
342
  const combo = getCombo(config, comboId);
339
343
  if (!combo) throw new UnknownComboError(comboId);
340
344
  const waitingTargets = combo.targets.filter(target =>
341
- targetProviderIsUsable(config, target)
342
- && !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
345
+ targetProviderIsUsable(config, target, now)
343
346
  && !excluded.has(targetKey(target))
344
347
  && isComboTargetInCooldown(comboId, target, now)
345
348
  && (customEligible?.(target) ?? true),
package/src/config.ts CHANGED
@@ -932,6 +932,15 @@ const clientIntegrationsSchema = z.object({
932
932
  "claude-desktop": z.boolean().optional().catch(undefined),
933
933
  }).passthrough();
934
934
 
935
+ const asideProfileSyncSchema = z.object({
936
+ allProfiles: z.boolean().optional(),
937
+ profiles: z.record(
938
+ z.string().regex(/^(0|[1-9][0-9]*)$/).refine(value => Number.isSafeInteger(Number(value))),
939
+ z.boolean(),
940
+ ).optional(),
941
+ legacyProfileId: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER).nullable().optional(),
942
+ }).passthrough();
943
+
935
944
  const agentTaskRecoverySchema = z.object({
936
945
  enabled: z.boolean().optional(),
937
946
  model: z.string().trim().min(1).optional(),
@@ -1134,7 +1143,10 @@ const configSchema = z.object({
1134
1143
  subagentModelsVersion: z.number().int().positive().optional().catch(undefined),
1135
1144
  subagentModels: z.array(z.string().min(1)).optional().catch(undefined),
1136
1145
  clientIntegrations: clientIntegrationsSchema.optional().catch(undefined),
1146
+ // A malformed profile policy must not fall back to legacy all-profile activation.
1147
+ asideProfileSync: asideProfileSyncSchema.optional().catch({ allProfiles: false }),
1137
1148
  providerContextCaps: z.record(z.string(), z.number().int().positive()).optional(),
1149
+ providerContextCapValues: z.record(z.string(), z.number().int().positive()).optional(),
1138
1150
  contextCapValue: z.number().int().positive().optional(),
1139
1151
  multiAgentGuidanceEnabled: z.boolean().optional(),
1140
1152
  // Invalid optional recovery config must not discard unrelated provider/account state.
@@ -3738,11 +3750,12 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements
3738
3750
  }
3739
3751
 
3740
3752
  /**
3741
- * Mirror `config.proxy` into HTTP(S)_PROXY env vars so Bun's native fetch routes every outbound
3742
- * provider call through the proxy no per-callsite changes (verified: Bun honors these plus
3743
- * NO_PROXY). User-set env vars always win; localhost/127.0.0.1 are appended to NO_PROXY so the
3744
- * CLI's own health checks and running-proxy API calls stay direct. Call once per process entry
3745
- * that makes outbound provider requests (server start, catalog sync).
3753
+ * Mirror `config.proxy` into HTTP(S)_PROXY env vars. Bun fetch consumes them natively; transports
3754
+ * such as the ChatGPT upstream WebSocket select the same environment explicitly. User-set HTTP(S)_PROXY
3755
+ * variables win; config fills missing scheme proxies, which take precedence over ALL_PROXY for WS.
3756
+ * localhost/127.0.0.1 are appended to NO_PROXY so the CLI's own health checks and
3757
+ * running-proxy API calls stay direct. Call once per process entry that makes outbound provider
3758
+ * requests (server start, catalog sync).
3746
3759
  */
3747
3760
  export function applyProxyEnv(config: OcxConfig): void {
3748
3761
  applyProxyEnvWith(config);