fix(codex): retag thread providers so history menu stays whole across the proxy boundary#1034
Merged
JerrettDavis merged 2 commits intoJun 16, 2026
Merged
Conversation
Codex stamps every thread with the model_provider it ran under and filters its history/projects menu by the active provider set. Because the provider scope rewrites config.toml to route Codex through the custom `headroom` provider, threads created through Headroom are tagged `headroom` while native threads stay `openai` -- so connecting Headroom appears to drop the existing history, and disconnecting drops the proxied history. Retag threads to match whichever provider is active: openai->headroom on apply_provider_scope, headroom->openai on revert_provider_scope. Only rows matching the source provider are touched, so third-party providers are left alone. All operations are best-effort (missing store / missing table / store locked by a running Codex is logged and skipped) so install and uninstall never fail on account of the history menu. The store is WAL-mode, so the update succeeds even while Codex is running. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`headroom wrap codex` injects the custom `headroom` provider into Codex's config; new threads then carry `model_provider = "headroom"` while native threads keep `openai`. Codex's history menu filters by the active provider set, so wrapping/unwrapping hid one set of threads. Reconcile the SQLite thread tags alongside the config edits: wrap pulls `openai -> headroom`, unwrap hands them back `headroom -> openai`. Best-effort and idempotent; third-party providers are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
PR governanceThis PR follows the template and is marked ready for human review. |
Contributor
Author
|
@chopratejas @JerrettDavis - this is a bit of a risky PR imo and requires a judgement call as to whether it fits Headroom's philosophy. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
JerrettDavis
approved these changes
Jun 16, 2026
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a Codex UX regression when routing through Headroom by retagging Codex thread rows in Codex’s SQLite state DB so the history/projects menu remains continuous across provider changes (openai ↔ headroom).
Changes:
- Add best-effort SQLite retagging for Codex threads (
model_provider) across both known Codex state stores. - Invoke retagging on Codex provider enable/disable in both persistent install helpers and CLI wrap/unwrap flows.
- Add unit + integration tests covering retag directionality, missing/invalid stores, and wrap/unwrap round-trip behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
headroom/providers/codex/threads.py |
New best-effort SQLite retag utility for Codex thread model_provider values across GUI/CLI stores. |
headroom/providers/codex/install.py |
Calls retagging after applying/reverting persistent provider-scope config changes. |
headroom/cli/wrap.py |
Calls retagging after wrap injection and after successful unwrap restore/cleanup/removal. |
tests/test_provider_codex_threads.py |
Unit tests for retagging logic, missing tables, and best-effort behavior on missing/corrupt stores. |
tests/test_cli/test_wrap_codex.py |
Integration test ensuring wrap/unwrap round-trips thread provider tags across both Codex stores. |
Merged
chopratejas
pushed a commit
that referenced
this pull request
Jun 16, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([#1041](#1041)) ([e67ee2a](e67ee2a)) * add dashboard agent usage stats ([#814](#814)) ([6d3f39f](6d3f39f)) * Add support for Mistral Vibe CLI ([#935](#935)) ([0932b8b](0932b8b)) * attribute reread waste to over-compression via marker check ([#901](#901)) ([f928576](f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([#999](#999)) ([0dc2e1c](0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([#913](#913)) ([2a4d300](2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([#918](#918)) ([5939004](5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([#909](#909)) ([7d4ae86](7d4ae86)) * **policy:** batch deep edits through one cache-bust ([#856](#856) P3a) ([#1015](#1015)) ([c2e52fe](c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([#856](#856) P2) ([#905](#905)) ([553ade4](553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([#720](#720)) ([7edb27a](7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([#651](#651)) ([0c5c89d](0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([#942](#942)) ([#954](#954)) ([a7ee8a6](a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([#950](#950)) ([1ec9320](1ec9320)) * **cache:** name the missing piece in semantic detector guard ([#1018](#1018)) ([3b0bcee](3b0bcee)) * **ci:** check out repo in PR Governance label job ([#1021](#1021)) ([4558bc2](4558bc2)) * **ci:** make PR governance advisory ([#1047](#1047)) ([74dff94](74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([#898](#898)) ([b9e2761](b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([#924](#924)) ([8c00f71](8c00f71)) * **codex:** PR health label check state ([#986](#986)) ([99c874d](99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([#1034](#1034)) ([74ae781](74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([#743](#743)) ([dff6a19](dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([#892](#892)) ([b1f700f](b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([#887](#887)) ([d6f0f0f](d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([#890](#890)) ([16ed73b](16ed73b)) * **compression:** measure short-value threshold on payload, not token ([#889](#889)) ([65b0e8c](65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([#893](#893)) ([6cdb846](6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([#897](#897)) ([9b0c840](9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([#997](#997)) ([#1027](#1027)) ([2d3701b](2d3701b)) * **learn:** scan subagent and workflow transcripts ([#1045](#1045)) ([0ddd4ed](0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([#947](#947)) ([7c8c909](7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([#903](#903)) ([0632eba](0632eba)) * **proxy:** add native Bedrock converse-stream route ([#917](#917)) ([b08ec15](b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([#1000](#1000)) ([7dbbb40](7dbbb40)) * **proxy:** make budget enforcement actually work ([#885](#885)) ([a14ab45](a14ab45)) * **proxy:** read RTK gain stats globally by default ([#957](#957)) ([b70fccb](b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([#821](#821)) ([e20f16b](e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([#1003](#1003)) ([919379a](919379a)) * support Copilot Business subscription auth ([#641](#641)) ([0b4a4bd](0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([#943](#943)) ([9b7b436](9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([#884](#884)) ([dd22cfd](dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([#704](#704)) ([e36fccd](e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Epicism
pushed a commit
to Epicism/headroom
that referenced
this pull request
Jun 17, 2026
… the proxy boundary (headroomlabs-ai#1034) ## Description Codex stamps every thread with the `model_provider` it ran under and filters its history/projects menu by the currently active provider set. When Headroom rewrites Codex's config to route through the custom `headroom` provider, threads created through Headroom are tagged `headroom` while native threads keep `openai` — so the two sets never appear in the same menu. The visible symptom: enabling Headroom appears to "lose" the entire native Codex history, and disabling it hides everything created while wrapped. This reconciles the thread tags in Codex's SQLite store alongside the existing config edits, so the menu stays whole across the proxy boundary in both directions: `openai -> headroom` on enable/wrap, `headroom -> openai` on revert/unwrap. Only rows matching the source provider are touched, so third-party providers (e.g. `anthropic`) are left alone. The provider key cannot be unified by config — Codex rejects naming a custom provider `openai` ("reserved built-in provider IDs") — so retagging the store is the only path. A DB-only retag is sufficient: resuming a retagged session still routes completions through the active provider; the rollout `.jsonl` files do not need rewriting. Every operation is best-effort: a missing store, a missing `threads` table, or a corrupt store is logged and skipped, never raised, so install/uninstall and wrap/unwrap never fail on account of the history menu. The store is WAL-mode, so the update succeeds even while Codex is running; the short busy timeout only covers a transient checkpoint lock. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - New `headroom/providers/codex/threads.py`: best-effort retag of Codex thread provider tags across both known stores (`<codex_home>/sqlite/state_5.sqlite` for the GUI and `<codex_home>/state_5.sqlite` for the CLI/TUI). `retag_to_headroom` / `retag_to_native` wrap the directional helper. `codex_home` is passed in by callers (never re-derived from `Path.home()`), so tests stay pointed at a temp dir. - `providers/codex/install.py`: `apply_provider_scope` calls `retag_to_headroom` after writing the provider block; `revert_provider_scope` calls `retag_to_native` after stripping it. - `cli/wrap.py`: `_inject_codex_provider_config` calls `retag_to_headroom`; `unwrap_codex` calls `retag_to_native` once the config restore reports a `restored`/`cleaned`/`removed` status. - Tests: `tests/test_provider_codex_threads.py` (retag direction, threads-table no-op, missing/corrupt store best-effort) and a wrap/unwrap round-trip integration test in `tests/test_cli/test_wrap_codex.py`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run --extra dev pytest tests/test_provider_codex_threads.py tests/test_cli/test_wrap_codex.py tests/test_install/test_providers.py -q ... 88 passed, 2 failed # The 2 failures are TestInjectAvoidsDuplicateTopLevelKeys::* — pre-existing on a clean # upstream/main checkout, unrelated to this change: they `import tomllib`, which is # stdlib only on Python 3.11+, and this environment runs Python 3.10.18. $ uv run --extra dev ruff check headroom/cli/wrap.py headroom/providers/codex/threads.py \ headroom/providers/codex/install.py tests/test_cli/test_wrap_codex.py tests/test_provider_codex_threads.py All checks passed! $ uv run --extra dev mypy headroom/providers/codex/threads.py headroom/providers/codex/install.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: macOS, Codex GUI v148 + Codex CLI, Python 3.10.18. - Exact command / steps: The root cause and fix were confirmed live in the Headroom desktop app, which performs the identical SQLite retag. Connecting Codex to Headroom hid ~140 native (`openai`) threads from the history menu; running `UPDATE threads SET model_provider='headroom' WHERE model_provider='openai'` on the live store (`~/.codex/sqlite/state_5.sqlite`) made the full menu reappear, and resuming a retagged session still routed completions through the active provider. This Python port is a 1:1 of that logic, exercised by the unit + wrap/unwrap integration tests above. - Observed result: full Codex history menu restored across enable/disable; third-party provider rows untouched; the real `~/.codex` stores were snapshotted before/after the test run and were not mutated by the tests. - Not tested: an end-to-end `headroom wrap codex` run against a live Codex GUI in this CI environment (no Codex install here); covered instead by the integration test invoking the real `wrap`/`unwrap` Click commands against a temp `$HOME`. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — behavior is in Codex's own history menu; covered by the proof above. ## Additional Notes - Documentation / CHANGELOG: N/A — internal behavior with no user-facing surface beyond the restored menu. - The 2 failing `TestInjectAvoidsDuplicateTopLevelKeys` tests are pre-existing on upstream/main and fail only because this environment runs Python 3.10 (no `tomllib`); they are unrelated to this change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
studyzy
pushed a commit
to studyzy/headroom
that referenced
this pull request
Jun 24, 2026
… the proxy boundary (headroomlabs-ai#1034) ## Description Codex stamps every thread with the `model_provider` it ran under and filters its history/projects menu by the currently active provider set. When Headroom rewrites Codex's config to route through the custom `headroom` provider, threads created through Headroom are tagged `headroom` while native threads keep `openai` — so the two sets never appear in the same menu. The visible symptom: enabling Headroom appears to "lose" the entire native Codex history, and disabling it hides everything created while wrapped. This reconciles the thread tags in Codex's SQLite store alongside the existing config edits, so the menu stays whole across the proxy boundary in both directions: `openai -> headroom` on enable/wrap, `headroom -> openai` on revert/unwrap. Only rows matching the source provider are touched, so third-party providers (e.g. `anthropic`) are left alone. The provider key cannot be unified by config — Codex rejects naming a custom provider `openai` ("reserved built-in provider IDs") — so retagging the store is the only path. A DB-only retag is sufficient: resuming a retagged session still routes completions through the active provider; the rollout `.jsonl` files do not need rewriting. Every operation is best-effort: a missing store, a missing `threads` table, or a corrupt store is logged and skipped, never raised, so install/uninstall and wrap/unwrap never fail on account of the history menu. The store is WAL-mode, so the update succeeds even while Codex is running; the short busy timeout only covers a transient checkpoint lock. Closes # ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - New `headroom/providers/codex/threads.py`: best-effort retag of Codex thread provider tags across both known stores (`<codex_home>/sqlite/state_5.sqlite` for the GUI and `<codex_home>/state_5.sqlite` for the CLI/TUI). `retag_to_headroom` / `retag_to_native` wrap the directional helper. `codex_home` is passed in by callers (never re-derived from `Path.home()`), so tests stay pointed at a temp dir. - `providers/codex/install.py`: `apply_provider_scope` calls `retag_to_headroom` after writing the provider block; `revert_provider_scope` calls `retag_to_native` after stripping it. - `cli/wrap.py`: `_inject_codex_provider_config` calls `retag_to_headroom`; `unwrap_codex` calls `retag_to_native` once the config restore reports a `restored`/`cleaned`/`removed` status. - Tests: `tests/test_provider_codex_threads.py` (retag direction, threads-table no-op, missing/corrupt store best-effort) and a wrap/unwrap round-trip integration test in `tests/test_cli/test_wrap_codex.py`. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text $ uv run --extra dev pytest tests/test_provider_codex_threads.py tests/test_cli/test_wrap_codex.py tests/test_install/test_providers.py -q ... 88 passed, 2 failed # The 2 failures are TestInjectAvoidsDuplicateTopLevelKeys::* — pre-existing on a clean # upstream/main checkout, unrelated to this change: they `import tomllib`, which is # stdlib only on Python 3.11+, and this environment runs Python 3.10.18. $ uv run --extra dev ruff check headroom/cli/wrap.py headroom/providers/codex/threads.py \ headroom/providers/codex/install.py tests/test_cli/test_wrap_codex.py tests/test_provider_codex_threads.py All checks passed! $ uv run --extra dev mypy headroom/providers/codex/threads.py headroom/providers/codex/install.py Success: no issues found in 2 source files ``` ## Real Behavior Proof - Environment: macOS, Codex GUI v148 + Codex CLI, Python 3.10.18. - Exact command / steps: The root cause and fix were confirmed live in the Headroom desktop app, which performs the identical SQLite retag. Connecting Codex to Headroom hid ~140 native (`openai`) threads from the history menu; running `UPDATE threads SET model_provider='headroom' WHERE model_provider='openai'` on the live store (`~/.codex/sqlite/state_5.sqlite`) made the full menu reappear, and resuming a retagged session still routed completions through the active provider. This Python port is a 1:1 of that logic, exercised by the unit + wrap/unwrap integration tests above. - Observed result: full Codex history menu restored across enable/disable; third-party provider rows untouched; the real `~/.codex` stores were snapshotted before/after the test run and were not mutated by the tests. - Not tested: an end-to-end `headroom wrap codex` run against a live Codex GUI in this CI environment (no Codex install here); covered instead by the integration test invoking the real `wrap`/`unwrap` Click commands against a temp `$HOME`. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A — behavior is in Codex's own history menu; covered by the proof above. ## Additional Notes - Documentation / CHANGELOG: N/A — internal behavior with no user-facing surface beyond the restored menu. - The 2 failing `TestInjectAvoidsDuplicateTopLevelKeys` tests are pre-existing on upstream/main and fail only because this environment runs Python 3.10 (no `tomllib`); they are unrelated to this change. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
studyzy
pushed a commit
to studyzy/headroom
that referenced
this pull request
Jun 24, 2026
🤖 I have created a release *beep* *boop* --- <details><summary>0.26.0</summary> ## [0.26.0](headroomlabs-ai/headroom@v0.25.0...v0.26.0) (2026-06-16) ### Features * add Copilot BYOK provider wrapper utilities and CLI support ([headroomlabs-ai#1041](headroomlabs-ai#1041)) ([e67ee2a](headroomlabs-ai@e67ee2a)) * add dashboard agent usage stats ([headroomlabs-ai#814](headroomlabs-ai#814)) ([6d3f39f](headroomlabs-ai@6d3f39f)) * Add support for Mistral Vibe CLI ([headroomlabs-ai#935](headroomlabs-ai#935)) ([0932b8b](headroomlabs-ai@0932b8b)) * attribute reread waste to over-compression via marker check ([headroomlabs-ai#901](headroomlabs-ai#901)) ([f928576](headroomlabs-ai@f928576)) * **bedrock:** cross-region + Converse compression; bundle proxy binary in images ([headroomlabs-ai#999](headroomlabs-ai#999)) ([0dc2e1c](headroomlabs-ai@0dc2e1c)) * **dashboard:** surface compression-vs-cache net impact in Prefix Cache panel ([headroomlabs-ai#913](headroomlabs-ai#913)) ([2a4d300](headroomlabs-ai@2a4d300)) * **evals:** adversarial-input robustness grid for compressors ([headroomlabs-ai#918](headroomlabs-ai#918)) ([5939004](headroomlabs-ai@5939004)) * **parser:** detect re-issued identical tool calls as reread waste ([headroomlabs-ai#909](headroomlabs-ai#909)) ([7d4ae86](headroomlabs-ai@7d4ae86)) * **policy:** batch deep edits through one cache-bust ([headroomlabs-ai#856](headroomlabs-ai#856) P3a) ([headroomlabs-ai#1015](headroomlabs-ai#1015)) ([c2e52fe](headroomlabs-ai@c2e52fe)) * **policy:** consume net-cost mutation gate in ContentRouter ([headroomlabs-ai#856](headroomlabs-ai#856) P2) ([headroomlabs-ai#905](headroomlabs-ai#905)) ([553ade4](headroomlabs-ai@553ade4)) * **proxy:** compress AWS Bedrock InvokeModel requests via configurable upstream ([headroomlabs-ai#720](headroomlabs-ai#720)) ([7edb27a](headroomlabs-ai@7edb27a)) ### Bug Fixes * **anthropic:** strip styled Claude model ids ([headroomlabs-ai#651](headroomlabs-ai#651)) ([0c5c89d](headroomlabs-ai@0c5c89d)) * **anyllm:** forward openai api_base/api_key to the any-llm backend ([headroomlabs-ai#942](headroomlabs-ai#942)) ([headroomlabs-ai#954](headroomlabs-ai#954)) ([a7ee8a6](headroomlabs-ai@a7ee8a6)) * **cache:** guard None exemplar embeddings in dynamic detector ([headroomlabs-ai#950](headroomlabs-ai#950)) ([1ec9320](headroomlabs-ai@1ec9320)) * **cache:** name the missing piece in semantic detector guard ([headroomlabs-ai#1018](headroomlabs-ai#1018)) ([3b0bcee](headroomlabs-ai@3b0bcee)) * **ci:** check out repo in PR Governance label job ([headroomlabs-ai#1021](headroomlabs-ai#1021)) ([4558bc2](headroomlabs-ai@4558bc2)) * **ci:** make PR governance advisory ([headroomlabs-ai#1047](headroomlabs-ai#1047)) ([74dff94](headroomlabs-ai@74dff94)) * **codex:** compute waste signals on the OpenAI Responses path ([headroomlabs-ai#898](headroomlabs-ai#898)) ([b9e2761](headroomlabs-ai@b9e2761)) * **codex:** poll /wham/usage for subscription limits (handshake no longer sends x-codex-* headers) ([headroomlabs-ai#924](headroomlabs-ai#924)) ([8c00f71](headroomlabs-ai@8c00f71)) * **codex:** PR health label check state ([headroomlabs-ai#986](headroomlabs-ai#986)) ([99c874d](headroomlabs-ai@99c874d)) * **codex:** retag thread providers so history menu stays whole across the proxy boundary ([headroomlabs-ai#1034](headroomlabs-ai#1034)) ([74ae781](headroomlabs-ai@74ae781)) * **codex:** write canonical hooks feature flag and migrate deprecated codex_hooks ([headroomlabs-ai#743](headroomlabs-ai#743)) ([dff6a19](headroomlabs-ai@dff6a19)) * **compression:** convert tree-sitter byte offsets to char offsets ([headroomlabs-ai#892](headroomlabs-ai#892)) ([b1f700f](headroomlabs-ai@b1f700f)) * **compression:** correct JSON array item counting and entropy gate ([headroomlabs-ai#887](headroomlabs-ai#887)) ([d6f0f0f](headroomlabs-ai@d6f0f0f)) * **compression:** keep container bodies compressible in code handler ([headroomlabs-ai#890](headroomlabs-ai#890)) ([16ed73b](headroomlabs-ai@16ed73b)) * **compression:** measure short-value threshold on payload, not token ([headroomlabs-ai#889](headroomlabs-ai#889)) ([65b0e8c](headroomlabs-ai@65b0e8c)) * **compression:** use thread-local tree-sitter parsers in code handler ([headroomlabs-ai#893](headroomlabs-ai#893)) ([6cdb846](headroomlabs-ai@6cdb846)) * **gemini:** surface functionResponse payloads to waste-signal detection ([headroomlabs-ai#897](headroomlabs-ai#897)) ([9b0c840](headroomlabs-ai@9b0c840)) * **learn:** decode directory names with spaces in Windows project paths ([headroomlabs-ai#997](headroomlabs-ai#997)) ([headroomlabs-ai#1027](headroomlabs-ai#1027)) ([2d3701b](headroomlabs-ai@2d3701b)) * **learn:** scan subagent and workflow transcripts ([headroomlabs-ai#1045](headroomlabs-ai#1045)) ([0ddd4ed](headroomlabs-ai@0ddd4ed)) * **openclaw:** declare headroom_retrieve tool contract ([headroomlabs-ai#947](headroomlabs-ai#947)) ([7c8c909](headroomlabs-ai@7c8c909)) * **policy:** correct warm-cache penalty in net_mutation_gain to (S + dT) ([headroomlabs-ai#903](headroomlabs-ai#903)) ([0632eba](headroomlabs-ai@0632eba)) * **proxy:** add native Bedrock converse-stream route ([headroomlabs-ai#917](headroomlabs-ai#917)) ([b08ec15](headroomlabs-ai@b08ec15)) * **proxy:** keep codex image-generation WS turns alive through the relay ([headroomlabs-ai#1000](headroomlabs-ai#1000)) ([7dbbb40](headroomlabs-ai@7dbbb40)) * **proxy:** make budget enforcement actually work ([headroomlabs-ai#885](headroomlabs-ai#885)) ([a14ab45](headroomlabs-ai@a14ab45)) * **proxy:** read RTK gain stats globally by default ([headroomlabs-ai#957](headroomlabs-ai#957)) ([b70fccb](headroomlabs-ai@b70fccb)) * route v1internal code assist requests to cloudcode-pa.googleapis… ([headroomlabs-ai#821](headroomlabs-ai#821)) ([e20f16b](headroomlabs-ai@e20f16b)) * **serena:** stop the Serena dashboard popup and make --no-serena actually disable Serena ([headroomlabs-ai#1003](headroomlabs-ai#1003)) ([919379a](headroomlabs-ai@919379a)) * support Copilot Business subscription auth ([headroomlabs-ai#641](headroomlabs-ai#641)) ([0b4a4bd](headroomlabs-ai@0b4a4bd)) * wire HEADROOM_EXCLUDE_TOOLS / HEADROOM_TOOL_PROFILES into Click proxy entrypoint ([headroomlabs-ai#943](headroomlabs-ai#943)) ([9b7b436](headroomlabs-ai@9b7b436)) * **wrap:** avoid duplicate top-level keys when injecting codex provider ([headroomlabs-ai#884](headroomlabs-ai#884)) ([dd22cfd](headroomlabs-ai@dd22cfd)) ### Code Refactoring * DRY cache logic, add thread safety, fix Bash exclusion ([headroomlabs-ai#704](headroomlabs-ai#704)) ([e36fccd](headroomlabs-ai@e36fccd)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This was referenced Jul 14, 2026
JerrettDavis
added a commit
that referenced
this pull request
Jul 15, 2026
## Description Closes #2159. Codex wrappers currently launch against a disposable `CODEX_HOME`, so session state created during a wrapped run can disappear when that temporary directory is removed. This change launches Codex against its durable home, keeps proxy routing process-local, and adds recovery for retained temporary homes and pinned recovery sources. ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Launch Codex against its durable `CODEX_HOME` and apply routing through process-local config overrides after the actual proxy port is resolved. - Preserve custom provider identity and reject providers that cannot be redirected safely. - Detect dangling temporary Codex homes before interactive wraps and offer recovery. - Add `headroom recover codex` with automatic discovery, repeatable `--source`, preview, confirmation, retained backups, and rollback on failure. - Search Python's temp root, `$TMPDIR`, `/tmp`, `/private/tmp`, and macOS `/private/var/folders/*/*/T` for retained `headroom-codex-home-*` directories. - Reuse `source-pinned/` copies left by interrupted or failed recovery attempts after the original temporary home has disappeared. - Report deleted temporary homes still referenced by SQLite rollout paths without treating paths pasted into prompts or errors as filesystem evidence. - Audit the durable thread index, rollout files, and history when no source remains, including indexed chat counts and history-only orphan records. - Normalize legacy localhost `headroom` providers in both SQLite thread rows and rollout `session_meta`, including retries after an earlier broken recovery, while preserving user-defined remote providers named `headroom`. - Merge compatible config, JSONL, rollout, SQLite, credential, and regular-file state without propagating deletions or runtime artifacts. - Rewrite recovered thread rollout paths to the durable home and restore legacy Headroom thread providers to the active provider. - Validate SQLite schemas, SQLx migration checksums, integrity, and foreign keys, and quarantine malformed JSONL. - Preserve failed targets with an atomic rename before rollback, avoiding recursive-deletion races with live SQLite runtime files. - Document discovery, migration, retained backups, rollback behavior, and the limits of deleted-source recovery. ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed in isolated Docker containers ### Test Output ```text $ uv run pytest tests/test_cli/test_wrap_codex.py tests/test_cli/test_recover_codex.py -q 122 passed $ uv run ruff check headroom/cli/recover.py headroom/providers/codex/recovery.py tests/test_cli/test_recover_codex.py All checks passed! $ uv run ruff format --check headroom/cli/recover.py headroom/providers/codex/recovery.py tests/test_cli/test_recover_codex.py 3 files already formatted $ uv run mypy headroom/cli/recover.py headroom/providers/codex/recovery.py Success: no issues found in 2 source files ``` All validation ran in `ghcr.io/astral-sh/uv:python3.12-bookworm` against a writable disposable copy of a read-only source mount. Codex was not installed or launched, and no real user Codex state was read or modified. The tests cover multi-root discovery, deleted-reference reporting, retained pinned-source recovery, durable SQLite path relocation, SQLite and rollout provider normalization, idempotent repair after an earlier broken recovery, remote provider preservation, unrelated dangling target rows, backup retention, atomic rollback, malformed-state quarantine, SQLite validation, and Windows-safe handle closure. The repository shim E2E was not launched locally because this recovery work intentionally avoids launching Codex. Upstream CI exercises wrapper E2E in isolated environments. ## Real Behavior Proof - Environment: `ghcr.io/astral-sh/uv:python3.12-bookworm`, Python 3.12, a writable disposable checkout copied from a read-only source mount, at head `2d89ecec`. - Exact command / steps: Run `pytest -q tests/test_cli/test_wrap_codex.py tests/test_cli/test_recover_codex.py`, then run `ruff check` and `ruff format --check` against `headroom/cli/wrap.py`, `headroom/cli/recover.py`, `headroom/providers/codex/recovery.py`, `tests/test_cli/test_wrap_codex.py`, and `tests/test_cli/test_recover_codex.py`. - Observed result: `122 passed in 10.08s`; Ruff reported `All checks passed!` and `5 files already formatted`. - Not tested: Launching a real Codex process or modifying a real user `CODEX_HOME`; these were intentionally excluded to protect live user state. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code where the behavior is hard to understand - [x] I have made corresponding documentation changes - [x] My changes generate no new warnings - [x] I have added tests that prove the fix is effective - [x] New and existing focused unit tests pass with my changes - [x] I have updated `CHANGELOG.md` if applicable ## Additional Notes The temporary-home behavior was introduced by #1507 in `ad9d086f43a664c4c2a19060b847f2e03ce4f6ad`. Related context: #730, #731, #961, #1034, #1050, #1349, #1853, #1889, #2103, and #2104. A temporary home that macOS or `TemporaryDirectory` already deleted cannot be reconstructed unless a retained `source-pinned/` copy exists. Recovery identifies genuine dangling SQLite paths, audits surviving durable history, and recovers any retained pinned source it can find. Prompt text without a rollout cannot reconstruct a full transcript. The unchecked changelog item is not applicable because this repository does not require a changelog entry for this fix. --------- Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Codex stamps every thread with the
model_providerit ran under and filters itshistory/projects menu by the currently active provider set. When Headroom rewrites
Codex's config to route through the custom
headroomprovider, threads created throughHeadroom are tagged
headroomwhile native threads keepopenai— so the two setsnever appear in the same menu. The visible symptom: enabling Headroom appears to "lose"
the entire native Codex history, and disabling it hides everything created while wrapped.
This reconciles the thread tags in Codex's SQLite store alongside the existing config
edits, so the menu stays whole across the proxy boundary in both directions:
openai -> headroomon enable/wrap,headroom -> openaion revert/unwrap. Only rowsmatching the source provider are touched, so third-party providers (e.g.
anthropic)are left alone. The provider key cannot be unified by config — Codex rejects naming a
custom provider
openai("reserved built-in provider IDs") — so retagging the store isthe only path. A DB-only retag is sufficient: resuming a retagged session still routes
completions through the active provider; the rollout
.jsonlfiles do not needrewriting.
Every operation is best-effort: a missing store, a missing
threadstable, or a corruptstore is logged and skipped, never raised, so install/uninstall and wrap/unwrap never
fail on account of the history menu. The store is WAL-mode, so the update succeeds even
while Codex is running; the short busy timeout only covers a transient checkpoint lock.
Closes #
Type of Change
Changes Made
headroom/providers/codex/threads.py: best-effort retag of Codex thread providertags across both known stores (
<codex_home>/sqlite/state_5.sqlitefor the GUI and<codex_home>/state_5.sqlitefor the CLI/TUI).retag_to_headroom/retag_to_nativewrap the directional helper.
codex_homeis passed in by callers (never re-derived fromPath.home()), so tests stay pointed at a temp dir.providers/codex/install.py:apply_provider_scopecallsretag_to_headroomafterwriting the provider block;
revert_provider_scopecallsretag_to_nativeafterstripping it.
cli/wrap.py:_inject_codex_provider_configcallsretag_to_headroom;unwrap_codexcallsretag_to_nativeonce the config restore reports arestored/cleaned/removedstatus.tests/test_provider_codex_threads.py(retag direction, threads-table no-op,missing/corrupt store best-effort) and a wrap/unwrap round-trip integration test in
tests/test_cli/test_wrap_codex.py.Testing
pytest)ruff check .)mypy headroom)Test Output
Real Behavior Proof
desktop app, which performs the identical SQLite retag. Connecting Codex to Headroom
hid ~140 native (
openai) threads from the history menu; runningUPDATE threads SET model_provider='headroom' WHERE model_provider='openai'on the livestore (
~/.codex/sqlite/state_5.sqlite) made the full menu reappear, and resuming aretagged session still routed completions through the active provider. This Python port
is a 1:1 of that logic, exercised by the unit + wrap/unwrap integration tests above.
provider rows untouched; the real
~/.codexstores were snapshotted before/after thetest run and were not mutated by the tests.
headroom wrap codexrun against a live Codex GUI in this CIenvironment (no Codex install here); covered instead by the integration test invoking
the real
wrap/unwrapClick commands against a temp$HOME.Review Readiness
Checklist
Screenshots (if applicable)
N/A — behavior is in Codex's own history menu; covered by the proof above.
Additional Notes
the restored menu.
TestInjectAvoidsDuplicateTopLevelKeystests are pre-existing onupstream/main and fail only because this environment runs Python 3.10 (no
tomllib);they are unrelated to this change.
🤖 Generated with Claude Code