mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description
`tokensave` was a **downloaded third-party Rust binary**
(`aovestdipaperino/tokensave`) that `headroom wrap` registered as a
code-graph MCP server. This removes it entirely and standardises on
**Serena** as the code-memory MCP — which was already the default in
`wrap`. Serena runs on demand via `uvx`, so Headroom no longer downloads
or executes a binary of its own for code memory.
The change is a *removal + safe transition*, not a behaviour flip:
Serena was already the default, so existing users move over
automatically. This PR also folds in a small README repositioning
(Headroom = the proxy; Serena is the recommended companion; RTK/lean-ctx
are third-party tools we don't control), since it's the same
tooling-stack story.
Closes #
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [x] Code refactoring (no functional changes)
## Changes Made
- **Removed the external tool:** `headroom/graph/tokensave_installer.py`
(the download-and-execute path), `build_tokensave_spec`, and the
`_ensure_tokensave_binary` / `_index_tokensave_project` /
`_setup_tokensave_mcp` helpers; dropped the dead `_setup_code_graph`.
- **`--code-memory`** now offers `serena` (default) or `none` — the
`tokensave` choice is gone. The strands `HeadroomBundle` uses Serena as
its (default-on) code-memory MCP.
- **Tombstone / transition (ledger-verified):** `headroom wrap` **and**
`headroom unwrap` remove a previously Headroom-installed `tokensave` MCP
entry so upgrading users stop launching it, and print that the leftover
`~/.local/bin/tokensave` binary and `.tokensave/` folders are safe to
delete. A user-managed `tokensave` entry is left untouched. Mirrors the
existing `codebase-memory-mcp` retirement.
- **Graceful for existing users:** `HEADROOM_CODE_MEMORY=tokensave` and
`--no-tokensave` resolve to Serena instead of erroring; `--no-serena`
now means "no code memory". **No state migration needed** — both tools'
indexes are regenerable caches of the source, so Serena simply
re-indexes.
- **Docs/README:** replaced the "tokensave binary trust model" section
with a Serena note + an "Upgrading from tokensave?" callout; retired the
RTK "first-class part of our stack" framing.
- **Tests:** deleted the tokensave-only test files
(`test_graph_tokensave.py`, `test_cli/test_tokensave_helpers.py`,
`test_cli/test_tokensave_setup.py`), rewrote `test_wrap_code_memory.py`
for the new resolver/dispatch, fixed a codex test that patched a removed
symbol.
Net: **+102 / −1187 lines.**
## Testing
- [x] Unit tests pass (`pytest`) — targeted to the affected areas
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ ruff check headroom/cli/wrap.py headroom/mcp_registry/ headroom/integrations/strands/ tests/test_wrap_code_memory.py tests/test_cli/conftest.py tests/test_cli/test_wrap_codex.py
All checks passed!
$ mypy headroom/cli/wrap.py headroom/mcp_registry headroom/integrations/strands/bundle.py
Success: no issues found in 12 source files
$ pytest tests/test_wrap_code_memory.py tests/test_cli/test_wrap_codex.py \
tests/test_cli/test_wrap_claude_vertex_proxy_env.py \
tests/test_cli/test_wrap_claude_finally_unbound.py -q
======================== 120 passed in 97.86s (0:01:37) ========================
$ pytest tests/test_cli --collect-only -q
========================= 713 tests collected in 1.82s ========================= # no import errors after symbol removal
```
## Real Behavior Proof
- **Environment:** macOS (darwin), Python 3.12.6, local `.venv`, on
branch `tejas/remove-tokensave`.
- **Exact command / steps:**
- `python -c "from headroom.integrations.strands.bundle import
HeadroomBundle; b=HeadroomBundle(enable_headroom_mcp=False,
enable_serena_mcp=False); print(len(b.tools))"` → confirms the module
imports after `build_tokensave_spec` removal (the import that my change
would otherwise break).
- CliRunner-driven `wrap codex --prepare-only` (in `test_wrap_codex.py`)
writes `[mcp_servers.serena]` (with `command = "uvx"`, `"--context",
"codex"`) to the codex config and **no** tokensave entry.
- `_resolve_code_memory` unit tests confirm: default → `serena`;
`HEADROOM_CODE_MEMORY=tokensave` → `serena`; `--no-serena` → `none`;
`--code-memory bogus` → `ClickException`.
- **Observed result:** import OK (`tools: 0`); Serena registered,
tokensave absent; resolver behaves as above; 120/120 tests pass.
- **Not tested:** a live `headroom wrap` against a real agent on a
machine with a *previously-installed* tokensave MCP entry — the
tombstone-removal path is covered by unit tests with a fake
registrar/ledger, not an end-to-end run.
## 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 did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title
## Additional Notes
- `--no-tokensave` / `--serena` / `--no-serena` are retained as hidden,
deprecated flags (no-ops or mapped) so existing scripts don't break.
- `--code-graph` is unchanged — it's the proxy's live file-watcher flag
and was never the tokensave MCP; only the dead tokensave hook behind it
was removed.
- `CHANGELOG.md` intentionally left untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
116 lines
4.7 KiB
Python
116 lines
4.7 KiB
Python
"""Code-memory MCP is selectable via --code-memory (default serena).
|
|
|
|
Covers the resolver precedence (selector > deprecated flags > default), the
|
|
graceful retirement of the removed ``tokensave`` option, the orchestrator
|
|
dispatch for each selection, and that --code-memory is exposed on the
|
|
code-memory-capable subcommands (claude/codex/grok) but not others.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from unittest.mock import patch
|
|
|
|
import click
|
|
from click.testing import CliRunner
|
|
|
|
from headroom.cli import wrap
|
|
|
|
|
|
def _clean_env() -> dict[str, str]:
|
|
env = dict(os.environ)
|
|
env.pop("HEADROOM_CODE_MEMORY", None)
|
|
return env
|
|
|
|
|
|
def test_default_is_serena() -> None:
|
|
with patch.dict(os.environ, _clean_env(), clear=True):
|
|
assert wrap._resolve_code_memory({}) == wrap._CODE_MEMORY_SERENA
|
|
|
|
|
|
def test_selector_env_wins() -> None:
|
|
for val in (wrap._CODE_MEMORY_SERENA, wrap._CODE_MEMORY_NONE):
|
|
with patch.dict(os.environ, {"HEADROOM_CODE_MEMORY": val}):
|
|
# selector beats any legacy flag
|
|
assert wrap._resolve_code_memory({"serena": True, "no_serena": True}) == val
|
|
|
|
|
|
def test_deprecated_flags_map_into_selector() -> None:
|
|
with patch.dict(os.environ, _clean_env(), clear=True):
|
|
assert wrap._resolve_code_memory({"serena": True}) == wrap._CODE_MEMORY_SERENA
|
|
# tokensave is retired: --no-tokensave is now a no-op → default serena
|
|
assert wrap._resolve_code_memory({"no_tokensave": True}) == wrap._CODE_MEMORY_SERENA
|
|
# --no-serena means "no code memory" now that tokensave is gone
|
|
assert wrap._resolve_code_memory({"no_serena": True}) == wrap._CODE_MEMORY_NONE
|
|
|
|
|
|
def test_retired_tokensave_selector_maps_to_serena() -> None:
|
|
# An explicit HEADROOM_CODE_MEMORY=tokensave (or --code-memory tokensave from
|
|
# an old script) degrades gracefully to Serena instead of erroring.
|
|
with patch.dict(os.environ, {"HEADROOM_CODE_MEMORY": "tokensave"}):
|
|
assert wrap._resolve_code_memory({}) == wrap._CODE_MEMORY_SERENA
|
|
|
|
|
|
def test_serena_dashboard_disabled_flips_existing_config(tmp_path, monkeypatch) -> None:
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
cfg = tmp_path / ".serena" / "serena_config.yml"
|
|
cfg.parent.mkdir(parents=True)
|
|
cfg.write_text(
|
|
"web_dashboard: true\nweb_dashboard_open_on_launch: true\ngui_log_window: false\n"
|
|
)
|
|
wrap._ensure_serena_dashboard_disabled()
|
|
text = cfg.read_text()
|
|
assert "web_dashboard_open_on_launch: false" in text
|
|
assert "web_dashboard: true" in text # other keys preserved
|
|
|
|
|
|
def test_serena_dashboard_disabled_creates_config(tmp_path, monkeypatch) -> None:
|
|
monkeypatch.setenv("HOME", str(tmp_path))
|
|
wrap._ensure_serena_dashboard_disabled()
|
|
cfg = tmp_path / ".serena" / "serena_config.yml"
|
|
assert cfg.exists()
|
|
assert "web_dashboard_open_on_launch: false" in cfg.read_text()
|
|
|
|
|
|
def test_invalid_env_raises() -> None:
|
|
with patch.dict(os.environ, {"HEADROOM_CODE_MEMORY": "bogus"}):
|
|
try:
|
|
wrap._resolve_code_memory({})
|
|
except click.ClickException:
|
|
pass
|
|
else: # pragma: no cover
|
|
raise AssertionError("invalid HEADROOM_CODE_MEMORY should raise ClickException")
|
|
|
|
|
|
def _dispatch_calls(selection: str, extra: dict | None = None) -> list[str]:
|
|
"""Run the orchestrator with a given selection, recording which setup/disable
|
|
helpers fire (all mocked)."""
|
|
calls: list[str] = []
|
|
env = _clean_env()
|
|
env["HEADROOM_CODE_MEMORY"] = selection
|
|
with (
|
|
patch.dict(os.environ, env, clear=True),
|
|
patch.object(wrap, "_setup_serena_mcp", lambda *a, **k: calls.append("serena")),
|
|
patch.object(
|
|
wrap, "_disable_tokensave_mcp", lambda *a, **k: calls.append("disable_tokensave")
|
|
),
|
|
patch.object(wrap, "_disable_serena_mcp", lambda *a, **k: calls.append("disable_serena")),
|
|
):
|
|
wrap._setup_coding_compressor(object(), serena_context="claude-code", **(extra or {}))
|
|
return calls
|
|
|
|
|
|
def test_orchestrator_dispatch() -> None:
|
|
# A legacy tokensave entry is always retired first, then the selection applies.
|
|
assert _dispatch_calls(wrap._CODE_MEMORY_SERENA) == ["disable_tokensave", "serena"]
|
|
assert set(_dispatch_calls(wrap._CODE_MEMORY_NONE)) == {"disable_tokensave", "disable_serena"}
|
|
|
|
|
|
def test_code_memory_option_present_only_on_code_memory_agents() -> None:
|
|
runner = CliRunner()
|
|
for tool in ("claude", "codex", "grok"):
|
|
out = runner.invoke(wrap.wrap, [tool, "--help"]).output
|
|
assert "--code-memory" in out, f"--code-memory missing from `wrap {tool} --help`"
|
|
# aider does not register a code-memory MCP → no flag
|
|
out = runner.invoke(wrap.wrap, ["aider", "--help"]).output
|
|
assert "--code-memory" not in out
|