mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
Installing Headroom for Codex via `headroom init` can make Codex Desktop
appear to lose its local chat/thread history. The data is never deleted
— Codex filters its sidebar/search by the active `model_provider`, and
the init path set `model_provider = "headroom"` without retagging
existing threads, so native `openai` threads disappeared from the menu.
The install (`headroom.providers.codex.install`) and wrap
(`headroom.cli.wrap`) paths already reconcile thread provider tags
across the proxy boundary (retag `openai -> headroom` on enable). The
init path — `_ensure_codex_provider` in `headroom/cli/init.py`, which is
exactly what the issue reproduces ("Headroom init proxy" provider,
`headroom-init-codex` hook) — was the one place that injected the
provider without retagging. This wires the same reconciliation into the
init path. The revert direction is already handled by `headroom unwrap
codex`.
Closes #961
## 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/cli/init.py`: `_ensure_codex_provider` now calls
`retag_to_headroom(path.parent)` after writing the init provider block,
so existing native threads stay visible under the active `headroom`
provider. Third-party providers (e.g. `anthropic`) are left untouched
(existing `retag_thread_providers` behaviour).
- `tests/test_cli/test_init_cli.py`: regression test seeding a Codex
Desktop `state_5.sqlite` and asserting `_ensure_codex_provider` retags
`openai -> headroom` while leaving other providers alone.
- `CHANGELOG.md`: Unreleased → Bug Fixes entry.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ uv run pytest tests/test_cli/test_init_cli.py tests/test_provider_codex_threads.py tests/test_provider_codex_install.py -q
84 passed in 0.84s
$ uv run ruff check headroom/cli/init.py tests/test_cli/test_init_cli.py
All checks passed!
$ uv run mypy headroom/cli/init.py
Success: no issues found in 1 source file
```
#### RED → GREEN proof
RED — new test with the prod fix (the `retag_to_headroom` call)
reverted:
```text
E AssertionError: existing openai threads not retagged: {'anthropic': 1, 'openai': 2}
1 failed in 0.44s
```
GREEN — with the fix applied:
```text
tests/test_cli/test_init_cli.py::test_init_codex_provider_retags_existing_threads
1 passed in 0.36s
```
## Real Behavior Proof
- Environment: Linux, Python 3.13, headroom @ this branch.
- Exact command / steps: seed a Codex Desktop store
(`<codex_home>/sqlite/state_5.sqlite`) with native threads, then run the
init provider injection:
```text
before init: {'anthropic': 1, 'openai': 2}
after init: {'anthropic': 1, 'headroom': 2}
config model_provider line: ['model_provider = "headroom"',
'[model_providers.headroom]']
```
- Observed result: after init, the two `openai` threads are retagged to
`headroom` (so they stay visible under the now-active provider), while
the `anthropic` thread is left untouched. Before the fix they stayed
`openai` and were filtered out of Codex Desktop's menu.
- Not tested: the live Codex Desktop GUI itself (proprietary, no
sandbox); the filtering behaviour is the documented `thread/list`
provider filter described in the issue, and the store-level retag that
makes history visible is covered above and by the unit test.
## 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
- [x] 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
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
Scope is limited to the init provider path; the install and wrap paths
already perform this reconciliation. Screenshots N/A (no UI change on
Headroom's side).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
This commit is contained in:
parent
feedead077
commit
e6bbc40b11
3 changed files with 50 additions and 0 deletions
|
|
@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* **codex:** retag existing Codex threads when `headroom init` injects the `headroom` provider, so Codex Desktop history stays visible. Codex filters its sidebar/search by the active `model_provider`; the init path set `model_provider = "headroom"` without retagging, so existing native `openai` threads disappeared from the menu (data was never deleted, only hidden). `_ensure_codex_provider` now reconciles thread tags openai→headroom, matching what the install and `wrap` paths already do; `headroom unwrap codex` handles the revert direction ([#961](https://github.com/chopratejas/headroom/issues/961)).
|
||||
* **install:** stop duplicating the container ENTRYPOINT in the `persistent-docker` runtime command. The published image already runs `headroom proxy` as its ENTRYPOINT, but `build_runtime_command` re-added `headroom proxy` after the image name, so the container ran `headroom proxy headroom proxy --host 0.0.0.0 …` and Click aborted with "Got unexpected extra arguments (headroom proxy)" — the deployment never became ready and rollback left nothing running. The runtime command now appends only the proxy flags ([#833](https://github.com/chopratejas/headroom/issues/833)).
|
||||
* **code:** keep Python `from __future__` imports before executable code during AST compression and validate compressed Python with `compile(..., "exec")` so compile-time syntax rules are enforced ([#1233](https://github.com/chopratejas/headroom/issues/1233)).
|
||||
* **proxy:** report real input tokens on the streaming `message_start` event for LiteLLM/Bedrock-backed requests. LiteLLM streaming never surfaces prompt tokens mid-stream, so `message_start.usage.input_tokens` was always `0`; Anthropic clients (e.g. Claude Code) read input-token metrics from that event, underreporting token usage by ~99% in OTel/CloudWatch dashboards. The Bedrock streamer now backfills `input_tokens` with the count Headroom actually sent upstream when the backend leaves it unset, preserving any non-zero value the backend genuinely reports ([#1132](https://github.com/chopratejas/headroom/issues/1132)).
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ from headroom.install.state import load_manifest, save_manifest
|
|||
from headroom.install.supervisors import start_supervisor
|
||||
from headroom.providers.claude import TOOL_SEARCH_DEFAULT, TOOL_SEARCH_ENV
|
||||
from headroom.providers.codex.install import codex_uses_chatgpt_auth
|
||||
from headroom.providers.codex.threads import retag_to_headroom
|
||||
|
||||
from .main import main
|
||||
|
||||
|
|
@ -333,6 +334,12 @@ def _ensure_codex_provider(path: Path, port: int) -> None:
|
|||
)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8")
|
||||
# Codex filters its history menu by the active model_provider, so existing
|
||||
# native threads vanish once we switch to "headroom". Retag them to match the
|
||||
# active provider so the history stays whole (#961), mirroring the install
|
||||
# (providers.codex.install) and wrap (cli.wrap) paths. The revert direction is
|
||||
# handled by `headroom unwrap codex`.
|
||||
retag_to_headroom(path.parent)
|
||||
|
||||
|
||||
def _codex_feature_block() -> str:
|
||||
|
|
|
|||
|
|
@ -1404,6 +1404,48 @@ def test_init_codex_writes_openai_base_url(monkeypatch, tmp_path: Path) -> None:
|
|||
)
|
||||
|
||||
|
||||
def test_init_codex_provider_retags_existing_threads(monkeypatch, tmp_path: Path) -> None:
|
||||
"""`headroom init` injects `model_provider = "headroom"` for Codex, which
|
||||
Codex Desktop filters its history menu by. Without retagging, existing native
|
||||
`openai` threads vanish from the sidebar/search (#961). `_ensure_codex_provider`
|
||||
must retag existing threads openai->headroom so the history stays visible —
|
||||
the same reconciliation the install and wrap paths already perform."""
|
||||
import sqlite3
|
||||
|
||||
init_cli, _ = _load_init_module(monkeypatch)
|
||||
|
||||
codex_home = tmp_path / ".codex"
|
||||
config_path = codex_home / "config.toml"
|
||||
# Codex Desktop reads <codex_home>/sqlite/state_5.sqlite.
|
||||
db = codex_home / "sqlite" / "state_5.sqlite"
|
||||
db.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(db))
|
||||
try:
|
||||
conn.execute("CREATE TABLE threads (id TEXT PRIMARY KEY, model_provider TEXT NOT NULL)")
|
||||
conn.executemany(
|
||||
"INSERT INTO threads (id, model_provider) VALUES (?, ?)",
|
||||
[("t1", "openai"), ("t2", "openai"), ("t3", "anthropic")],
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
init_cli._ensure_codex_provider(config_path, 8787)
|
||||
|
||||
conn = sqlite3.connect(str(db))
|
||||
try:
|
||||
counts = dict(
|
||||
conn.execute("SELECT model_provider, COUNT(*) FROM threads GROUP BY model_provider")
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
# Native threads now live under the active headroom provider (stay visible);
|
||||
# third-party providers are left untouched.
|
||||
assert counts.get("headroom") == 2, f"existing openai threads not retagged: {counts}"
|
||||
assert counts.get("openai", 0) == 0, f"openai threads still hidden: {counts}"
|
||||
assert counts.get("anthropic") == 1, f"third-party provider must be left alone: {counts}"
|
||||
|
||||
|
||||
def test_init_codex_strip_removes_openai_base_url(monkeypatch, tmp_path: Path) -> None:
|
||||
"""_strip_codex_init_block must remove both the managed block and any orphaned
|
||||
openai_base_url lines left by a crashed or partial init."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue