headroom/tests/test_provider_codex_threads.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

201 lines
6.7 KiB
Python
Raw Permalink Normal View History

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