diff --git a/src/codex/history-job.ts b/src/codex/history-job.ts index 1f4e174c51..5cfb1e1f4a 100644 --- a/src/codex/history-job.ts +++ b/src/codex/history-job.ts @@ -23,7 +23,7 @@ import type { CodexHistoryWorkerOperation, HistoryWorkerResult, } from "./history-worker"; -import { historyBackupPathFor } from "./history-provider"; +import { resolveExistingHistoryBackupPath } from "./history-provider"; import type { CodexHistoryFailureReason, CodexHistoryVerifiedNoopProof } from "./history-provider"; import { getCodexHome, resolveCodexStateDbPath } from "./paths"; @@ -32,7 +32,7 @@ import { getCodexHome, resolveCodexStateDbPath } from "./paths"; * * The SQLite root can differ from CODEX_HOME and both environment/config inputs * can change between invocations. The parent resolves one exact target and hands - * those canonical paths to the Worker rather than asking the Worker to infer a + * those resolved paths to the Worker rather than asking the Worker to infer a * possibly different environment. */ export function resolveCodexHistoryJobTarget(): { @@ -45,10 +45,10 @@ export function resolveCodexHistoryJobTarget(): { return { canonicalCodexHome: home, canonicalStateDbPath: stateDb, - // Derived by the provider's own rule rather than guessed: the manifest lives - // in the config directory under a hash of the state database, so a - // hand-built path would address a different file entirely. - canonicalBackupPath: historyBackupPathFor(stateDb), + // Resolve through the provider's canonical-first compatibility rule. Passing + // only the newly normalized name would hide a pre-#4442 Windows manifest from + // the Worker and make an upgrade look like an empty backup. + canonicalBackupPath: resolveExistingHistoryBackupPath(stateDb), }; } diff --git a/structure/catalog.md b/structure/catalog.md index 8cce6268b0..12a064690b 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -309,7 +309,7 @@ Provider `showThinkingSummary` is a Responses request default; it does not rewri ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. History Worker targets preserve the canonical-first legacy-manifest fallback when they cross the Worker boundary. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. diff --git a/structure/codex-home.md b/structure/codex-home.md index 1f1edfd7da..4d31d594e2 100644 --- a/structure/codex-home.md +++ b/structure/codex-home.md @@ -248,7 +248,7 @@ Plan-based automatic exclusions leave native credential files untouched and pres ## Paginated history writer boundary -`src/codex/history-provider.ts` rejects provider-history changes with `history_paginated_requires_native_writer` when a target begins with an ordinal-bearing record or declares `history_mode=paginated`. Apply, manifest-backed restore, and explicit legacy recovery preflight all selected targets before changing database rows or manifests. The append boundary checks again. Codex owns ordinal allocation and the live projection cursor; reading the last ordinal and appending N+1 is not safe concurrent coordination. Legacy unnumbered rollouts retain their existing behavior. This guard prevents the observed stable-format corruption; it does not implement native-writer integration or guarantee a concurrent legacy-to-paginated conversion is excluded. +`src/codex/history-provider.ts` rejects provider-history changes with `history_paginated_requires_native_writer` when a target begins with an ordinal-bearing record or declares `history_mode=paginated`. Apply, manifest-backed restore, and explicit legacy recovery preflight all selected targets before changing database rows or manifests. The append boundary checks again. Codex owns ordinal allocation and the live projection cursor; reading the last ordinal and appending N+1 is not safe concurrent coordination. Legacy unnumbered rollouts retain their existing behavior. History Worker targets resolve the canonical manifest first and an existing pre-normalization Windows filename second, so passing an explicit target cannot bypass upgrade recovery. This guard prevents the observed stable-format corruption; it does not implement native-writer integration or guarantee a concurrent legacy-to-paginated conversion is excluded. Injection preflights affected history using the normalized config candidate before writing config/profile/journal, then checks again after the complete artifact write. Native restore also rechecks after successful journal restoration or fallback removal, while exact config/profile/journal preimages and any coordinated remove transaction remain available for compensation. Detected migration restores all three preimages before returning a structured refusal, including on legacy-uncoordinated homes. A failed config restore stops catalog/history work; coordinated restore rolls back its published remove transition. Legacy first-line provider patches are bound to the validated file identity before and after writing. These compensating checks do not provide a native-writer lock or authorize external ordinal allocation. diff --git a/structure/config.md b/structure/config.md index f799ebd2c5..d33fc67528 100644 --- a/structure/config.md +++ b/structure/config.md @@ -141,7 +141,8 @@ that prefix hashed to a different backup filename before the normalization, so t (`history-provider.ts` for mutation, `native-residue.ts` for observation) fall back to the legacy filename when no canonical manifest exists. When both names exist the canonical manifest wins and the legacy file is left in place; a conflict is never resolved by silently replacing -either file. +either file. History Worker job targets use that same canonical-first lookup rather than passing a +canonical-only filename that would bypass the provider's legacy fallback. `history-provider.ts` remains the strict mutation owner and maps shared validation failures to its restore/no-op integrity states. `native-residue.ts` remains a read-only observer and maps the same diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index c445858e6b..a8e1966e69 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -573,7 +573,7 @@ The provider editor field policy exposes `showThinkingSummary` as a boolean prov ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. History Worker targets preserve the canonical-first legacy-manifest fallback when they cross the Worker boundary. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. Codex account DTOs and cards expose the routing-plan exclusion separately from credential health; the [plan exclusion contract](providers/openai-tiers.md#automatic-pool-plan-exclusions) also governs CLI projection. Private pool credential metadata follows the [quota-history publication identity contract](providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index 17e54e3664..490286a1e0 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -344,7 +344,7 @@ Provider configuration documents distinguish actual summaries from raw reasoning ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. History Worker targets preserve the canonical-first legacy-manifest fallback when they cross the Worker boundary. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Private pool credential metadata follows the [quota-history publication identity contract](../providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it. diff --git a/structure/providers/openai-tiers.md b/structure/providers/openai-tiers.md index 527aacba6b..92844dddeb 100644 --- a/structure/providers/openai-tiers.md +++ b/structure/providers/openai-tiers.md @@ -450,7 +450,7 @@ Listener startup diagnostics follow [the runtime lifecycle contract](../runtime. `src/codex/auth-api.ts` projects `selectionExcludedReason: "plan_excluded"` and `selectionExcludedPlan` from the routing config, even when a newer display-only WHAM plan could not be persisted. The dashboard and account CLI show the policy reason separately from credential health; renewal clears the derived fields. The automatic next-session action and badge are omitted for excluded rows. ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. History Worker targets preserve the canonical-first legacy-manifest fallback when they cross the Worker boundary. See the [history writer contract](../codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior. diff --git a/structure/runtime.md b/structure/runtime.md index a09d007d7b..1751fd380b 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -314,7 +314,7 @@ Responses route normalization resolves provider summary defaults from the origin ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. History Worker targets preserve the canonical-first legacy-manifest fallback when they cross the Worker boundary. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. diff --git a/structure/subagents.md b/structure/subagents.md index 69047b076b..42ce04f353 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -343,7 +343,7 @@ Final-route summary visibility is recomputed after fallback from the original Re ## Paginated history writer boundary -`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. +`src/codex/history-provider.ts` refuses external writes to paginated or migration-capable history. `src/codex/inject.ts` checks affected rows and manifest-owned restore targets before and after config/profile/journal changes, including successful journal and fallback restores, and compensates detected migration. Failed config restore stops later catalog/history work and rolls back a coordinated remove transition. History Worker targets preserve the canonical-first legacy-manifest fallback when they cross the Worker boundary. See the [history writer contract](codex-home.md#paginated-history-writer-boundary) for guarantees and concurrent-writer limits. Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback and preserved affinity. diff --git a/tests/codex-integration/codex-sqlite-home.test.ts b/tests/codex-integration/codex-sqlite-home.test.ts index f8f7c088bb..7fcfd94c88 100644 --- a/tests/codex-integration/codex-sqlite-home.test.ts +++ b/tests/codex-integration/codex-sqlite-home.test.ts @@ -4,12 +4,13 @@ import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { resolveCodexHistoryJobTarget } from "../../src/codex/history-job"; -import { historyBackupPathFor } from "../../src/codex/history-provider"; +import { legacyHistoryBackupPathFor, resolveExistingHistoryBackupPath } from "../../src/codex/history-provider"; import { resolveCodexLogsDbPath, resolveCodexSqliteHome, resolveCodexStateDbPath } from "../../src/codex/paths"; import { removeTreeWithRetry } from "../helpers/remove-tree"; const originalCodexHome = process.env.CODEX_HOME; const originalSqliteHome = process.env.CODEX_SQLITE_HOME; +const originalOpenCodexHome = process.env.OPENCODEX_HOME; const roots: string[] = []; afterEach(() => { @@ -17,6 +18,8 @@ afterEach(() => { else process.env.CODEX_HOME = originalCodexHome; if (originalSqliteHome === undefined) delete process.env.CODEX_SQLITE_HOME; else process.env.CODEX_SQLITE_HOME = originalSqliteHome; + if (originalOpenCodexHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = originalOpenCodexHome; for (const root of roots.splice(0)) removeTreeWithRetry(root); }); @@ -108,11 +111,31 @@ describe("Codex SQLite home resolution", () => { const configured = resolveCodexHistoryJobTarget(); expect(configured.canonicalStateDbPath).toBe(join(configSqliteHome, "state_5.sqlite")); - expect(configured.canonicalBackupPath).toBe(historyBackupPathFor(configured.canonicalStateDbPath)); + expect(configured.canonicalBackupPath).toBe(resolveExistingHistoryBackupPath(configured.canonicalStateDbPath)); unlinkSync(join(codexHome, "config.toml")); const fromEnv = resolveCodexHistoryJobTarget(); expect(fromEnv.canonicalStateDbPath).toBe(join(envSqliteHome, "state_5.sqlite")); - expect(fromEnv.canonicalBackupPath).toBe(historyBackupPathFor(fromEnv.canonicalStateDbPath)); + expect(fromEnv.canonicalBackupPath).toBe(resolveExistingHistoryBackupPath(fromEnv.canonicalStateDbPath)); + }); + + test.skipIf(process.platform !== "win32")("history jobs preserve a pre-normalization extended-path manifest", () => { + const root = mkdtempSync(join(tmpdir(), "ocx-sqlite-home-legacy-")); + roots.push(root); + const codexHome = join(root, "codex"); + const configHome = join(root, "opencodex"); + const sqliteHome = join(root, "sqlite"); + mkdirSync(codexHome); + mkdirSync(configHome); + mkdirSync(sqliteHome); + process.env.CODEX_HOME = codexHome; + process.env.OPENCODEX_HOME = configHome; + process.env.CODEX_SQLITE_HOME = `\\\\?\\${sqliteHome}`; + + const stateDb = resolveCodexStateDbPath({ codexHome }); + const legacyBackup = legacyHistoryBackupPathFor(stateDb); + writeFileSync(legacyBackup, "{}\n"); + + expect(resolveCodexHistoryJobTarget().canonicalBackupPath).toBe(legacyBackup); }); });