|
| 1 | +# Codex App / codex-rs findings |
| 2 | + |
| 3 | +## Summary |
| 4 | + |
| 5 | +The sidebar disappearance is caused by two independent filters in Codex App / app-server: |
| 6 | + |
| 7 | +1. Provider filter: when `model_providers` is omitted, app-server defaults to the active configured provider. |
| 8 | +2. Source filter: when `source_kinds` is omitted or empty, app-server defaults to `INTERACTIVE_SESSION_SOURCES`. |
| 9 | + |
| 10 | +In the local codex-rs checkout at `/Users/jun/Developer/codex/codex-cli/codex-rs`, `INTERACTIVE_SESSION_SOURCES` is: |
| 11 | + |
| 12 | +```text |
| 13 | +cli |
| 14 | +vscode |
| 15 | +custom atlas |
| 16 | +custom chatgpt |
| 17 | +``` |
| 18 | + |
| 19 | +It does not include `exec`. |
| 20 | + |
| 21 | +## Source anchors |
| 22 | + |
| 23 | +codex-rs source anchors: |
| 24 | + |
| 25 | +- `/Users/jun/Developer/codex/codex-cli/codex-rs/rollout/src/lib.rs` |
| 26 | + - `INTERACTIVE_SESSION_SOURCES` includes `Cli`, `VSCode`, `Custom("atlas")`, `Custom("chatgpt")`. |
| 27 | +- `/Users/jun/Developer/codex/codex-cli/codex-rs/app-server/src/filters.rs` |
| 28 | + - `compute_source_filters(None)` returns `INTERACTIVE_SESSION_SOURCES`. |
| 29 | + - `compute_source_filters(Some(Vec::new()))` also returns `INTERACTIVE_SESSION_SOURCES`. |
| 30 | + - `ThreadSourceKind::Exec` requires an explicit source filter. |
| 31 | +- `/Users/jun/Developer/codex/codex-cli/codex-rs/app-server/src/request_processors/thread_processor.rs` |
| 32 | + - `model_providers: None` becomes `Some(vec![self.config.model_provider_id.clone()])`. |
| 33 | + - `source_kinds` flows through `compute_source_filters()`. |
| 34 | + - Those filters are passed to `thread_store.list_threads()`. |
| 35 | +- `/Users/jun/Developer/codex/codex-cli/codex-rs/state/src/runtime/threads.rs` |
| 36 | + - SQL filter applies `threads.archived = 0`, `threads.preview <> ''`, optional `threads.source IN (...)`, optional `threads.model_provider IN (...)`, and optional `threads.cwd IN (...)`. |
| 37 | + |
| 38 | +## Local DB evidence |
| 39 | + |
| 40 | +Read-only query against `/Users/jun/.codex/state_5.sqlite` for project cwd `/Users/jun/Developer/new/700_projects/opencodex`: |
| 41 | + |
| 42 | +| model_provider | source | count | |
| 43 | +| --- | --- | ---: | |
| 44 | +| `openai` | `cli` | 7 | |
| 45 | +| `openai` | `exec` | 2 | |
| 46 | +| `opencodex` | `exec` | 43 | |
| 47 | +| `opencodex` | subagent thread-spawn JSON | 2 | |
| 48 | + |
| 49 | +Default Codex App list while opencodex is active: |
| 50 | + |
| 51 | +```sql |
| 52 | +WHERE archived = 0 |
| 53 | + AND preview <> '' |
| 54 | + AND source IN ('cli', 'vscode', 'atlas', 'chatgpt') |
| 55 | + AND model_provider = 'opencodex' |
| 56 | +``` |
| 57 | + |
| 58 | +That returns zero rows locally because opencodex-created project rows are `source = 'exec'`. |
| 59 | + |
| 60 | +## Upstream patch direction |
| 61 | + |
| 62 | +The cleaner upstream fix would be in Codex App / codex-rs: |
| 63 | + |
| 64 | +- either request `sourceKinds` including `exec` for the project sidebar, |
| 65 | +- or make the project sidebar intentionally provider/source agnostic when the user is browsing project history, |
| 66 | +- or expose a UI affordance for source filtering. |
| 67 | + |
| 68 | +opencodex cannot change Codex App's `thread/list` request payload. The opencodex-side fix therefore must be an explicit compatibility mode that temporarily adjusts local metadata and restores it later. |
| 69 | + |
| 70 | +## opencodex fix direction |
| 71 | + |
| 72 | +For `syncResumeHistory: true`: |
| 73 | + |
| 74 | +- backup original thread metadata into `~/.opencodex/codex-history-backup.json`; |
| 75 | +- remap old OpenAI `cli`/`vscode` rows to `model_provider = 'opencodex'`; |
| 76 | +- promote opencodex-created user `exec` rows to `source = 'cli'`; |
| 77 | +- update the rollout JSONL first `session_meta` line consistently so Codex's rollout scanner does not repair the DB back to the hidden state; |
| 78 | +- on `ocx stop` / `ocx restore`, restore only rows recorded in the backup manifest. |
| 79 | + |
| 80 | +Default remains unchanged: no history mutation unless the user explicitly enables `syncResumeHistory`. |
| 81 | + |
| 82 | +## Legacy PR #13 upgrade edge |
| 83 | + |
| 84 | +There is one unsafe-to-automate edge case: |
| 85 | + |
| 86 | +1. A user enabled `syncResumeHistory: true` on a development build before backup support existed. |
| 87 | +2. That build remapped old `openai` interactive rows to `opencodex`. |
| 88 | +3. The user upgrades while those rows are still remapped. |
| 89 | +4. The new backup manifest does not exist, so `ocx stop` cannot know which `opencodex` interactive rows were originally OpenAI rows. |
| 90 | + |
| 91 | +The fix must not silently rewrite all `opencodex` `cli`/`vscode` rows to `openai`, because future or app-created rows can legitimately be opencodex-owned. Instead: |
| 92 | + |
| 93 | +- normal `ocx stop` detects and reports ambiguous unbacked rows; |
| 94 | +- it leaves them unchanged by default; |
| 95 | +- `ocx recover-history --legacy-openai` is the explicit manual recovery path for users who know those rows came from the old remap. |
| 96 | + |
| 97 | +## 2026-06-22 correction: native restore cannot leave opencodex providers behind |
| 98 | + |
| 99 | +Live local testing showed that the conservative "leave unbacked opencodex rows unchanged" approach |
| 100 | +breaks Codex App after `ocx stop`: |
| 101 | + |
| 102 | +```text |
| 103 | +Codex can't load config.toml, so this thread can't resume. |
| 104 | +Fix config.toml: Model provider `opencodex` not found. |
| 105 | +``` |
| 106 | + |
| 107 | +The root cause is straightforward: `ocx stop` removes `[model_providers.opencodex]` from |
| 108 | +`~/.codex/config.toml`. Any remaining `threads.model_provider = 'opencodex'` row can therefore point |
| 109 | +Codex App at a provider id that no longer exists. This applies not only to legacy `cli`/`vscode` rows, |
| 110 | +but also to opencodex-created `exec` rows and subagent rows if the user resumes them directly. |
| 111 | + |
| 112 | +Revised opencodex invariant: |
| 113 | + |
| 114 | +- while opencodex is active, `syncResumeHistory: true` may remap/promote history so the App sidebar is visible; |
| 115 | +- after native restore (`ocx stop`, `ocx restore`, uninstall), no user thread should be left with |
| 116 | + `model_provider = 'opencodex'` unless the Codex config still contains that provider; |
| 117 | +- backed-up OpenAI rows restore to OpenAI; |
| 118 | +- opencodex-owned user rows are ejected to `openai`, and `exec` source is promoted to `cli`, so native |
| 119 | + Codex can list/resume them after the proxy provider has been removed; |
| 120 | +- root `model = "provider/model"` values are also stripped on native restore because provider-prefixed |
| 121 | + routed model ids are invalid without the opencodex provider/catalog. |
| 122 | + |
| 123 | +Local repair evidence after the correction: |
| 124 | + |
| 125 | +- `ocx recover-history --legacy-openai` recovered 218 user thread rows to `openai`; |
| 126 | +- `ocx stop` left zero user rows with `model_provider = 'opencodex'`; |
| 127 | +- `~/.codex/config.toml` no longer contains `[model_providers.opencodex]`, root |
| 128 | + `model_provider = "opencodex"`, or root `model = "provider/model"`. |
0 commit comments