diff --git a/CHANGELOG.md b/CHANGELOG.md index b9fa7396b..a3e7cc53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)). diff --git a/headroom/cli/init.py b/headroom/cli/init.py index 59e241400..2c68f1faf 100644 --- a/headroom/cli/init.py +++ b/headroom/cli/init.py @@ -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: diff --git a/tests/test_cli/test_init_cli.py b/tests/test_cli/test_init_cli.py index f94d293fd..72cc7210f 100644 --- a/tests/test_cli/test_init_cli.py +++ b/tests/test_cli/test_init_cli.py @@ -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 /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."""