fix(server): Codex shadow homes keep their own cloud config bundle cache#26817
Problem
A Codex provider with a shadow home is disabled with Cannot create Codex shadow home entry 'cloud-config-bundle-cache.json' because '<shadow>/cloud-config-bundle-cache.json' already exists and is not a symlink. T3 links that cache from the main Codex home into every shadow home. Codex refreshes it with an atomic temp-and-rename write, which replaces the link with a regular file, and the next shadow-home setup rejects that file as a conflict. Reproduced on main with the real dev server: a shadow home that holds its own cache is disabled on start, and a linked one is disabled after a simulated Codex refresh and a restart.
Change
cloud-config-bundle-cache.json joins PRIVATE_ENTRY_NAMES, next to models_cache.json. Codex stores the cache per ChatGPT identity (chatgpt_user_id, account_id, signed payload) and ignores a cache from a different identity, so it was never meant to be shared. A shadow home now keeps its own cache file, a stale link to the shared copy is removed, and new shadow homes no longer get the link.
Scope and approval
Fixes pingdotgg/t3code#26811
Triaged via the automated issue flow; GitCafe issue #26811.
Verification
- Red first:
keeps a shadow home's own cloud config bundle cacheanddoes not share the cloud config bundle cache with shadow homesinapps/server/src/provider/Drivers/CodexHomeLayout.test.tsfail on main (2 failed, 9 passed): the first with the reporter'sCodexShadowHomeEntryConflictError, the second because a fresh shadow home gets the link. With the fix: 11/11 pass. vp run check:changedpasses.- Real dev server (
T3CODE_DEV_SEED=0 vp run dev, Linux x64, codex-cli 0.163.0, synthetic homes): three Codex instances share one home. A: the shadow home holds its own cache. B: the cache is linked to the shared one. C: an empty shadow home.
| Step | Before (main 7632c3dc70) | After (fix) |
|---|---|---|
| First start | A disabled with the conflict error; B and C read the main account's cache through a link | All three start; A keeps its file, B's link is removed, C gets no link |
| Codex refresh (temp + rename) in the shadow home, then restart | A and B disabled | All three start; each shadow home keeps its own cache; shared cache untouched |
before-runtime.txt Β· after-runtime.txt Β· drivers: repro-setup.sh, codex-rewrite.sh, merge-settings.py
- Not checked: a real ChatGPT-authenticated Codex refreshing the cache (the refresh is simulated with the same temp-and-rename write), and macOS, where it was reported. The report's
config.tomlconflict did not reproduce: with codex-cli 0.163.0,codex mcp add,codex features enableand app-serverconfig/value/writeandconfig/batchWriteall write through the shadow home'sconfig.tomllink and leave it a link (config-toml-check.txt, appserver-write.py). This PR leavesconfig.tomlhandling unchanged. - No UI changes, so there are no screenshots; this is a server-only fix proved by the tests and the runtime transcripts.
Models: Claude Opus 5.5 (lead, tests, implementation, review) through Claude Code in T3 Code; GPT-6.1-Sol (review) through the Codex harness.
π€ Generated with Claude Code
Checks
Added 3 commits
Approvability
Verdict: Approved at a1796b3
Macroscope's review found this PR approvable β Codex shadow homes now keep their cloud-config bundle cache separate instead of linking to the shared home. Existing cache links are removed without deleting their targets, and direct-home behavior remains unchanged.
You can add or adjust custom eligibility rules. Learn more.
Thread transfer impact
β Thread transfer remains within every enforced ceiling.
| Provider | Metric | Main baseline | This PR | Impact | PR ceiling | |
|---|---|---|---|---|---|---|
| Codex | Total thread wire | 5.0 KiB | 5.0 KiB | 0 B (0.0%) | 6.8 KiB | β |
| Codex | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | β |
| Codex | Live turn WebSocket wire | 1.3 KiB | 1.3 KiB | 0 B (0.0%) | 2.0 KiB | β |
| Codex | Live turn WebSocket decoded | 21.6 KiB | 21.6 KiB | 0 B (0.0%) | 24.4 KiB | β |
| Codex | Live turn messages | 2 | 2 | 0 (0.0%) | 8 | β |
| Claude | Total thread wire | 5.0 KiB | 5.0 KiB | β29 B (β0.6%) | 6.8 KiB | β |
| Claude | Thread snapshot wire | 3.8 KiB | 3.8 KiB | 0 B (0.0%) | 4.9 KiB | β |
| Claude | Live turn WebSocket wire | 1.3 KiB | 1.2 KiB | β29 B (β2.3%) | 2.0 KiB | β |
| Claude | Live turn WebSocket decoded | 21.9 KiB | 21.9 KiB | β41 B (β0.2%) | 24.4 KiB | β |
| Claude | Live turn messages | 2 | 1 | β1 (β50.0%) | 8 | β |
Baseline: dac8b11 Β· PR result: a1796b3 Β· Source CI: success
Scenario and decoded snapshot size
10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
- Codex decoded thread snapshot: 108.5 KiB
- Claude decoded thread snapshot: 108.8 KiB
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.
No actionable comments were generated in the recent review.
External image Recent review info
-
External image Run configuration
- Source mode: Auto
- Repository: pingdotgg/t3code
- Branch: fix/issue-26811
- File: .coderabbit.config.ts
- Commit: a1796b3
- Layers: Repository TS config
- Review profile: CHILL
- Plan: Team
- Run ID:
0584c524-2f2e-47c6-9bc8-7624fb71b2a4
External image Commits
Reviewing files that changed from the base of the PR and between dac8b11ddc25bd5da62a367fdb6c3de777ae2986 and a1796b31ccb3d3dca531a140a797398def80c80f.
External image Files selected for processing: 2
apps/server/src/provider/Drivers/CodexHomeLayout.test.tsapps/server/src/provider/Drivers/CodexHomeLayout.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.
Walkthrough
Codex shadow-home materialization now treats
cloud-config-bundle-cache.jsonas private. Tests cover existing shadow files, fresh entries, and symlinks.Changes: Codex shadow cache
Layer / File(s) Summary Private cache materialization
apps/server/src/provider/Drivers/CodexHomeLayout.ts,apps/server/src/provider/Drivers/CodexHomeLayout.test.tsPRIVATE_ENTRY_NAMESnow includescloud-config-bundle-cache.json. Tests verify existing shadow cache contents remain unchanged, while fresh and symlinked shadow cache entries are removed without changing the shared cache.Priority: External image Normal
Estimated code review effort: 2 (Simple) | ~10 minutes
Change: Bug fix Β· Severity of issue fixed: Medium
Suggested reviewers:
t3dotgg
Merge Risk: External image Minimal Β· up to a1796
The PR isolates Codex cache state without an established merge-blocking risk.
Pre-merge checks: Passed 4
Check name Status Explanation Title check Passed The title clearly and concisely describes the primary fix: keeping the Codex shadow homes' cloud config bundle caches private. Description check Passed The description includes all required sections. It explains the problem, the implementation, scope and approval reference, focused verification results, limitations, and agent attribution. Linked Issues check Passed The change satisfies the coding requirement in issue #26811.CodexHomeLayout.tsaddscloud-config-bundle-cache.jsontoPRIVATE_ENTRY_NAMES, so new shadow homes do not receive the shared-cache liβ¦Out of Scope Changes check Passed The changed production code and tests are directly connected to issue #26811. The tests cover the new private-entry behavior. No unrelated production behavior or UI change is present in the reviewed dβ¦Full details: Linked Issues check
Explanation
The change satisfies the coding requirement in issue
#26811.CodexHomeLayout.tsaddscloud-config-bundle-cache.jsontoPRIVATE_ENTRY_NAMES, so new shadow homes do not receive the shared-cache link and existing cache links are removed during materialization. The added tests inCodexHomeLayout.test.tsverify preservation of a shadow home's own cache, removal of stale links, and preservation of the shared cache.
Full details: Out of Scope Changes check
Explanation
The changed production code and tests are directly connected to issue
#26811. The tests cover the new private-entry behavior. No unrelated production behavior or UI change is present in the reviewed diff.
Finishing Touches
π Generate docstrings
- Commit to this branch
- Create a new PR
π§ͺ Generate unit tests (beta)
- Commit to this branch
- Create a new PR
- Autofix Β· Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Comment @coderabbitai help to get the list of available commands.
cloud-requirements-cache.json has the same shape and should probably join PRIVATE_ENTRY_NAMES here too. Like the config bundle, it is a signed_payload + signature cache scoped to chatgpt_user_id, so sharing it across accounts is wrong for the same reason, and an atomic refresh in a shadow home would hit the same CodexShadowHomeEntryConflictError.
Seen on macOS: a secondary account's shadow home had cloud-config-bundle-cache.json replaced by a real file (the reported error), while cloud-requirements-cache.json was still a symlink to ~/.codex, holding the primary account's chatgpt_user_id.
Suggested addition:
| 1 | const PRIVATE_ENTRY_NAMES = new Set([ |
| 2 | "auth.json", |
| 3 | "auth.lock", |
| 4 | "models_cache.json", |
| 5 | "cloud-config-bundle-cache.json", |
| 6 | "cloud-requirements-cache.json", |
| 7 | ]); |
The existing stale-link removal then cleans up already-linked shadow homes, as it does for the config bundle.
Closing in favour of #23472, which makes the same change.