Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/codex/catalog/build-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,8 @@ export interface ObservedCatalogMergeInput {
readonly openaiContextCap?: NativeContextLimitsInput;
/** Exact display-only labels for bare native OpenAI models. */
readonly nativeDisplayNames?: Readonly<Record<string, string>>;
/** Pristine installed-catalog multi-agent pins; see MultiAgentModeOptions.nativeDefaults. */
readonly nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>;
}

/**
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -937,6 +940,7 @@ export function mergeCatalogEntriesForSync(
),
openaiContextCap?: NativeContextLimitsInput,
keepNativeChatGptOnV1 = false,
nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>,
): 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.
Expand Down Expand Up @@ -973,6 +977,7 @@ export function mergeCatalogEntriesForSync(
accountBoundEntries,
suppressedBareNativeSlugs,
openaiContextCap,
nativeMultiAgentDefaults,
policy: {
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
warningPolicy: "emit",
Expand Down
45 changes: 44 additions & 1 deletion src/codex/catalog/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,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<string, string | null>;
}

/** Catalog rows that run on the ChatGPT backend (encrypt v2 child tasks). */
Expand Down Expand Up @@ -738,13 +746,32 @@ export function applyMultiAgentMode(
&& hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
? routedNativeSlug
: undefined;
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;
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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (typeof upstreamPin === "string") {
entry.multi_agent_version = upstreamPin;
} else if (options.nativeDefaults !== undefined
&& !nativeAlias
&& codexForwardCapabilityAlias === undefined
&& isNativeCatalogEntry
&& !hasNativeDefault
&& typeof entry.multi_agent_version === "string") {
continue;
} else if (v2FeatureEnabled) {
entry.multi_agent_version = "v2";
} else {
Expand Down Expand Up @@ -919,3 +946,19 @@ export function readNativeBaseline(catalogPath: string): Map<string, number> {
}
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<Record<string, unknown>>[] | null | undefined,
): Map<string, string | null> {
const out = new Map<string, string | null>();
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;
}
3 changes: 3 additions & 0 deletions src/codex/catalog/retained-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
readCodexCatalogPath,
readCodexCatalogPathForHome,
readNativeBaseline,
nativeMultiAgentDefaults,
} from "./parsing";
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
import {
Expand Down Expand Up @@ -452,6 +453,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)
Expand Down Expand Up @@ -523,6 +525,7 @@ function writeRetainedCatalogSync({
suppressedBareNativeSlugs,
openaiContextCap,
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
nativeMultiAgentDefaults: nativePinBaseline,
policy: {
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
Expand Down
2 changes: 2 additions & 0 deletions src/codex/convergence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
catalogHasRoutedEntries,
findSupportedNativeTemplate,
legacyCatalogBackupPath,
nativeMultiAgentDefaults,
parseCatalogJson,
type RawCatalog,
type RawEntry,
Expand Down Expand Up @@ -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],
Expand Down
96 changes: 96 additions & 0 deletions tests/codex-integration/codex-v2-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
buildCatalogEntriesFromObservedState,
mergeCatalogEntriesFromObservedState,
} from "../../src/codex/catalog/sync";
import { nativeMultiAgentDefaults } from "../../src/codex/catalog/parsing";
import {
getAgentsEnabled,
getAgentsMaxDepth,
Expand Down Expand Up @@ -2065,5 +2066,100 @@ 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<string, string | null>([
["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" };
// 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, 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();
});

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<string, string | null>([["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";
2 changes: 1 addition & 1 deletion tests/fixtures/file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,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": 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,
Expand Down
Loading