Skip to content

[Bug]: Codex shadow home fails to materialize (and the instance is disabled) when SQLite creates -shm/-wal in the shadow home #20261

Closed

Description

@Mans230

Before submitting

  • I searched existing issues and did not find a duplicate.
  • I included enough detail to reproduce or investigate the problem.

Scope. This is a different component from #19556. #19556 is about the offered codex update command; this one is the Codex shadow-home materializer (CodexHomeLayout.ts). It needs no codex update at all and reproduces with the repo's own test harness. Screenshot of the reported state: the codex-2 provider card goes to Disabled with Driver 'codex' failed to create instance: Cannot create Codex shadow home entry 'goals_1.sqlite-shm' ….

Area

apps/server

Steps to reproduce

Any Codex provider instance with a shadowHomePath (i.e. a second ChatGPT account) eventually stops starting.

  1. Configure a second Codex instance with a shadow home, e.g. config.shadowHomePath: ~/.codex-t3/codex-2 (shared home ~/.codex).
  2. Start it once and let Codex run. SQLite derives its sidecar names from the database path it was handed, so goals_1.sqlite is opened through the shared symlink but goals_1.sqlite-shm / goals_1.sqlite-wal are created in the shadow home as real files.
  3. The main instance runs too, so the same sidecar names also appear in the shared home.
  4. Restart T3 Code. materializeCodexShadowHome now tries to link those names and fails.

Reproduces deterministically in isolation (see the verification section): create the shared home with goals_1.sqlite + goals_1.sqlite-shm + goals_1.sqlite-wal, drop real goals_1.sqlite-shm / goals_1.sqlite-wal files into the shadow home, call materializeCodexShadowHome.

Expected behavior

A local -shm / -wal in the shadow home should be replaced by the shared link, exactly like the local .sqlite-maintenance.lock and mcp-oauth-locks that are already handled — those entries exist precisely because "Codex creates these runtime locks itself when it starts in a shadow home before the shared home has one". SQLite's per-database sidecars are the same case.

Actual behavior

The instance fails to create and is shown as Disabled:

Driver 'codex' failed to create instance: Cannot create Codex shadow home entry
'goals_1.sqlite-shm' because
'C:\Users\Administrator\.codex-t3\codex-2\goals_1.sqlite-shm' already exists and is not a symlink.

Impact

  • An existing, working provider instance stops working entirely until the shadow home is manually repaired; only a shadow-home user (second account) is affected, and they are affected permanently once SQLite writes a sidecar.
  • The same gap also silently splits shared state: at the time of writing, the shadow home had state_5.sqlite-wal at 0 bytes while the shared home had 86,552 bytes of WAL frames for the same database, i.e. the shadow instance was reading the database without the shared WAL. Two homes owning two different -shm/-wal pairs for one database file also means the SQLite lock table is not shared between the instances.

Version or commit

  • T3 Code (Nightly) 0.1.1-nightly.20261016.3203 (ProductVersion 0.1.1.0, FileVersion 0.1.1-nightly.20261016.3203), the newest nightly at the time of writing. The same two-entry set is in the shipped bundle, so this is not a stale-build problem (see below).
  • Source checked: pingdotgg/t3code@cfafb4bb54d244e4612f5681aad3832ea64cb0fb.

Environment

  • Windows Server 2022 (10.0.20348, AMD64), T3 Code desktop nightly, local server.
  • Shared home C:\Users\Administrator\.codex; shadow home C:\Users\Administrator\.codex-t3\codex-2.
  • Instance config: "codex-2": { "driver": "codex", "displayName": "Codex 2", "enabled": true, "config": { "shadowHomePath": "~/.codex-t3/codex-2" } }. No binaryPath / setupMode — nothing custom.

Logs or stack traces

Shadow home listing at failure time (real files have no LinkType):

Name                    LinkType     Target
goals_1.sqlite          SymbolicLink C:\Users\Administrator\.codex\goals_1.sqlite
goals_1.sqlite-shm                  32768 bytes          <-- real file, conflict
goals_1.sqlite-wal                  0 bytes              <-- real file
memories_1.sqlite       SymbolicLink C:\Users\Administrator\.codex\memories_1.sqlite
memories_1.sqlite-shm               32768 bytes          <-- real file
memories_1.sqlite-wal               0 bytes              <-- real file
logs_2.sqlite-shm       SymbolicLink C:\Users\Administrator\.codex\logs_2.sqlite-shm
queue_1.sqlite-shm      SymbolicLink C:\Users\Administrator\.codex\queue_1.sqlite-shm
state_5.sqlite          SymbolicLink C:\Users\Administrator\.codex\state_5.sqlite
state_5.sqlite-shm                  32768 bytes          <-- real file
state_5.sqlite-wal                  0 bytes              <-- real file

Workaround

Close T3 Code, delete the shadow home's local (non-symlink) sidecars, then start T3 Code again so the materializer re-links them. Only -shm / -wal files are touched; the databases themselves are symlinks and are left alone. This is what was done here, and the instance starts after the next launch.

Root cause

apps/server/src/provider/Drivers/CodexHomeLayout.ts:

const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]);
const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp"]);
// Codex creates these runtime locks itself when it starts in a shadow home before the shared
// home has one. They guard shared state, so a local copy is replaced with the shared link.
const REPLACEABLE_SHARED_RUNTIME_ENTRIES = new Set(["mcp-oauth-locks", ".sqlite-maintenance.lock"]);

and in ensureSymlink:

if (state._tag === "NotSymlink") {
  if (!REPLACEABLE_SHARED_RUNTIME_ENTRIES.has(input.entryName)) {
    return yield* new CodexShadowHomeEntryConflictError({ /* … */ });
  }
  yield* input.fileSystem.remove(link, { recursive: true }).pipe(/* … */);
  return yield* createLink;
}

Anything not in that set hard-fails when a local file already occupies the name. The comment states the exact criterion for membership — "Codex creates these runtime locks itself when it starts in a shadow home" — and SQLite's per-database -shm / -wal sidecars satisfy it, but they are not in the set. The repo already tests the intended mechanism for the neighbouring case, CodexHomeLayout.test.ts:

it.effect("replaces a Codex-created local sqlite maintenance lock with the shared lock", …
  yield* writeTextFile(sharedLock, "");
  yield* writeTextFile(shadowLock, "");
  …
  yield* materializeCodexShadowHome(layout);
  expect(yield* fileSystem.readLink(shadowLock)).toBe(sharedLock);

That test is exactly this scenario for .sqlite-maintenance.lock. The reproduction below is the same shape for goals_1.sqlite-shm, and it throws.

Verification (this is not environment-specific)

1. The code under test is byte-identical to main. The module was copied verbatim from apps/server/src/provider/Drivers/CodexHomeLayout.ts at cfafb4b; SHA-256 54f7573c612fe0d6689a42ee6219fab28835078b4f8c728555c3839b33529e53 for both the repo copy and the raw main file.

2. The repro runs the repo's own test suite alongside. A standalone harness (npm effect@4.0.5, @effect/platform-node@4.0.5, @effect/vitest@4.0.5, vitest@5.0.5) aliases @t3tools/contracts to a minimal shim and points the other three imports at the real repo files. Result: 13/13 pass, including all 9 of the repo's existing CodexHomeLayout tests (so the harness is faithful) and 4 added ones.

3. The added test reproduces the reported failure with the exact message:

// 1) first materialization: shared home has only the database file
yield* fileSystem.writeFileString(path.join(shared, "goals_1.sqlite"), "db");
yield* materializeCodexShadowHome(layout);

// 2) Codex runs with CODEX_HOME=<shadow>: SQLite creates the sidecars locally
yield* fileSystem.writeFileString(path.join(shadow, "goals_1.sqlite-shm"), "shm");
yield* fileSystem.writeFileString(path.join(shadow, "goals_1.sqlite-wal"), "wal");

// 3) the main instance creates the same names in the shared home
yield* fileSystem.writeFileString(path.join(shared, "goals_1.sqlite-shm"), "shm");
yield* fileSystem.writeFileString(path.join(shared, "goals_1.sqlite-wal"), "wal");

// 4) next provider start
const error = yield* Effect.flip(materializeCodexShadowHome(layout));
// _tag       === "CodexShadowHomeEntryConflictError"
// entryName  === "goals_1.sqlite-shm"
// message    === `Cannot create Codex shadow home entry 'goals_1.sqlite-shm' because
//                 '…\goals_1.sqlite-shm' already exists and is not a symlink.`

4. The shipped build contains the same code. From the installed server.asar of 0.1.1-nightly.20261016.3203:

const REPLACEABLE_SHARED_RUNTIME_ENTRIES = /* @__PURE__ */ new Set(["mcp-oauth-locks", ".sqlite-maintenance.lock"]);

The bundled error string Cannot create Codex shadow home entry → already exists and is not a symlink is present, and searching the bundle for sqlite-shm / sqlite-wal returns 0 occurrences — sidecars are handled nowhere. So this is not an app-version difference and not caused by any local configuration: shadowHomePath is the only setting involved.

Proposed fix

Treat the per-database sidecars as what they are — runtime entries that Codex/SQLite creates locally and that must point at shared state:

const isReplaceableSharedRuntimeEntry = (entryName: string) =>
  REPLACEABLE_SHARED_RUNTIME_ENTRIES.has(entryName) || /\.sqlite-(?:shm|wal)$/.test(entryName);

with the NotSymlink branch calling isReplaceableSharedRuntimeEntry(...) instead of REPLACEABLE_SHARED_RUNTIME_ENTRIES.has(...). Verified in the harness: with only that change, the failing scenario above completes and both -shm and -wal in the shadow home become links to the shared home (PROPOSED PATCH test passes). The change is 5 lines; the only other difference in the patched copy used for that run was this predicate and its call site.

Activity

  1. juliusmarminge commented on Oct 16, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Tracking this under open #19861, which keeps Codex SQLite (including WAL/SHM) in one home so shadow-home materialization no longer creates private sidecars that disable the instance. Different from #19556 (managed codex update).

  2. added
    bugSomething is broken or behaving incorrectly.
    on Oct 16, 2026
  3. juliusmarminge commented on Oct 21, 2026

    @juliusmarminge
    Member

    Duplicate of #5817. Its August 20 and October 7 comments document the same Windows failure: symlinked base databases with regular shadow-local WAL/SHM files disable the secondary Codex provider. The October 19 follow-up reports the exact goals_1.sqlite-shm conflict. Closing this report into that existing tracking issue; no new runtime reproduction was run during duplicate triage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is broken or behaving incorrectly.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions