From 06134ef32853bd26667917db25a115d85965e864 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Fri, 18 Sep 2026 05:21:42 +0900 Subject: [PATCH 1/3] fix(catalog): preserve unknown live multi-agent pins Switching the catalog multiAgentMode to default cleared multi_agent_version on any entry missing from the bundled snapshot, erasing legitimate pins on preserved live/native rows. The pristine installed-catalog backup is now authoritative for the rows it contains, and preserved rows outside it keep the pin they already carry. --- src/codex/catalog/build-entries.ts | 7 +++- src/codex/catalog/parsing.ts | 38 ++++++++++++++++++- src/codex/catalog/retained-sync.ts | 3 ++ src/codex/convergence.ts | 2 + tests/codex-integration/codex-v2-gate.test.ts | 38 +++++++++++++++++++ tests/fixtures/file-size-baseline.json | 2 +- 6 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/codex/catalog/build-entries.ts b/src/codex/catalog/build-entries.ts index 2372c599e3..884b9e46a5 100644 --- a/src/codex/catalog/build-entries.ts +++ b/src/codex/catalog/build-entries.ts @@ -494,6 +494,8 @@ export interface ObservedCatalogMergeInput { readonly openaiContextCap?: NativeContextLimitsInput; /** Exact display-only labels for bare native OpenAI models. */ readonly nativeDisplayNames?: Readonly>; + /** Pristine installed-catalog multi-agent pins; see MultiAgentModeOptions.nativeDefaults. */ + readonly nativeMultiAgentDefaults?: ReadonlyMap; } /** @@ -529,6 +531,7 @@ export function mergeCatalogEntriesFromObservedState({ policy, openaiContextCap, nativeDisplayNames, + nativeMultiAgentDefaults, }: ObservedCatalogMergeInput): RawEntry[] { // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at // the observed-core boundary so callers can safely retain evidence objects or repeat the merge. @@ -886,7 +889,7 @@ export function mergeCatalogEntriesFromObservedState({ applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs), multiAgentMode, multiAgentV2Enabled, - { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection }, + { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults }, ); applyFullModelPickerOrder(versionedEntries, modelPickerOrder); for (const entry of versionedEntries) { @@ -937,6 +940,7 @@ export function mergeCatalogEntriesForSync( ), openaiContextCap?: NativeContextLimitsInput, keepNativeChatGptOnV1 = false, + nativeMultiAgentDefaults?: ReadonlyMap, ): RawEntry[] { // Retained for source compatibility with the original helper contract. Raw provider ids must // not suppress same-named native rows; actual admitted combo entries own that decision now. @@ -973,6 +977,7 @@ export function mergeCatalogEntriesForSync( accountBoundEntries, suppressedBareNativeSlugs, openaiContextCap, + nativeMultiAgentDefaults, policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, warningPolicy: "emit", diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index e1629a0ed5..2dcb5ad2ed 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -627,6 +627,14 @@ export interface MultiAgentModeOptions { * so they can still spawn Grok/Claude — ChatGPT encrypts v2 NEW_TASK bodies. */ keepNativeChatGptOnV1?: boolean; + /** + * Pristine installed-catalog pins keyed by bare native slug. When provided, the + * backup — not the bundled snapshot — is authoritative for the rows it contains, + * and a preserved live/native row outside it keeps the pin it already carries: + * an absent baseline entry cannot distinguish a stale forced stamp from a + * legitimate user- or provider-preserved pin, so the non-destructive read wins. + */ + nativeDefaults?: ReadonlyMap; } /** Catalog rows that run on the ChatGPT backend (encrypt v2 child tasks). */ @@ -707,13 +715,25 @@ export function applyMultiAgentMode( && hasNativeOpenAiCapabilityMetadata(routedNativeSlug) ? routedNativeSlug : undefined; + const nativeLookupSlug = trustedAccountBoundNativeCatalogSlug(entry) ?? slug; + const hasNativeDefault = !nativeAlias + && codexForwardCapabilityAlias === undefined + && options.nativeDefaults?.has(nativeLookupSlug) === true; const upstreamPin = nativeAlias ? nativeMultiAgentVersion(slug) : codexForwardCapabilityAlias ? nativeMultiAgentVersion(codexForwardCapabilityAlias) - : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version; + : hasNativeDefault + ? options.nativeDefaults?.get(nativeLookupSlug) + : UPSTREAM_NATIVE_ENTRIES.get(nativeLookupSlug)?.multi_agent_version; if (typeof upstreamPin === "string") { entry.multi_agent_version = upstreamPin; + } else if (options.nativeDefaults !== undefined + && !nativeAlias + && codexForwardCapabilityAlias === undefined + && !hasNativeDefault + && typeof entry.multi_agent_version === "string") { + continue; } else if (v2FeatureEnabled) { entry.multi_agent_version = "v2"; } else { @@ -888,3 +908,19 @@ export function readNativeBaseline(catalogPath: string): Map { } return out; } + +/** + * Extract the pristine baseline's per-slug multi-agent pins. A bare native row that + * carried no pin maps to null so "baseline says unpinned" stays distinguishable + * from "baseline never contained this row". + */ +export function nativeMultiAgentDefaults( + models: readonly Readonly>[] | null | undefined, +): Map { + const out = new Map(); + for (const entry of models ?? []) { + if (typeof entry.slug !== "string" || entry.slug.includes("/")) continue; + out.set(entry.slug, typeof entry.multi_agent_version === "string" ? entry.multi_agent_version : null); + } + return out; +} diff --git a/src/codex/catalog/retained-sync.ts b/src/codex/catalog/retained-sync.ts index 8269d0fccd..46e81a50e3 100644 --- a/src/codex/catalog/retained-sync.ts +++ b/src/codex/catalog/retained-sync.ts @@ -33,6 +33,7 @@ import { readCodexCatalogPath, readCodexCatalogPathForHome, readNativeBaseline, + nativeMultiAgentDefaults, } from "./parsing"; import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing"; import { @@ -443,6 +444,7 @@ function writeRetainedCatalogSync({ // like `gpt-5.5`; those must not delete the native OpenAI/Codex base row. const baselineCatalog = readCatalogBackup(catalogPath); const baseline = readNativeBaseline(catalogPath); + const nativePinBaseline = nativeMultiAgentDefaults(baselineCatalog?.models); const gatheredProviderNames = new Set( Object.entries(config.providers ?? {}) .filter(([, prov]) => prov.disabled !== true) @@ -514,6 +516,7 @@ function writeRetainedCatalogSync({ suppressedBareNativeSlugs, openaiContextCap, nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames, + nativeMultiAgentDefaults: nativePinBaseline, policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs], diff --git a/src/codex/convergence.ts b/src/codex/convergence.ts index a8bb4b611b..806ef48115 100644 --- a/src/codex/convergence.ts +++ b/src/codex/convergence.ts @@ -36,6 +36,7 @@ import { catalogHasRoutedEntries, findSupportedNativeTemplate, legacyCatalogBackupPath, + nativeMultiAgentDefaults, parseCatalogJson, type RawCatalog, type RawEntry, @@ -373,6 +374,7 @@ function prepareCatalog( suppressedBareNativeSlugs, openaiContextCap, nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames, + nativeMultiAgentDefaults: nativeMultiAgentDefaults(baselineCatalogModels), policy: { ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY, nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs], diff --git a/tests/codex-integration/codex-v2-gate.test.ts b/tests/codex-integration/codex-v2-gate.test.ts index 7b9ce035ae..9199be11b9 100644 --- a/tests/codex-integration/codex-v2-gate.test.ts +++ b/tests/codex-integration/codex-v2-gate.test.ts @@ -2065,5 +2065,43 @@ describe("3-state multi-agent mode", () => { // gpt-5.5 has no upstream pin — cleared (codex flag decides) expect(native.multi_agent_version).toBeUndefined(); }); + + test("mode default prefers pristine-baseline pins over the bundled snapshot", () => { + // The installed pristine backup is authoritative for the rows it contains: a + // baseline pin wins even when the bundled snapshot pins a different value, and + // a baseline row with no pin still gets stale forced-stamp cleanup. + const diskSol = { ...template(), slug: "gpt-5.6-sol", display_name: "GPT-5.6 Sol", multi_agent_version: "v2" }; + const diskLuna = { ...template(), slug: "gpt-5.6-luna", display_name: "GPT-5.6 Luna", multi_agent_version: "v2" }; + const diskNative = { ...template(), slug: "gpt-5.5", display_name: "gpt-5.5", multi_agent_version: "v2" }; + const merged = mergeCatalogEntriesForSync( + [diskSol as never, diskLuna as never, diskNative as never], + [], new Map(), [], false, new Set(), null, new Set(), new Set(), "default", + new Set(), false, true, [], new Set(), new Set(), undefined, false, + new Map([ + ["gpt-5.6-sol", "v1"], + ["gpt-5.6-luna", "v1"], + ["gpt-5.5", null], + ]), + ); + // Baseline says v1 — applied instead of the bundled snapshot's v2 pin. + expect(merged.find(e => e.slug === "gpt-5.6-sol")?.multi_agent_version).toBe("v1"); + expect(merged.find(e => e.slug === "gpt-5.6-luna")?.multi_agent_version).toBe("v1"); + // Baseline contains the row with no pin — stale forced stamp is cleared. + expect(merged.find(e => e.slug === "gpt-5.5")?.multi_agent_version).toBeUndefined(); + }); + + test("mode default preserves pins on live native rows outside the pristine baseline", () => { + // A preserved on-disk row the pristine backup never contained may carry a + // user- or provider-preserved pin newer than our bundled snapshot. It was not + // stamped by us, so default mode must not delete it. + const liveNative = { ...template(), slug: "custom-native", display_name: "Custom Native", multi_agent_version: "v2" }; + const merged = mergeCatalogEntriesForSync( + [liveNative as never], + [], new Map(), [], false, new Set(), null, new Set(), new Set(), "default", + new Set(), false, true, [], new Set(), new Set(), undefined, false, + new Map([["gpt-5.6-sol", "v2"]]), + ); + expect(merged.find(e => e.slug === "custom-native")?.multi_agent_version).toBe("v2"); + }); }); import { ManagementRequest as Request } from "../helpers/management-auth"; diff --git a/tests/fixtures/file-size-baseline.json b/tests/fixtures/file-size-baseline.json index 619f6c8ff7..60e550ff09 100644 --- a/tests/fixtures/file-size-baseline.json +++ b/tests/fixtures/file-size-baseline.json @@ -40,7 +40,7 @@ "tests/codex-integration/codex-reset-credit-recovery.test.ts": 2135, "tests/codex-integration/codex-routing.test.ts": 3443, "tests/codex-integration/codex-shim.test.ts": 2388, - "tests/codex-integration/codex-v2-gate.test.ts": 2069, + "tests/codex-integration/codex-v2-gate.test.ts": 2107, "tests/providers/cursor/cursor-blob.test.ts": 3657, "tests/providers/kiro/kiro-adapter.test.ts": 2050, "tests/providers/kiro/kiro-stream.test.ts": 2258, From 99ef32247f31b46d91597952b0f1669764ef9901 Mon Sep 17 00:00:00 2001 From: lidge-jun Date: Fri, 18 Sep 2026 09:56:56 +0900 Subject: [PATCH 2/3] fix(catalog): keep unknown-pin preservation to native rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nativeMultiAgentDefaults builds its baseline from bare native slugs and skips anything containing a slash, so a routed row can never appear in it. The new preservation branch read that guaranteed absence as evidence that the pin might be user- or provider-preserved, and every routed row carrying a stale forced multi_agent_version kept it. structure/subagents.md documents the opposite: default mode clears stale values and routed normalization deletes the key. Require the entry to be native — a bare slug, or a trusted account-bound native slug — before the absence of a baseline entry is allowed to mean anything, and cover a routed row in the same test. --- src/codex/catalog/parsing.ts | 9 ++++++++- tests/codex-integration/codex-v2-gate.test.ts | 6 +++++- tests/fixtures/file-size-baseline.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index 2dcb5ad2ed..f55f5808db 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -715,7 +715,13 @@ export function applyMultiAgentMode( && hasNativeOpenAiCapabilityMetadata(routedNativeSlug) ? routedNativeSlug : undefined; - const nativeLookupSlug = trustedAccountBoundNativeCatalogSlug(entry) ?? slug; + const accountBoundNativeSlug = trustedAccountBoundNativeCatalogSlug(entry); + const nativeLookupSlug = accountBoundNativeSlug ?? slug; + // The baseline is built from bare native slugs only, so "absent from the + // baseline" is not evidence about a routed row — it is guaranteed. Only a + // native row can carry a pin the baseline legitimately failed to mention; + // a routed row keeps the documented default-mode normalization. + const isNativeCatalogEntry = accountBoundNativeSlug !== undefined || !slug.includes("/"); const hasNativeDefault = !nativeAlias && codexForwardCapabilityAlias === undefined && options.nativeDefaults?.has(nativeLookupSlug) === true; @@ -731,6 +737,7 @@ export function applyMultiAgentMode( } else if (options.nativeDefaults !== undefined && !nativeAlias && codexForwardCapabilityAlias === undefined + && isNativeCatalogEntry && !hasNativeDefault && typeof entry.multi_agent_version === "string") { continue; diff --git a/tests/codex-integration/codex-v2-gate.test.ts b/tests/codex-integration/codex-v2-gate.test.ts index 9199be11b9..9cd2b930c6 100644 --- a/tests/codex-integration/codex-v2-gate.test.ts +++ b/tests/codex-integration/codex-v2-gate.test.ts @@ -2095,13 +2095,17 @@ describe("3-state multi-agent mode", () => { // user- or provider-preserved pin newer than our bundled snapshot. It was not // stamped by us, so default mode must not delete it. const liveNative = { ...template(), slug: "custom-native", display_name: "Custom Native", multi_agent_version: "v2" }; + // A routed row is never in the bare-native baseline, so its absence proves + // nothing; default mode still clears its stale pin. + const staleRouted = { ...template(), slug: "provider/model", display_name: "Routed", multi_agent_version: "v1" }; const merged = mergeCatalogEntriesForSync( - [liveNative as never], + [liveNative as never, staleRouted as never], [], new Map(), [], false, new Set(), null, new Set(), new Set(), "default", new Set(), false, true, [], new Set(), new Set(), undefined, false, new Map([["gpt-5.6-sol", "v2"]]), ); expect(merged.find(e => e.slug === "custom-native")?.multi_agent_version).toBe("v2"); + expect(merged.find(e => e.slug === "provider/model")?.multi_agent_version).toBeUndefined(); }); }); import { ManagementRequest as Request } from "../helpers/management-auth"; diff --git a/tests/fixtures/file-size-baseline.json b/tests/fixtures/file-size-baseline.json index 60e550ff09..ae5f085f87 100644 --- a/tests/fixtures/file-size-baseline.json +++ b/tests/fixtures/file-size-baseline.json @@ -40,7 +40,7 @@ "tests/codex-integration/codex-reset-credit-recovery.test.ts": 2135, "tests/codex-integration/codex-routing.test.ts": 3443, "tests/codex-integration/codex-shim.test.ts": 2388, - "tests/codex-integration/codex-v2-gate.test.ts": 2107, + "tests/codex-integration/codex-v2-gate.test.ts": 2111, "tests/providers/cursor/cursor-blob.test.ts": 3657, "tests/providers/kiro/kiro-adapter.test.ts": 2050, "tests/providers/kiro/kiro-stream.test.ts": 2258, From 41f6600471174700268fa3e46f5a31249a5c910c Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Fri, 18 Sep 2026 10:40:51 +0900 Subject: [PATCH 3/3] test(catalog): cover account-bound baseline pin keys --- tests/codex-integration/codex-v2-gate.test.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/codex-integration/codex-v2-gate.test.ts b/tests/codex-integration/codex-v2-gate.test.ts index 9cd2b930c6..a7fb5cd818 100644 --- a/tests/codex-integration/codex-v2-gate.test.ts +++ b/tests/codex-integration/codex-v2-gate.test.ts @@ -20,6 +20,7 @@ import { buildCatalogEntriesFromObservedState, mergeCatalogEntriesFromObservedState, } from "../../src/codex/catalog/sync"; +import { nativeMultiAgentDefaults } from "../../src/codex/catalog/parsing"; import { getAgentsEnabled, getAgentsMaxDepth, @@ -2107,5 +2108,58 @@ describe("3-state multi-agent mode", () => { expect(merged.find(e => e.slug === "custom-native")?.multi_agent_version).toBe("v2"); expect(merged.find(e => e.slug === "provider/model")?.multi_agent_version).toBeUndefined(); }); + + test("mode default keys baseline pins by trusted account-bound slugs only", () => { + // hasNativeDefault resolves the lookup slug through + // trustedAccountBoundNativeCatalogSlug, so an account-bound clone tracks its + // bound native's pristine pin: the backup's "v1" beats both the bundled + // snapshot's "v2" and a stale stamp on the clone, and a baseline row with no + // pin still clears the clone's stale stamp. + const boundSol = { + ...template(), + slug: "team/gpt-5.6-sol", + display_name: "team / GPT-5.6 Sol", + opencodex_catalog_kind: CODEX_ACCOUNT_BOUND_CATALOG_KIND, + multi_agent_version: "v2", + }; + const boundNative = { + ...template(), + slug: "team/gpt-5.5", + display_name: "team / gpt-5.5", + opencodex_catalog_kind: CODEX_ACCOUNT_BOUND_CATALOG_KIND, + multi_agent_version: "v2", + }; + // An untrusted slashed row must not key the baseline by its post-slash part: + // "external/gpt-5.6-sol" is not the native "gpt-5.6-sol" row, so its preserved + // pin survives instead of being rewritten to the baseline's "v1". + const foreignRouted = { + ...template(), + slug: "external/gpt-5.6-sol", + display_name: "External Sol", + multi_agent_version: "v2", + }; + const merged = mergeCatalogEntriesForSync( + [foreignRouted as never], [], new Map(), [], false, + new Set(), null, new Set(), new Set(), "default", + new Set(), false, true, [boundSol as never, boundNative as never], + new Set(), new Set(), undefined, false, + new Map([["gpt-5.6-sol", "v1"], ["gpt-5.5", null]]), + ); + expect(merged.find(e => e.slug === "team/gpt-5.6-sol")?.multi_agent_version).toBe("v1"); + expect(merged.find(e => e.slug === "team/gpt-5.5")?.multi_agent_version).toBeUndefined(); + expect(merged.find(e => e.slug === "external/gpt-5.6-sol")?.multi_agent_version).toBe("v2"); + + // The baseline extractor itself never indexes slashed rows, so account-bound + // or routed rows inside a backup cannot alias a bare native slug. + const defaults = nativeMultiAgentDefaults([ + { slug: "gpt-5.6-sol", multi_agent_version: "v1" }, + { slug: "team/gpt-5.6-sol", multi_agent_version: "v2" }, + { slug: "gpt-5.5" }, + ]); + expect(defaults.get("gpt-5.6-sol")).toBe("v1"); + expect(defaults.has("team/gpt-5.6-sol")).toBe(false); + expect(defaults.has("gpt-5.5")).toBe(true); + expect(defaults.get("gpt-5.5")).toBeNull(); + }); }); import { ManagementRequest as Request } from "../helpers/management-auth";