- ASCII block logo replaces plain # heading
- Power-stats line + nav links above the fold
- Time-boxed section headings (30s / 60s)
- What-it-does bullets pruned to one clause each
- Agent table notes trimmed to ≤5 words with ● markers
- Pipeline internals + provider slices moved to collapsed <details>
- New When-to-use / When-to-skip section
- GIFs centered via HTML with captions
- Integrations and What's-inside remain collapsed <details>
Fixes#454, #455.
Streaming record_request paths were calling metrics without
attempted_input_tokens, so attempted_input_tokens_total stayed at 0 for
backend-routed traffic (litellm-azure, bedrock, anthropic streaming).
active_savings_percent then divided by zero and the dashboard headline
showed 0% even while compression was working. The three streaming sites
now pass the pre-compression request size as the attempted denominator,
matching the non-streaming sibling in openai.py.
The dashboard headline also falls back to proxy_savings_percent when
attempted is missing so historical 0% values self-heal.
The "Compression Quality" widget computed totalWaste / saved as a
percentage and could exceed 100. The metric is conceptually broken (the
two values measure different things across different surfaces — a perfect
semantic compressor surfaces zero waste signals and scores "low quality"
by this formula), not merely unbounded, so capping it just hides the
underlying confusion. Dropped the widget and the matching Quality column
on Recent Requests; removed the dead confidence getters.
For #454's diagnostic gap, added two visibility levers:
- --compress-user-messages CLI flag (+ HEADROOM_COMPRESS_USER_MESSAGES
env) flips the router's skip_user_messages default off for workloads
where the bulk of input lives in user messages (OpenAI/Azure chat with
pasted code/RAG context).
- /stats now includes router.route_counts aggregating the router's
protection categories (user_msg, system_msg, recent_code,
excluded_tool, …) so operators can see why compression is low without
local patching.
Analysis of the issue reporter's attached proxy_savings logs showed
day-on-day savings ranging 0.6%–76% based on workload shape (pasted user
content vs tool-output rounds), not a version regression — the dashboard
0% headline made workload variance look like a regression.
Derive source-tree versions from release history so headroom --version no longer reports stale project metadata.
Restart stale idle proxies after an upgrade, but leave active sessions running to avoid interrupting ongoing conversations.
Remove _version.py from version-sync ownership and make version verification catch package/plugin manifest drift.
compression_units.py:
- Replace dict-unpacking pattern with dataclasses.replace() so mypy can
type-check fields. The `**base` form forced mypy to infer
`dict[str, object]`, which doesn't satisfy the per-field types of
UnitCompressionResult (46 arg-type errors).
- Use `isinstance(candidates, Iterable)` for the transform-iteration
guard. The previous `iter()` call had a `# type: ignore[arg-type]`
that was misclassified — mypy actually emits `call-overload` here.
live_zone_thresholds.rs:
- Update the JsonArray threshold assertion from 1024 to 512 to match
the new constant. eaf5980 lowered THRESHOLD_JSON_ARRAY from 1024 → 512
in live_zone.rs but missed this integration test.
d9d8972 wired auto-MCP registration into ``init`` so ``[Retrieve
more: hash=…]`` markers stay live for users who never ran
``headroom mcp install`` separately, but the ``seq_claude_local``
e2e assertion was still pinned to the pre-MCP two-command sequence
and failed in docker-native-e2e on main.
The ``-e HEADROOM_PROXY_URL=…`` arg is only emitted when the proxy
port differs from the 8787 default; this case sets ``--port 9011``,
so the env arg is included in the expected argv.
The per-chunk SSE parser only flushes events terminated by `\n\n`.
When upstream truncates mid-event (client disconnect, network drop,
RemoteProtocolError), the message_start (cache_read /
cache_creation) or message_delta (output_tokens) usage events sit
in the residual sse_buffer and never get parsed — the finalizer
then logs cache_read=cache_write=0, which the freeze heuristic on
the next request reads as "no provider cache, reprocess
everything," producing a different prefix and a real cache miss
on the *next* turn.
Append `\n\n` to the residual buffer at end-of-stream so the
existing parser drains the partial event. Only fills None / 0
slots so a real cache_read=0 from earlier in the stream isn't
clobbered.
The proxy compresses tool_result payloads and emits [Retrieve more: hash=…]
markers, but Claude Code / Codex had no headroom_retrieve tool to call on
those markers unless the user separately ran 'headroom mcp install'. The
markers were dead pointers — silent quality loss.
Adds a per-agent MCP registrar abstraction (mcp_registry/) and wires it
into wrap and init so MCP install happens automatically alongside rtk:
- mcp_registry/base.py — MCPRegistrar ABC, ServerSpec, RegisterResult,
RegisterStatus enum.
- mcp_registry/claude.py — Claude Code registrar (claude mcp add CLI
with .claude.json / mcp.json file fallback).
- mcp_registry/codex.py — OpenAI Codex registrar (marker-delimited TOML
block edits to ~/.codex/config.toml; preserves user's other config).
- mcp_registry/install.py — install_everywhere() orchestrator with
detect-then-register semantics.
- mcp_registry/display.py — shared format_result()/format_results() for
consistent CLI output across wrap, init, and 'headroom mcp install'.
Adding a new agent (Cursor, Continue, Cline, Windsurf, Goose) is now a
single new file plus one entry in get_all_registrars(); call sites and
display logic don't change.
Test seam is constructor injection (home_dir, claude_cli) — zero patches
in 66 new tests across the registry. Removed 13 brittle CLI integration
tests in test_mcp.py that were patching module-level globals; equivalent
coverage now lives at the registrar/orchestrator layer.
wrap codex: snapshot ~/.codex/config.toml at the top of the command so
the existing wrap→unwrap round-trip captures the true pre-wrap state
even though MCP install now writes to the same file mid-flow.
220 tests pass (66 new + 154 existing CLI + integration). ruff and mypy
clean on touched files.
PR #431 (merged) added text-block compression to support DeepSeek + Cline,
but the gate ("skip user/system") leaves assistant text blocks compressible
by default. Assistant content is echoed back by the client in subsequent
turns and becomes part of the upstream provider's prefix cache (Anthropic
explicit cache_control, DeepSeek/OpenAI auto-prefix). Compressing it
silently changes the bytes the next turn must match for a cache hit —
turning a 90% read discount into a 25% write penalty on Anthropic, or a
full prefill on DeepSeek/OpenAI when the in-process result cache evicts
or differs across restarts.
Re-aligns the design around prefix-cache safety:
* Block-level cache_control protection (defense in depth). Any block
carrying cache_control is the client's explicit cache breakpoint;
never modified, regardless of role or block type. Closes the gap
that frozen_message_count alone leaves — that count is a coarse
message-level approximation; this is the per-block guarantee.
Applies to both tool_result and text paths.
* compress_assistant_text_blocks defaults to False (off). Assistant
text blocks are skipped by default, restoring pre-#431 cache safety
for Anthropic flows. Per-request opt-in via kwargs (or via
ContentRouterConfig.compress_assistant_text_blocks for deployment-
wide enable) preserves the Cline + DeepSeek goal — only enable
when the backend doesn't honor cache_control AND compression is
deterministic enough that the auto-prefix cache still hits across
eviction/restart.
* Unknown roles default-skip too (was: compressed). developer/judge/
custom roles are safer to leave untouched than to compress
aggressively without thinking through their cache semantics.
* Online streaming usage parser. Replaces the per-stream
list[bytes] buffer with a single last_completion_tokens int updated
per chunk via a module-level _parse_completion_tokens_from_sse_chunk
helper. Streaming memory is now O(1) regardless of stream length —
important for 200K-output reasoning models and DeepSeek V4 Pro's
384K max output.
* Renames the unused min_tokens parameter to min_chars (the threshold
has always been chars, not tokens, in both the tool_result and text
paths). Now also wired through ContentRouterConfig
.min_chars_for_block_compression so the threshold is configurable
per Realignment build constraints.
Tests:
* 17 new tests in tests/test_transforms_content_router.py covering
the role matrix (user / system / assistant / tool / unknown),
cache_control protection on both paths, opt-in semantics, the
min_chars threshold, and idempotent pinning detection.
* 9 new tests in tests/test_streaming_usage_parser.py covering the
online parser's success and edge cases (usage frame, [DONE],
invalid JSON, multi-frame chunks, zero tokens, non-dict payloads,
invalid UTF-8).
Trade-off: deployments pointed at non-cache-aware backends (DeepSeek
direct, OpenAI direct) lose blanket assistant-text compression by
default — they opt in via config. Anthropic flows go back to being
prefix-cache-safe out of the box.
ContentRouter._process_content_blocks previously only compressed tool_result
blocks. Anthropic-format requests routed through the OpenAI/DeepSeek backend
arrive with text blocks in their content lists; those were passing through
unchanged, so DeepSeek + Cline saw zero compression. Adds text-block
handling with role-based protection (user/system text blocks are skipped so
the user's actual prompt is never silently mutated) and reuses the existing
two-tier compression cache.
Streaming OpenAI-via-backend path now buffers chunks to parse the final
SSE usage frame for completion_tokens, forwards waste_signals through
metrics + RequestLog, and emits a RequestLog entry so the dashboard's
recent-requests feed and "What Headroom Removed" stop being empty for
this code path. Same RequestLog wiring added to the non-streaming
backend path, which previously logged nothing at all.
DeepSeek V4 entries added to the model registry and OpenAIProvider
context limits with values verified against api-docs.deepseek.com
(1M context / 384K max output) and LiteLLM model_cost (deprecated
deepseek-chat / deepseek-reasoner aliases at 131K). LiteLLM lookup
remains the first source; these are the manual fallback that suppresses
the unknown-model warning.