fix(codex): retag thread providers so history menu stays whole across the proxy boundary (#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>
2026-06-16 22:13:21 +02:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sqlite3
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
fix(codex): discover updated Codex state stores (#1889)
## Description
Codex Desktop can move its local thread database to a later
`state_<n>.sqlite` file after an app update. Headroom already retags
Codex thread providers when it enables or disables the `headroom`
provider, but the helper only looked at the v148 `state_5.sqlite`
locations. When Codex starts reading a newer state store, native
`openai` chats stay in that newer database while Headroom switches the
active provider to `headroom`, so Codex filters those chats out of the
history menu.
This discovers numeric Codex state stores in the two existing Codex home
locations, then applies the same best-effort retagging to every
discovered store. Legacy `state_5.sqlite` behavior stays intact,
third-party providers remain untouched, and corrupt or
schema-incompatible stores are skipped without breaking install, init,
wrap, or unwrap. Closes #1853.
## 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
- `headroom/providers/codex/threads.py`: discover direct numeric
`state_<n>.sqlite` stores under `<codex_home>/sqlite` and
`<codex_home>`, preserving deterministic ordering and existing
best-effort retag behavior.
- `tests/test_provider_codex_threads.py`: cover updated Codex
state-store versions, multi-store retagging, adjacent non-store
boundaries, corrupt and OS-error continuation, and the existing legacy
`state_5.sqlite` path.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_provider_codex_threads.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/providers/codex/threads.py
tests/test_provider_codex_threads.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_provider_codex_threads.py -q
======================== 10 passed, 1 warning in 0.32s ========================
uv run ruff check headroom/providers/codex/threads.py tests/test_provider_codex_threads.py
All checks passed!
```
## Real Behavior Proof
- Environment: local SQLite fixtures.
- Exact command / steps: Seed a Codex home with
`<codex_home>/sqlite/state_6.sqlite` containing native `openai` thread
rows, call `retag_to_headroom(codex_home)`, and inspect the
`threads.model_provider` counts.
- Observed result: the updated state store is discovered and matching
rows move to `headroom`; third-party provider rows remain unchanged. The
same helper still retags legacy `state_5.sqlite` stores and skips
corrupt, inaccessible, or missing stores without raising.
- Not tested: live Codex Desktop UI after an update; the proof exercises
the same SQLite provider tags that Codex filters its history menu by.
## 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
## Additional Notes
Documentation and changelog updates are N/A for this narrow repair to
existing Codex history retagging behavior. Install, init, wrap, and
unwrap keep using the shared provider helper without new call-site
branches.
2026-07-08 18:21:21 -04:00
|
|
|
import pytest
|
|
|
|
|
|
fix(codex): retag thread providers so history menu stays whole across the proxy boundary (#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>
2026-06-16 22:13:21 +02:00
|
|
|
from headroom.providers.codex import threads
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed(path: Path, rows: list[tuple[str, str]]) -> None:
|
|
|
|
|
conn = sqlite3.connect(str(path))
|
|
|
|
|
try:
|
|
|
|
|
conn.execute("CREATE TABLE threads (id TEXT PRIMARY KEY, model_provider TEXT NOT NULL)")
|
|
|
|
|
conn.executemany("INSERT INTO threads (id, model_provider) VALUES (?, ?)", rows)
|
|
|
|
|
conn.commit()
|
|
|
|
|
finally:
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _count(path: Path, provider: str) -> int:
|
|
|
|
|
conn = sqlite3.connect(str(path))
|
|
|
|
|
try:
|
|
|
|
|
(n,) = conn.execute(
|
|
|
|
|
"SELECT COUNT(*) FROM threads WHERE model_provider = ?", (provider,)
|
|
|
|
|
).fetchone()
|
|
|
|
|
return n
|
|
|
|
|
finally:
|
|
|
|
|
conn.close()
|
|
|
|
|
|
|
|
|
|
|
fix(codex): discover updated Codex state stores (#1889)
## Description
Codex Desktop can move its local thread database to a later
`state_<n>.sqlite` file after an app update. Headroom already retags
Codex thread providers when it enables or disables the `headroom`
provider, but the helper only looked at the v148 `state_5.sqlite`
locations. When Codex starts reading a newer state store, native
`openai` chats stay in that newer database while Headroom switches the
active provider to `headroom`, so Codex filters those chats out of the
history menu.
This discovers numeric Codex state stores in the two existing Codex home
locations, then applies the same best-effort retagging to every
discovered store. Legacy `state_5.sqlite` behavior stays intact,
third-party providers remain untouched, and corrupt or
schema-incompatible stores are skipped without breaking install, init,
wrap, or unwrap. Closes #1853.
## 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
- `headroom/providers/codex/threads.py`: discover direct numeric
`state_<n>.sqlite` stores under `<codex_home>/sqlite` and
`<codex_home>`, preserving deterministic ordering and existing
best-effort retag behavior.
- `tests/test_provider_codex_threads.py`: cover updated Codex
state-store versions, multi-store retagging, adjacent non-store
boundaries, corrupt and OS-error continuation, and the existing legacy
`state_5.sqlite` path.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_provider_codex_threads.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/providers/codex/threads.py
tests/test_provider_codex_threads.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_provider_codex_threads.py -q
======================== 10 passed, 1 warning in 0.32s ========================
uv run ruff check headroom/providers/codex/threads.py tests/test_provider_codex_threads.py
All checks passed!
```
## Real Behavior Proof
- Environment: local SQLite fixtures.
- Exact command / steps: Seed a Codex home with
`<codex_home>/sqlite/state_6.sqlite` containing native `openai` thread
rows, call `retag_to_headroom(codex_home)`, and inspect the
`threads.model_provider` counts.
- Observed result: the updated state store is discovered and matching
rows move to `headroom`; third-party provider rows remain unchanged. The
same helper still retags legacy `state_5.sqlite` stores and skips
corrupt, inaccessible, or missing stores without raising.
- Not tested: live Codex Desktop UI after an update; the proof exercises
the same SQLite provider tags that Codex filters its history menu by.
## 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
## Additional Notes
Documentation and changelog updates are N/A for this narrow repair to
existing Codex history retagging behavior. Install, init, wrap, and
unwrap keep using the shared provider helper without new call-site
branches.
2026-07-08 18:21:21 -04:00
|
|
|
def test_codex_state_db_paths_discovers_numeric_files_in_directory_order(tmp_path: Path) -> None:
|
|
|
|
|
sqlite_home = tmp_path / "sqlite"
|
|
|
|
|
sqlite_home.mkdir()
|
|
|
|
|
nested = sqlite_home / "nested"
|
|
|
|
|
nested.mkdir()
|
|
|
|
|
|
|
|
|
|
_seed(sqlite_home / "state_10.sqlite", [("a", "openai")])
|
|
|
|
|
_seed(sqlite_home / "state_2.sqlite", [("b", "openai")])
|
|
|
|
|
_seed(sqlite_home / "state_backup.sqlite", [("c", "openai")])
|
|
|
|
|
(sqlite_home / "state_6.sqlite-wal").write_text("wal", encoding="utf-8")
|
|
|
|
|
_seed(nested / "state_7.sqlite", [("d", "openai")])
|
|
|
|
|
_seed(tmp_path / "state_9.sqlite", [("e", "openai")])
|
|
|
|
|
_seed(tmp_path / "state_1.sqlite", [("f", "openai")])
|
|
|
|
|
(tmp_path / "other.db").write_text("other", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
assert threads._codex_state_db_paths(tmp_path) == [
|
|
|
|
|
sqlite_home / "state_2.sqlite",
|
|
|
|
|
sqlite_home / "state_10.sqlite",
|
|
|
|
|
tmp_path / "state_1.sqlite",
|
|
|
|
|
tmp_path / "state_9.sqlite",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_retag_thread_providers_discovers_later_state_store_and_skips_adjacent_files(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
) -> None:
|
|
|
|
|
sqlite_home = tmp_path / "sqlite"
|
|
|
|
|
sqlite_home.mkdir()
|
|
|
|
|
nested = sqlite_home / "nested"
|
|
|
|
|
nested.mkdir()
|
|
|
|
|
|
|
|
|
|
later = sqlite_home / "state_6.sqlite"
|
|
|
|
|
legacy = tmp_path / "state_5.sqlite"
|
|
|
|
|
backup = sqlite_home / "state_backup.sqlite"
|
|
|
|
|
sidecar = sqlite_home / "state_6.sqlite-wal"
|
|
|
|
|
nested_store = nested / "state_7.sqlite"
|
|
|
|
|
|
|
|
|
|
_seed(later, [("a", "openai"), ("b", "openai"), ("c", "anthropic")])
|
|
|
|
|
_seed(legacy, [("d", "openai"), ("e", "headroom")])
|
|
|
|
|
_seed(backup, [("f", "openai")])
|
|
|
|
|
sidecar.write_text("wal", encoding="utf-8")
|
|
|
|
|
_seed(nested_store, [("g", "openai")])
|
|
|
|
|
|
|
|
|
|
threads.retag_to_headroom(tmp_path)
|
|
|
|
|
|
|
|
|
|
assert _count(later, "headroom") == 2
|
|
|
|
|
assert _count(later, "openai") == 0
|
|
|
|
|
assert _count(later, "anthropic") == 1
|
|
|
|
|
assert _count(legacy, "headroom") == 2
|
|
|
|
|
assert _count(legacy, "openai") == 0
|
|
|
|
|
assert _count(backup, "openai") == 1
|
|
|
|
|
assert _count(backup, "headroom") == 0
|
|
|
|
|
assert _count(nested_store, "openai") == 1
|
|
|
|
|
assert _count(nested_store, "headroom") == 0
|
|
|
|
|
|
|
|
|
|
|
fix(codex): retag thread providers so history menu stays whole across the proxy boundary (#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>
2026-06-16 22:13:21 +02:00
|
|
|
def test_retag_one_moves_only_matching_provider(tmp_path: Path) -> None:
|
|
|
|
|
db = tmp_path / "state_5.sqlite"
|
|
|
|
|
_seed(db, [("a", "openai"), ("b", "openai"), ("c", "headroom"), ("d", "anthropic")])
|
|
|
|
|
|
|
|
|
|
moved = threads._retag_one(db, frm="openai", to="headroom")
|
|
|
|
|
assert moved == 2
|
|
|
|
|
assert _count(db, "openai") == 0
|
|
|
|
|
assert _count(db, "headroom") == 3
|
|
|
|
|
# Third-party providers are left alone.
|
|
|
|
|
assert _count(db, "anthropic") == 1
|
|
|
|
|
|
|
|
|
|
back = threads._retag_one(db, frm="headroom", to="openai")
|
|
|
|
|
assert back == 3
|
|
|
|
|
assert _count(db, "headroom") == 0
|
|
|
|
|
assert _count(db, "openai") == 3
|
|
|
|
|
assert _count(db, "anthropic") == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_retag_one_noop_without_threads_table(tmp_path: Path) -> None:
|
|
|
|
|
db = tmp_path / "state_5.sqlite"
|
|
|
|
|
sqlite3.connect(str(db)).close() # empty schema, no threads table
|
|
|
|
|
assert threads._retag_one(db, frm="openai", to="headroom") == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_retag_thread_providers_silent_when_no_store(tmp_path: Path) -> None:
|
|
|
|
|
# No stores exist under this codex_home: must not raise.
|
|
|
|
|
threads.retag_thread_providers(tmp_path, frm="openai", to="headroom")
|
|
|
|
|
|
|
|
|
|
|
fix(codex): discover updated Codex state stores (#1889)
## Description
Codex Desktop can move its local thread database to a later
`state_<n>.sqlite` file after an app update. Headroom already retags
Codex thread providers when it enables or disables the `headroom`
provider, but the helper only looked at the v148 `state_5.sqlite`
locations. When Codex starts reading a newer state store, native
`openai` chats stay in that newer database while Headroom switches the
active provider to `headroom`, so Codex filters those chats out of the
history menu.
This discovers numeric Codex state stores in the two existing Codex home
locations, then applies the same best-effort retagging to every
discovered store. Legacy `state_5.sqlite` behavior stays intact,
third-party providers remain untouched, and corrupt or
schema-incompatible stores are skipped without breaking install, init,
wrap, or unwrap. Closes #1853.
## 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
- `headroom/providers/codex/threads.py`: discover direct numeric
`state_<n>.sqlite` stores under `<codex_home>/sqlite` and
`<codex_home>`, preserving deterministic ordering and existing
best-effort retag behavior.
- `tests/test_provider_codex_threads.py`: cover updated Codex
state-store versions, multi-store retagging, adjacent non-store
boundaries, corrupt and OS-error continuation, and the existing legacy
`state_5.sqlite` path.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_provider_codex_threads.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/providers/codex/threads.py
tests/test_provider_codex_threads.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_provider_codex_threads.py -q
======================== 10 passed, 1 warning in 0.32s ========================
uv run ruff check headroom/providers/codex/threads.py tests/test_provider_codex_threads.py
All checks passed!
```
## Real Behavior Proof
- Environment: local SQLite fixtures.
- Exact command / steps: Seed a Codex home with
`<codex_home>/sqlite/state_6.sqlite` containing native `openai` thread
rows, call `retag_to_headroom(codex_home)`, and inspect the
`threads.model_provider` counts.
- Observed result: the updated state store is discovered and matching
rows move to `headroom`; third-party provider rows remain unchanged. The
same helper still retags legacy `state_5.sqlite` stores and skips
corrupt, inaccessible, or missing stores without raising.
- Not tested: live Codex Desktop UI after an update; the proof exercises
the same SQLite provider tags that Codex filters its history menu by.
## 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
## Additional Notes
Documentation and changelog updates are N/A for this narrow repair to
existing Codex history retagging behavior. Install, init, wrap, and
unwrap keep using the shared provider helper without new call-site
branches.
2026-07-08 18:21:21 -04:00
|
|
|
def test_retag_thread_providers_skips_unreadable_store_directory(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
|
|
|
) -> None:
|
|
|
|
|
sqlite_home = tmp_path / "sqlite"
|
|
|
|
|
sqlite_home.mkdir()
|
|
|
|
|
_seed(sqlite_home / "state_6.sqlite", [("a", "openai")])
|
|
|
|
|
fallback = tmp_path / "state_5.sqlite"
|
|
|
|
|
_seed(fallback, [("b", "openai")])
|
|
|
|
|
original_iterdir = Path.iterdir
|
|
|
|
|
|
|
|
|
|
def fail_for_sqlite(path: Path):
|
|
|
|
|
if path == sqlite_home:
|
|
|
|
|
raise OSError("permission denied")
|
|
|
|
|
return original_iterdir(path)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(Path, "iterdir", fail_for_sqlite)
|
|
|
|
|
|
|
|
|
|
threads.retag_to_headroom(tmp_path)
|
|
|
|
|
|
|
|
|
|
assert _count(fallback, "headroom") == 1
|
|
|
|
|
assert _count(sqlite_home / "state_6.sqlite", "openai") == 1
|
|
|
|
|
|
|
|
|
|
|
fix(codex): retag thread providers so history menu stays whole across the proxy boundary (#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>
2026-06-16 22:13:21 +02:00
|
|
|
def test_retag_thread_providers_best_effort_on_corrupt_store(tmp_path: Path) -> None:
|
|
|
|
|
bad = tmp_path / "state_5.sqlite"
|
|
|
|
|
bad.write_text("not a sqlite database", encoding="utf-8")
|
|
|
|
|
# A corrupt store is logged and skipped, never raised.
|
|
|
|
|
threads.retag_thread_providers(tmp_path, frm="openai", to="headroom")
|
|
|
|
|
|
|
|
|
|
|
fix(codex): discover updated Codex state stores (#1889)
## Description
Codex Desktop can move its local thread database to a later
`state_<n>.sqlite` file after an app update. Headroom already retags
Codex thread providers when it enables or disables the `headroom`
provider, but the helper only looked at the v148 `state_5.sqlite`
locations. When Codex starts reading a newer state store, native
`openai` chats stay in that newer database while Headroom switches the
active provider to `headroom`, so Codex filters those chats out of the
history menu.
This discovers numeric Codex state stores in the two existing Codex home
locations, then applies the same best-effort retagging to every
discovered store. Legacy `state_5.sqlite` behavior stays intact,
third-party providers remain untouched, and corrupt or
schema-incompatible stores are skipped without breaking install, init,
wrap, or unwrap. Closes #1853.
## 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
- `headroom/providers/codex/threads.py`: discover direct numeric
`state_<n>.sqlite` stores under `<codex_home>/sqlite` and
`<codex_home>`, preserving deterministic ordering and existing
best-effort retag behavior.
- `tests/test_provider_codex_threads.py`: cover updated Codex
state-store versions, multi-store retagging, adjacent non-store
boundaries, corrupt and OS-error continuation, and the existing legacy
`state_5.sqlite` path.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_provider_codex_threads.py -q`)
- [x] Linting passes (`uv run ruff check
headroom/providers/codex/threads.py
tests/test_provider_codex_threads.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_provider_codex_threads.py -q
======================== 10 passed, 1 warning in 0.32s ========================
uv run ruff check headroom/providers/codex/threads.py tests/test_provider_codex_threads.py
All checks passed!
```
## Real Behavior Proof
- Environment: local SQLite fixtures.
- Exact command / steps: Seed a Codex home with
`<codex_home>/sqlite/state_6.sqlite` containing native `openai` thread
rows, call `retag_to_headroom(codex_home)`, and inspect the
`threads.model_provider` counts.
- Observed result: the updated state store is discovered and matching
rows move to `headroom`; third-party provider rows remain unchanged. The
same helper still retags legacy `state_5.sqlite` stores and skips
corrupt, inaccessible, or missing stores without raising.
- Not tested: live Codex Desktop UI after an update; the proof exercises
the same SQLite provider tags that Codex filters its history menu by.
## 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
## Additional Notes
Documentation and changelog updates are N/A for this narrow repair to
existing Codex history retagging behavior. Install, init, wrap, and
unwrap keep using the shared provider helper without new call-site
branches.
2026-07-08 18:21:21 -04:00
|
|
|
def test_retag_thread_providers_skips_corrupt_discovered_store_and_continues(
|
|
|
|
|
tmp_path: Path,
|
|
|
|
|
) -> None:
|
|
|
|
|
sqlite_home = tmp_path / "sqlite"
|
|
|
|
|
sqlite_home.mkdir()
|
|
|
|
|
|
|
|
|
|
bad = sqlite_home / "state_6.sqlite"
|
|
|
|
|
later = sqlite_home / "state_7.sqlite"
|
|
|
|
|
bad.write_text("not a sqlite database", encoding="utf-8")
|
|
|
|
|
_seed(later, [("a", "openai"), ("b", "anthropic")])
|
|
|
|
|
|
|
|
|
|
threads.retag_to_headroom(tmp_path)
|
|
|
|
|
|
|
|
|
|
assert _count(later, "headroom") == 1
|
|
|
|
|
assert _count(later, "openai") == 0
|
|
|
|
|
assert _count(later, "anthropic") == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_retag_thread_providers_skips_os_error_store_and_continues(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
|
|
|
) -> None:
|
|
|
|
|
sqlite_home = tmp_path / "sqlite"
|
|
|
|
|
sqlite_home.mkdir()
|
|
|
|
|
locked = sqlite_home / "state_6.sqlite"
|
|
|
|
|
later = sqlite_home / "state_7.sqlite"
|
|
|
|
|
_seed(locked, [("a", "openai")])
|
|
|
|
|
_seed(later, [("b", "openai"), ("c", "anthropic")])
|
|
|
|
|
original_retag_one = threads._retag_one
|
|
|
|
|
|
|
|
|
|
def fail_for_locked(path: Path, *, frm: str, to: str) -> int:
|
|
|
|
|
if path == locked:
|
|
|
|
|
raise OSError("locked")
|
|
|
|
|
return original_retag_one(path, frm=frm, to=to)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(threads, "_retag_one", fail_for_locked)
|
|
|
|
|
|
|
|
|
|
threads.retag_to_headroom(tmp_path)
|
|
|
|
|
|
|
|
|
|
assert _count(locked, "openai") == 1
|
|
|
|
|
assert _count(later, "headroom") == 1
|
|
|
|
|
assert _count(later, "anthropic") == 1
|
|
|
|
|
|
|
|
|
|
|
fix(codex): retag thread providers so history menu stays whole across the proxy boundary (#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>
2026-06-16 22:13:21 +02:00
|
|
|
def test_enable_disable_wrappers_retag_expected_direction(tmp_path: Path) -> None:
|
|
|
|
|
db = tmp_path / "state_5.sqlite"
|
|
|
|
|
_seed(db, [("a", "openai"), ("b", "headroom")])
|
|
|
|
|
|
|
|
|
|
threads.retag_to_headroom(tmp_path)
|
|
|
|
|
assert _count(db, "headroom") == 2
|
|
|
|
|
assert _count(db, "openai") == 0
|
|
|
|
|
|
|
|
|
|
threads.retag_to_native(tmp_path)
|
|
|
|
|
assert _count(db, "openai") == 2
|
|
|
|
|
assert _count(db, "headroom") == 0
|