mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
10 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a5d8a7ace
|
fix(mcp): reap orphaned mcp serve on client death (#2226)
## Description `headroom mcp serve` processes survive after the launching MCP client (e.g. Claude Code) exits, get reparented to init/launchd (`ppid == 1`), and never terminate — piling up one pinned Python interpreter + tree-sitter grammars per dead session (observed 3+ simultaneously). An MCP stdio server is supposed to shut down on stdin EOF, but an abrupt client `SIGKILL` leaves the MCP SDK's blocking stdin-reader thread wedged, so `await self.server.run(...)` in `run_stdio()` never returns and the process orphans. Refs #2185 (its secondary "orphaned `mcp serve` pileup", left out of #2204's `Refs`-only Perl fix), #1761 (same symptom: "orphaned `headroom mcp serve` processes accumulate … even after quitting"). ## 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/ccr/mcp_server.py`: - Added `PARENT_DEATH_POLL_INTERVAL = 5.0` module constant. - Added `HeadroomMCPServer._await_parent_death(interval)`: captures the launch ppid and resolves once it changes. Watching for a *change* (not a hard `== 1`) is portable to Linux PID subreapers, which adopt the orphan with their own pid. - Reworked `run_stdio()` to run that watchdog concurrently with `server.run()`. On parent death it `os._exit(0)`s **from inside** the `stdio_server()` context manager — the wedged stdin reader would also hang the context-manager teardown and a cooperative `server.run` cancel, so a hard exit is the only reliable reaper. The normal stdin-EOF path is unchanged: `server.run` wins the race, the watchdog is cancelled, and the context manager unwinds cleanly. `tests/test_ccr_mcp_server.py`: 3 regression tests (below). `CHANGELOG.md`: entry under Unreleased → Fixed. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_server.py -q`) - [x] Linting passes (`uv run ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py`) - [x] Type checking passes (`uv run mypy headroom/ccr/mcp_server.py`) - [x] New tests added for new functionality - [x] Manual testing performed (see Real Behavior Proof) New tests: - `test_parent_death_watchdog_fires_when_reparented` — ppid change resolves the watchdog. - `test_parent_death_watchdog_stays_quiet_with_live_parent` — a stable ppid never trips it. - `test_run_stdio_reaps_process_on_parent_death` — on reparent, `run_stdio` cleans up and hits `os._exit(0)` even though the (stubbed) `server.run` never returns. ### Test Output ```text $ uv run pytest tests/test_ccr_mcp_server.py -q collected 21 items tests/test_ccr_mcp_server.py ..................... [100%] ============================== 21 passed in 0.57s ============================== $ uv run ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py All checks passed! $ uv run mypy headroom/ccr/mcp_server.py Success: no issues found in 1 source file ``` ## Real Behavior Proof - Environment: macOS 26.5 arm64, Python 3.14.6, headroom built from this branch via `uv sync --all-extras` (Rust extension compiled). No provider call. - Exact command / steps: launch a real `HeadroomMCPServer.run_stdio()` as a child of a throwaway parent, with stdin wired to a FIFO whose write end is held open by a separate process (so stdin **never** reaches EOF — this isolates the watchdog as the only possible reaper). Then `kill -9` the parent to reparent the server to `pid 1`, and watch. The watchdog poll interval is passed via `run_stdio(parent_death_poll_interval=…)` to A/B the exact same shipped code path: ```text ### interval=9999s (watchdog effectively OFF — reproduces the bug) ### ppid(pre-kill)=43438 -> STILL ALIVE after 8s (orphan lingers) ### interval=0.5s (watchdog ON — the fix) ### ppid(pre-kill)=43461 -> REAPED at ~2s ``` And with the default flow (`headroom mcp serve`, default 5s interval), the watchdog logs before the process exits: ```text headroom.ccr.mcp - INFO - Headroom MCP Server starting (proxy: http://127.0.0.1:8787) headroom.ccr.mcp - WARNING - parent process gone (ppid 41956 -> 1); shutting down MCP server ``` - Observed result: with the watchdog disabled the orphaned server lingers indefinitely (reproduces the reported pileup); with it enabled the orphan is reaped within one poll interval of the parent dying. - Not tested: Linux/systemd and Windows spawn paths (the change is POSIX-portable via ppid-change detection, but I only exercised macOS); the reporters' desktop-app menu-bar quit path. ## 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 - [x] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md ## Additional Notes - Deliberately `os._exit(0)`, not a cooperative shutdown: the failure mode is a wedged native stdin-reader thread, so both `server.run` cancellation and the `stdio_server` context-manager exit can block forever. Exiting from inside the context manager is the only path that reliably reaps the orphan; the normal EOF path never reaches it. - A Linux-only `prctl(PR_SET_PDEATHSIG)` fast-path could cut reap latency to ~0, but it is racy (must re-check `getppid()` after arming) and non-portable, so the portable poll is the primary mechanism. Happy to add prctl as a follow-up optimization if wanted. - Watchdog latency is bounded by `PARENT_DEATH_POLL_INTERVAL` (5s default); trivial to make env-configurable if a tighter bound is preferred. --- 🤖 This PR was created with [Claude Code](https://claude.com/claude-code) but checked by the author Co-authored-by: JerrettDavis <mxjerrett@gmail.com> |
||
|
|
931eed879d
|
fix(mcp): surface dead proxy state (#1786)
## Description When the configured Headroom proxy is down, the MCP server can still start cleanly and return successful-looking no-op compression or zeroed stats. That hides the real failure from the client and makes it look like Headroom is working while compression has stopped. This change makes proxy-backed MCP tool paths surface unreachable-proxy state explicitly instead of silently degrading. Closes #881 ## 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 - Detect unreachable configured proxy state before returning proxy-backed MCP tool results. - Report proxy-unreachable status for compression and stats instead of presenting no-op output as healthy. - Preserve local MCP behavior when proxy checking is disabled or a local-only tool path is intended. - Keep the short `/livez` health probe isolated from the shared proxy client used by retrieval and stats calls. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_server.py tests/test_provider_registry.py -q`) - [x] Linting passes (`uv run ruff check headroom/ccr/mcp_server.py headroom/providers/registry.py tests/test_ccr_mcp_server.py tests/test_provider_registry.py`; `uv run ruff format --check headroom/ccr/mcp_server.py headroom/providers/registry.py tests/test_ccr_mcp_server.py tests/test_provider_registry.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text collected 26 items tests\test_ccr_mcp_server.py ...s.......... [ 53%] tests\test_provider_registry.py ............ [100%] ======================== 25 passed, 1 skipped in 6.64s ======================== All checks passed! 4 files already formatted ``` ## Real Behavior Proof - Environment: Windows, focused local MCP tests. - Exact command / steps: Run `uv run pytest .tmp\headroom_t45_regression.py -q` in the base and head worktrees, then run `uv run pytest tests/test_ccr_mcp_server.py tests/test_provider_registry.py -q`, `uv run ruff check headroom/ccr/mcp_server.py headroom/providers/registry.py tests/test_ccr_mcp_server.py tests/test_provider_registry.py`, and `uv run ruff format --check headroom/ccr/mcp_server.py headroom/providers/registry.py tests/test_ccr_mcp_server.py tests/test_provider_registry.py` in the head worktree. - Observed result: `base: KeyError: 'proxy'` on the new proxy-unreachable assertions, `head: .tmp\headroom_t45_regression.py .... [100%]`, broader head suite `25 passed, 1 skipped in 6.64s`, and the proxy health probe regression preserved the shared proxy client used by retrieval and stats. - Not tested: The reporter's bundled macOS runtime, live Claude Desktop MCP logs, and the full test suite. ## 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 - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] 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 are left unchecked for now; the behavior change is an error-surfacing fix for existing MCP tools and Headroom generates changelog entries from conventional commits. |
||
|
|
9cbdba4dc1
|
fix(ccr): make expired retrieve misses terminal (#1781)
## Description Expired CCR hashes currently come back through `headroom_retrieve` as the same generic missing-content error used for typos and never-stored hashes. That leaves agents with no terminal signal, so they can retry a dead hash instead of rerunning the source command or rereading the source file. This change uses the cache store's existing TTL status metadata before the MCP retrieval path loses that distinction, then returns expired-hash guidance only when the local store proves the entry existed and expired. Closes #1776 ## 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 - Uses CCR store status metadata to distinguish expired local hashes from never-stored hashes in the MCP retrieval path. - Keeps proxy fallback and successful local retrieval behavior unchanged. - Adds focused regression coverage for expired stored hashes, the status-to-retrieve TTL boundary, proxy fallback preservation, and missing-hash negative space. ## Testing - [x] Unit tests pass (`uv run pytest tests/test_ccr_mcp_server.py -q`) - [x] Linting passes (`uv run ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py`) - [ ] Type checking passes (`uv run mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text Base pytest: FAILED tests\test_ccr_mcp_server.py::test_mcp_retrieve_expired_hash_returns_terminal_guidance E KeyError: 'status' Head pytest: tests\test_ccr_mcp_server.py ...s.......... [100%] 13 passed, 1 skipped in 0.35s Ruff: All checks passed! ``` ## Real Behavior Proof - Environment: Windows, focused local pytest through the headless runner. - Exact command / steps: Store a CCR entry with a short TTL, advance beyond expiry, call `HeadroomMCPServer._retrieve_content(hash)`, force a second entry to cross TTL between status inspection and `retrieve()`, stub a proxy-backed retrieval for local misses, then call the same method with a never-stored hash and no proxy hit. - Observed result: The already-expired hash and the hash that expires during retrieval both return terminal expired guidance with `status: expired`; missing and expired local hashes still return proxy data when the proxy fallback succeeds; a never-stored hash with no proxy hit still returns the generic missing-hash error and no expired status. - Not tested: Full suite, live agent retry behavior, and live external proxy-backed retrieval. ## 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 are left unchecked because this is a narrow MCP error-shape fix and Headroom's changelog is generated from conventional commits. |
||
|
|
1c0e15243e
|
fix(mcp): show lifetime totals and label rolling session scope in headroom_stats (#1428)
## Description `headroom_stats` currently formats only the rolling session view from `/stats`, so users see session numbers with no explicit scope label and no lifetime totals even though the proxy already exposes lifetime savings data. This PR keeps the current session summary, labels it as rolling-session output, and appends lifetime totals from `persistent_savings.lifetime`. It stays formatting-only on an existing payload surface. Closes #1166 ## 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 - label the existing `headroom_stats` session block as rolling-session output - append lifetime totals from the existing stats payload - add focused formatter regressions and fallback coverage - update `CHANGELOG.md` ## Testing - [ ] Unit tests pass (`uv run pytest tests/test_ccr_mcp_server.py -v`) - [ ] Linting passes (`uv run ruff check .`) - [ ] Type checking passes (`uv run mypy headroom`) or explain N/A truthfully - [x] New tests added for new functionality when applicable - [ ] Manual testing performed ### Test Output ```text Focused local commands passed: - uv run pytest tests/test_ccr_mcp_server.py -x -v 9 passed, 1 skipped - uv run ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py All checks passed - uv run ruff format headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py --check 2 files already formatted Base proof on origin/main with the updated regression file: - pytest -k "window_scoped" failed because the output still says "Headroom Session Summary" - pytest -k "includes_lifetime_totals_from_persistent_savings" failed because the formatted text still has no "Lifetime Savings:" section Not run locally: - uv run mypy headroom - Template-level broader commands `uv run pytest tests/test_ccr_mcp_server.py -v` and `uv run ruff check .` ``` ## Real Behavior Proof - Environment: focused `HeadroomMCPServer._handle_stats()` test payloads with and without `persistent_savings.lifetime` - Exact command / steps: run `uv run pytest tests/test_ccr_mcp_server.py -x -v`, specifically the new `_handle_stats()` regressions that feed summary-only, summary-plus-lifetime, missing-lifetime, and zero-lifetime payloads through the MCP stats formatter - Observed result: output contains `Headroom Window-Scoped Session Summary`, appends `Lifetime Savings:` when lifetime data is present, and omits that section cleanly when lifetime data is absent - Not tested: broader MCP output redesign beyond this formatter ## 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 - [ ] 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 - [ ] New and existing unit tests pass locally with my changes - [x] I have updated the CHANGELOG.md if applicable ## Additional Notes - Scoped to the MCP text surface only; dashboard and broader savings-window work stay out of scope. - Attribution: the issue body identified the exact mismatch between current `headroom_stats` output and the already-live lifetime stats payload. |
||
|
|
c2fc4d3753
|
fix(ccr): make headroom_retrieve a hash-only full-content lookup (#1532)
The optional `query` parameter on headroom_retrieve routed retrieval through CompressionStore.search(), which BM25-scored the items inside a single cached blob and dropped everything below a 0.3 relevance floor. On small per-blob corpora with conversational queries this returned an empty result the large majority of the time, so the LLM saw "nothing found" for content that was actually present — pushing users to turn compression off entirely. Retrieval is fundamentally a hash lookup (this already matches the Rust proxy's CCR store, which is put/get only — "no BM25 search"). Remove the query/search path end to end and always return the full original content: Core (Python proxy): - tool schemas (anthropic/openai/google) drop the `query` property - parse_tool_call returns the hash (str | None) instead of (hash, query) - response handler, proxy POST/GET/tool-call handlers, the MCP retrieve tool, and the streaming feedback recorders retrieve by hash only - proactive context-tracker expansion always restores full content - delete CompressionStore.search() and its BM25 machinery (the bm25 module stays — it is still used by relevance/) - CCRToolCall.query, CCRToolResult.was_search, and ExpansionRecommendation.expand_full/search_query are removed Plugins (advertised a now-defunct query param to the LLM): - hermes (Python), openclaw + opencode (TypeScript) retrieve tools drop `query` from their schemas, signatures, request URLs, and tests Benchmarks/docs: - ccr_regression + adversarial benchmarks switch from store.search() to full hash retrieval (search input-injection tests repurposed to the hash, the only remaining input surface) - wiki/ARCHITECTURE.md, wiki/ccr.md, docs/content/docs/ccr.mdx, config.py and store docstrings updated to describe hash-only retrieval Tests updated to assert full-content retrieval and guard the removed surface; the full CCR/proxy/store/TOIN suite passes. ruff + mypy clean. ## Description <!-- Briefly explain the change and why it is needed. --> 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) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - ## Testing <!-- Check what you actually ran, then paste the real command output below. --> - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [ ] New tests added for new functionality - [ ] Manual testing performed ### Test Output ```text # Paste relevant command output or artifact links here ``` ## Real Behavior Proof - Environment: - Exact command / steps: - Observed result: - Not tested: ## Review Readiness - [ ] I have performed a self-review - [ ] This PR is ready for human review ## Checklist - [ ] My code follows the project's style guidelines - [ ] I have performed a self-review of my code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) Add screenshots to help explain your changes. ## Additional Notes <!-- Mention any N/A checklist items, tradeoffs, follow-ups, or maintainer context. --> |
||
|
|
ad7993bf15
|
fix(codex): stop pinning Codex memory MCP to one project db (#1269)
## Description Stop `headroom wrap codex --memory` from pinning the global `headroom_memory` MCP server to one absolute SQLite path. Today the wrapper writes `--db <wrap-cwd>/.headroom/memory.db` into `~/.codex/config.toml`, which makes later Codex sessions either reopen a stale project-local DB or fail with `unable to open database file` when that original path disappears. This change lets the MCP server use its existing per-cwd default again, so each Codex session resolves `.headroom/memory.db` from the active project instead of a serialized past cwd. Closes #1147 The current Codex-memory config surface was shaped by https://github.com/chopratejas/headroom/issues/462 and https://github.com/chopratejas/headroom/issues/730; this PR keeps that surface project-scoped again instead of globally pinning one DB. ## 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 - remove the injected `--db` argument from the global `headroom_memory` Codex MCP block while keeping `--user` intact - preserve the wrap-time local `.headroom/memory.db` setup and Claude-memory import path for the current project - treat only wrap-owned Codex markers as snapshot-suppression and unwrap-cleanup signals, so pre-existing named MCP blocks still back up and restore - log a startup diagnostic from `headroom.memory.mcp_server` that records the configured DB path, config source, cwd/project root, resolved storage scope, path existence/readability, and whether the path was static or cwd-derived - add a shared MCP SDK test stub so both the memory MCP and CCR MCP test surfaces still run in CI when `mcp` is absent - make the shared MCP stub re-import target modules under the stubbed dependency set and restore any pre-existing target module object plus dotted parent-package attribute state after cleanup - add focused regressions and guard coverage for the persisted Codex config shape, named-MCP marker backup and restore, the no-backup memory-only unwrap path, the wrap-memory-then-unwrap cleanup path, the failed-wrap memory-only cleanup path, the startup-diagnostic path classification, the shared-store CCR retrieval path, and the shared MCP stub import lifecycle - add a `CHANGELOG.md` entry for the user-visible Codex memory scoping fix ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [ ] 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_ccr_mcp_server.py tests/test_memory/test_mcp_server.py tests/test_cli/test_wrap_codex.py tests/test_mcp_stub.py ======================== 78 passed, 1 warning in 5.96s ======================== Pytest warning: PytestConfigWarning: Unknown config option: asyncio_mode Pytest post-success atexit noise: PermissionError: [WinError 5] Access is denied: 'C:\Users\Rod\AppData\Local\Temp\pytest-of-Rod\pytest-current' uv run ruff check headroom/cli/wrap.py headroom/memory/mcp_server.py tests/_mcp_stub.py tests/test_ccr_mcp_server.py tests/test_cli/test_wrap_codex.py tests/test_mcp_stub.py tests/test_memory/test_mcp_server.py All checks passed! uv run ruff format headroom/cli/wrap.py headroom/memory/mcp_server.py tests/_mcp_stub.py tests/test_ccr_mcp_server.py tests/test_cli/test_wrap_codex.py tests/test_mcp_stub.py tests/test_memory/test_mcp_server.py --check 7 files already formatted ``` ## Real Behavior Proof - Environment: isolated temp project directories, a temp Codex home, the real `wrap codex` and `unwrap codex` CLI commands under pytest, a mocked missing-`codex` launch path for the failed-wrap cleanup case, and shared MCP-SDK stubs for the memory MCP and CCR MCP test modules so CI still exercises those paths without a real `mcp` install. - Exact command / steps: run `uv run pytest tests/test_ccr_mcp_server.py tests/test_memory/test_mcp_server.py tests/test_cli/test_wrap_codex.py tests/test_mcp_stub.py`; prove the persisted config shape with `TestCodexMemoryMcpConfig::test_inject_omits_db_and_replaces_existing_memory_block`; prove prepare-only wrap cleanup with `test_wrap_codex_memory_prepare_only_unwrap_removes_memory_mcp_without_prior_config`; prove failed-wrap cleanup with `test_wrap_codex_memory_launch_failure_unwrap_cleans_memory_only_config`; guard pre-existing named Codex MCP preservation with `test_memory_only_wrap_restores_preexisting_named_mcp_block` and `test_memory_only_wrap_without_backup_preserves_named_mcp_block`; prove the startup diagnostic classifications with `test_memory_mcp_startup_context_reports_dynamic_project_db` and `test_memory_mcp_startup_context_reports_static_external_db`; prove the shared-store CCR retrieval path with `test_mcp_uses_shared_singleton_store` and `test_mcp_retrieves_proxy_stored_content`; prove stub import cleanup with `test_import_module_with_mcp_stub_imports_target_and_cleans_up`, `test_import_module_with_mcp_stub_reimports_target_and_restores_originals`, and `test_import_module_with_mcp_stub_cleans_up_dotted_target_attribute`. - Observed result: the persisted global `headroom_memory` block now keeps `--user` but omits `--db`; prepare-only memory setup still bootstraps the current project's `.headroom/memory.db`; `headroom unwrap codex --no-stop-proxy` now removes both the prepare-only generated config and the failed-wrap memory-only config instead of leaving `[mcp_servers.headroom_memory]` behind; pre-existing named Codex MCP blocks remain restorable across both normal and no-backup memory-only unwrap paths because only wrap-owned markers suppress backups or trigger named-block cleanup; the memory MCP server now logs whether its DB path came from the cwd default or an explicit static path, along with the resolved path and scope it will open; CI can exercise both MCP test modules even when the `mcp` package is absent from the shard environment, and the shared stub now re-imports target modules under the stubbed SDK while restoring both dependency and dotted parent-package target-module import state after cleanup. - Not tested: full end-to-end interactive Codex CLI launch. ## 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 - [ ] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [ ] 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 The code change stays narrowly scoped to Codex memory config persistence, cleanup, and startup observability. It does not widen into larger memory-routing redesign or startup-failure recovery logic. |
||
|
|
f216e43055
|
fix(mcp): report correct savings_percent in headroom_compress (#1106)
## Description
`headroom_compress` reports `savings_percent` backwards. In
`_compress_content`:
```python
savings_pct = (
round((1 - result.compression_ratio) * 100, 1) if result.compression_ratio < 1.0 else 0
)
```
`compression_ratio` is already the saved fraction (`CompressResult`:
"0.0 = no savings, 1.0 = 100% removed"), so `1 - compression_ratio`
gives the *retained* percentage instead. A no-op comes back as 100% and
a real 71% reduction as 28.8%. The `else 0` branch also zeroes out a
genuine 100% result.
`_Stats.record_compression` a few lines up already does it the right way
(`1 - output_tokens / input_tokens`), so this is just bringing the
return value in line with that.
Closes # (no existing issue — found while evaluating the tool; can file
one if you'd rather track it)
## 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/ccr/mcp_server.py`: derive `savings_percent` from
`output_tokens`/`input_tokens` like `record_compression` does, so
`savings_percent` and `tokens_saved` can't disagree.
- `tests/test_ccr_mcp_server.py`: regression test tying
`savings_percent` to the token counts, including the no-op-isn't-100%
case.
## 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
Ran the relevant checks against the changed code (borrowed the prebuilt
`_core.abi3.so` from the released wheel so the checkout could import the
pipeline):
```text
$ ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py
All checks passed!
$ mypy --ignore-missing-imports headroom/ccr/mcp_server.py
Success: no issues found in 1 source file
$ pytest tests/test_ccr_mcp_server.py -q
.... [100%]
4 passed in 1.71s
```
I ran the ccr test module and lint/type checks on the changed files, not
the whole repo suite (that needs a full Rust build) — CI covers the
rest.
## Real Behavior Proof
- Environment: macOS, Python 3.14, checkout + prebuilt `_core` from
headroom 0.26.0
- Exact command / steps: ran `compress()` on three inputs (a 40-record
JSON array, an incompressible string, repeated prose) and compared the
old `round((1 - compression_ratio) * 100, 1)` against the token-derived
value `(1 - comp/orig) * 100`.
- Observed result: the old expression returns the retained %, so 0%
saved is reported as 100% and a real 71.2% reduction as 28.8%; the new
value matches actual savings in every case:
```text
input orig comp actual old formula new formula
array(40) 497 143 71.2% 28.8% 71.2%
noop 14 14 0.0% 100.0% 0.0%
prose 111 111 0.0% 100.0% 0.0%
```
- Not tested: the full repo test suite and the E2E workflows (need a
complete Rust build / maintainer-approved CI); only the ccr test module
and lint/type checks on the changed files were run locally.
## 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
Docs/CHANGELOG left unchecked as N/A — no user-facing doc covers this
field, though I'm happy to add a CHANGELOG line if you want one.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
|
||
|
|
08fb845fe3
|
fix(ccr): return stored content when headroom_retrieve query matches nothing (#1213) (#1236)
## Description Fixes #1213. `headroom_retrieve` with a `query` returns *"Content not found"* for entries that exist and are unexpired, whenever the query matches no item above the BM25 relevance floor. `HeadroomMCPServer._retrieve_content`'s `query` branch returns only inside `if results:`. An empty `store.search()` result — legitimate when no item clears `score_threshold=0.3` (common for repetitive / low-diversity content, or a query token that matches nothing) — falls through to the generic *"Content not found. It may have expired or the hash may be incorrect."* error, even though `store.retrieve(hash_key)` would return the entry. This conflates *hash missing/expired* with *query matched zero items* and silently discards a valid entry. The `query=None` branch already does the right thing (`store.retrieve`), so the two paths were asymmetric. ## 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/ccr/mcp_server.py`: in `_retrieve_content`, when `query` is given but `store.search()` returns empty, fall back to `store.retrieve(hash_key)` and return the full content (`results=[]`, `count=0`, plus an explanatory `note`) instead of falling through. Genuine misses (`retrieve` → `None`) still reach the "Content not found" error. - `tests/test_ccr_mcp_server.py`: regression tests (below). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ pytest tests/test_ccr_mcp_server.py -q 5 passed $ ruff check headroom/ccr/mcp_server.py tests/test_ccr_mcp_server.py All checks passed! $ ruff format --check ... 2 files already formatted ``` ## Real Behavior Proof - Environment: Python 3.13, `HeadroomMCPServer(check_proxy=False)` against the real shared `CompressionStore` (no proxy / network). - Exact command / steps: `store.store(repetitive_text, "<<small>>")` → `hash`; then `_retrieve_content(hash, query="zzqx_nonmatching_token")`. - Observed result: **before** the fix → `{"error": "Content not found. ..."}` while `store.retrieve(hash)` returns the entry; **after** → `{"source": "local", "original_content": <text>, "count": 0, "note": "Entry exists but no item matched ..."}`. A genuinely missing hash still returns the error. - Not tested: end-to-end through a running proxy / live MCP client (verified at the store + `_retrieve_content` level, which is where the bug lives). ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review |
||
|
|
249af6cc7b
|
fix(ccr): use shared compression store (#875)
## Description Use shared get_compression_store() singleton in MCP _get_local_store so headroom_retrieve sees proxy-compressed content. Fixes #860 ## 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) ## Testing Describe the tests you ran to verify your changes: - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff check .`) - [x] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality |
||
|
|
5884f7fb41 |
fix: restore Windows mypy compatibility
Make Unix-only file locking imports type-safe on Windows, tighten beacon lock cleanup, and fix the remaining exposed typing issues so the repository's mypy check passes cleanly on Windows again. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> |