Unified context compression layer combining Headroom-style generic compression with Infigraph's semantic graph awareness. Works for ALL content, gets smarter when graph data is available.
Goal: 70-90% token reduction with 95%+ answer quality preservation. Zero quality loss on lossless compression types.
- Architecture
- Competitive Analysis: Headroom vs Infigraph Context Engine
- Phase 0: Baseline Measurement
- Phase 1: Cache-Aligned Live-Zone Compression
- Phase 2: Tool Output Shaping
- Phase 3: Session Context Tracking
- Phase 4: Generic Content Compressors + ML Text Compression
- Phase 5: Cross-Agent Context Sharing
- Phase 6: Budget-Aware Scaling
- Phase 7: Compress MCP Tool
- Phase 8: A/B Testing and Production Rollout
- Test Strategy
- Quality Benchmarks
- Success Criteria
- Risk Register
Layer 0: CACHE PROTECTION (CacheAligner)
│ Identify frozen prefix (system prompt, tool defs, old turns)
│ NEVER mutate frozen prefix — only compress live zone
│ Live zone = latest tool output + latest user message
│
Layer 1: CONTENT CLASSIFICATION (ContentRouter)
│ Detect content type → route to optimal compressor
│
Layer 2: TYPE-SPECIFIC COMPRESSION
│ ├── Code → Graph-aware (signatures + edges, not full source)
│ ├── JSON → Statistical sampling (Kneedle for representative subset)
│ ├── Logs → Pattern dedup + error preservation
│ ├── Build → Collapse compile lines, keep errors/warnings
│ ├── Stack → Keep app frames, collapse framework frames
│ ├── Prose → ML extractive summary (Kompress or local model)
│ ├── File tree → Count collapse (src/ (47 files))
│ └── Table → Header + row count + samples
│
Layer 3: SESSION DEDUP
│ Track seen files/symbols per turn
│ Same content hash → "(seen: auth.rs::login, turn 3)"
│ Changed content → show full (content hash differs)
│ Stale (>10 turns) → show full (may have scrolled out)
│
Layer 4: BUDGET-AWARE SCALING
│ >70% budget remaining → minimal compression
│ 50-70% → standard compression
│ 20-50% → aggressive (shorter summaries)
│ <20% → minimal (one-liners only)
│
Layer 5: CROSS-AGENT SHARING
Subagent spawns get compressed context, not full replay
Shared compressed store with agent provenance
Auto-dedup across agents working on same files
Tool call (e.g. search "auth login")
│
▼
Raw output (2000 tokens)
│
▼
┌──────────────────────┐
│ L0: Cache Protection │ ← Is this in the frozen prefix? → SKIP
│ (Live-zone gate) │ Only compress new content
└──────────┬───────────┘
▼
┌──────────────────────┐
│ L1: Classify │ ← "search results with code snippets"
│ → Code type │
└──────────┬───────────┘
▼
┌──────────────────────┐
│ L2: Graph-aware │ ← 20 results → 20 one-liners with
│ compression │ symbol kind, line range, edge counts
│ │ (500 tokens, ZERO info loss on existence)
└──────────┬───────────┘
▼
┌──────────────────────┐
│ L3: Session dedup │ ← 5 of 20 already seen → mark "(seen)"
│ │ (400 tokens)
└──────────┬───────────┘
▼
┌──────────────────────┐
│ L4: Budget check │ ← 60% budget left → standard level OK
│ │ (400 tokens, no further compression)
└──────────┬───────────┘
▼
Compressed output (400 tokens = 80% reduction)
- Cache-first — never break the LLM provider's KV cache prefix
- Never compress what's being edited — edit targets always get full source
- Never reduce result counts — show ALL results, just shorter format per result
- Lossless where possible — dedup, skip-seen, schema extraction are lossless
- Progressive disclosure — summary first, detail on
detail=truerequest - Budget-aware — compress harder as context fills up
- Zero retrieval tax — no decompress round-trip, just ask with
detail=true - Cross-agent efficient — subagents get compressed context, not full replay
- Measurable — every compression logged for quality monitoring
Built into crates/infigraph-mcp/ as response middleware in dispatch_tool. Not a proxy. For non-Infigraph content, exposed as a compress MCP tool.
| Feature | Headroom | Infigraph CE | Winner |
|---|---|---|---|
| Cache alignment | ✅ CacheAligner, live-zone only | ✅ L0 Cache Protection | Tie |
| Content routing | ✅ ContentRouter | ✅ L1 Classifier | Tie |
| JSON compression | ✅ SmartCrusher (Kneedle statistical sampling) | ✅ Statistical sampling (adopted) | Tie |
| Code compression | ✅ Tree-sitter AST body collapse | ✅ Graph-aware (knows callers/callees/edges) | Infigraph |
| Log compression | ✅ LogCompressor (93% reduction) | ✅ Pattern dedup + error preservation | Tie |
| ML text compression | ✅ Kompress-v2-base (HF model) | ✅ Extractive summary (local model or Kompress) | Tie |
| Image compression | ✅ ML router | ❌ Not planned (low priority for code tools) | Headroom |
| Reversible retrieval | ✅ headroom_retrieve + BM25 search | ✅ detail=true parameter (no extra tool call) | Infigraph |
| Session dedup | ❌ No cross-turn memory | ✅ L3 seen-tracking with content hashes | Infigraph |
| Graph-aware ranking | ❌ No code graph | ✅ Cluster results, relevance by graph distance | Infigraph |
| Budget-aware scaling | ❌ Static compression | ✅ L4 adaptive by remaining tokens | Infigraph |
| Edit protection | ❌ Doesn't know what you're editing | ✅ Never compress edit targets | Infigraph |
| Cross-agent sharing | ✅ SharedContext + provenance | ✅ L5 compressed context passing | Tie |
| Stack trace compression | ❌ Not specialized | ✅ App/framework frame separation | Infigraph |
| Build output compression | ✅ Dedicated, compile-line collapse | Infigraph | |
| File tree compression | ❌ Not mentioned | ✅ Count collapse | Infigraph |
| Domain-specific compressors | 6 types | 8 types | Infigraph |
| Deployment | Library, proxy, MCP, wrapper | Native MCP middleware (zero overhead) | Infigraph |
Summary: Headroom has 3 things we adopted (CacheAligner, statistical JSON sampling, ML text compression). We have 7 things Headroom doesn't (graph-awareness, session dedup, budget scaling, edit protection, stack traces, build output, file trees). Our retrieval mechanism (detail=true) is simpler and cheaper than their headroom_retrieve tool call.
Goal: Establish current token usage patterns before any optimization.
- Add token counting to
handle_tools_callincrates/infigraph-mcp/src/main.rs(not dispatch_tool — tests call that directly) - Used word-count heuristic (words * 1.4) instead of tiktoken-rs — no new dependency, sub-microsecond
- Log to
.infigraph/compression_metrics.jsonl:{tool, timestamp, raw_tokens, compressed_tokens, compression_ratio, detail_requested, args_summary} - Gated behind
INFIGRAPH_METRICS=1env var
- Run 20 representative tasks across 5 categories (see Quality Benchmarks below)
- Record per-task: total tokens consumed, tokens per tool call, number of tool calls
- Identified top-2: get_doc_context (49%) and search (38%) = 87% of all output tokens
- Create
tests/compression_eval/directory - Define 20 eval tasks as structured test cases:
struct EvalTask { id: String, category: Category, // Search, Edit, Refactor, Understand, Debug description: String, setup: Vec<ToolCall>, // tool calls to set up context query: ToolCall, // the tool call being evaluated expected_answer: String, // ground truth quality_check: QualityCheck, // how to verify answer quality }
- Categories (4 tasks each):
- Search (5): find symbol, find usage pattern, find config, find test, find doc
- Edit (5): fix bug, add parameter, change return type, add error handling, rename
- Refactor (5): extract function, rename across files, move to module, change signature, remove dead code
- Understand (5): explain flow, trace data path, identify dependencies, architecture overview, find blast radius
- Execute all 20 tasks with current (uncompressed) tool outputs
- Record: tokens_used, answer_correct (binary), answer_quality (1-5 human rating)
- Store results in
tests/compression_eval/baseline_results.json - This becomes the control group for all subsequent phases
compression_metrics.jsonllogging in place- 20 eval tasks defined and baselined
- Top-2 token-heavy tools identified: get_doc_context (49%), search (38%)
Goal: Ensure compression never breaks the LLM provider's KV cache prefix. This is the foundation — all subsequent phases build on top of this.
Outcome: Verified by construction — no new code needed. MCP server only touches live zone (fresh tool results in handle_tools_call). Tool defs use static vec![] with BTreeMap-backed serde_json (deterministic key order). Two guard tests added in tool_parity.rs.
- Verified: MCP tool outputs are returned from
handle_tools_callas new content — always live zone -
compress_tool_outputruns only inhandle_tools_callon fresh results, never touches frozen prefix
- Not needed as separate module — compression only runs in
handle_tools_callby construction
-
build_tools_list()is a staticvec![],serde_json::MapusesBTreeMap(deterministic) -
detailparameter added statically at startup, not mid-conversation
- Added
tool_definitions_are_byte_stabletest — asserts identical bytes across calls - Added
tool_schema_token_budgettest — ~4k tokens, under 10k cap
- Descriptions already terse from prior work. ~4k tokens total.
- Live-zone safety verified by construction
- Tool definition byte-stability test passing
- Commit:
6ce9c1d
Goal: Add summary/detail modes to the top-5 token-heavy tools. Target: 50-70% reduction on these tools. Never reduce result counts — only reduce verbosity per result.
Outcome: 4 compressors implemented (search 55.7%, get_doc_context 88.3%, find_all_references 39%, get_architecture 54.3%). Combined 72.5% savings exceeds 40% gate. trace_callers/callees bypassed (5-7 tokens). Compression in compress.rs, wired into handle_tools_call in main.rs. Commit: d62cd17.
- Summary/detail modes designed and implemented for search, get_doc_context, find_all_references, get_architecture
- trace_callers/callees bypassed (output already 5-7 tokens)
- Created
compress.rswithCompressionLevelenum (Off, Summary, Aggressive, Minimal) -
compress_tool_output()wired intohandle_tools_callinmain.rs - Per-tool level caps via
effective_level()— search capped at Summary (Phase 6.4 finding)
-
compress_search,compress_doc_context,compress_references,compress_architecture
-
detail: booladded to tool definitions inbuild_tools_list -
for_edit=truebypasses compression
- Metrics logged to
.infigraph/compression_metrics.jsonl(gated INFIGRAPH_METRICS=1) -
get_compression_statsMCP tool (implemented in Phase 8.2, commitabb34ca)
- Covered by Phase 6.4 eval (4-level sweep, 20 tasks, must_contain assertions)
-
should_bypass()in compress.rs: security tools, small outputs (<100 tokens), errors, detail=true, for_edit=true
- Not needed — detail=true is cheap and explicit
- Compressors already fall through to raw on parse failure (pattern match returns raw.to_string())
catch_unwindaround compress+dedup inhandle_tools_call; panic → log,record_compress_failure(), return rawget_compression_statsreportsCompress failurescount
- Core tool compressors: search, get_doc_context, find_all_references, get_architecture
- Later additions: list_files, detect_dead_code, get_api_surface, git_summary, search_sessions (Decisions/Files truncation by level)
- Metrics logging in handle_tools_call (gated INFIGRAPH_METRICS=1)
- Bypass rules: security tools, small outputs, errors, detail=true, for_edit=true, focus set (3.3)
- Per-tool level caps (search capped at Summary) — commit
651ec59 - Compression failure fallback (2.9):
compress_pipeline_safe+ Compress failures in stats
Goal: Avoid re-sending content the LLM already has in context. Target: additional 20-30% reduction.
Outcome: Core seen-dedup (3.1+3.2+3.4) implemented in session_context.rs. FNV-1a hashing, 6-call staleness window, dedup on by default (INFIGRAPH_DEDUP=0 to disable). Focus tracking (3.3) and cross-session dedup (3.7) shipped. Still deferred: 3.6 (graph-aware compaction — undetectable from MCP).
- Created
session_context.rswithSessionContext,SeenEntry,CompressionLevel - Global
SESSION: Mutex<Option<SessionContext>> - FNV-1a content hashing, token budget tracking
- On every tool response, record content hash via FNV-1a
- On subsequent calls, check if content was seen:
- Same content hash →
"(seen in turn 3: auth.rs::login)" - Different hash → show full (content changed)
- Seen > 10 turns ago → show full (may have scrolled out of context window)
- Same content hash →
- Configurable staleness threshold (set to 6 calls, not 10 — tight window bounds damage from stale dedup)
- Track files/symbols from
get_doc_contextwithfor_edit=trueandget_code_snippet - Never compress content in the focus set (
should_bypass→is_in_focus) - Focus ages out after staleness window (default 6 calls via
focus_clock) - File path from
symbol_id(path::name) is also tracked so sibling symbols in the same file stay uncompressed - Focused args also skip
(seen …)dedup placeholders inapply_seen_dedup - Free-text
queryis not a focus candidate (avoids false-positive bypass onsearch)
-
apply_seen_dedupcalled aftercompress_tool_outputinhandle_tools_call(viacompress_pipeline_safe) - Dedup runs on already-compressed output
- Dedup on by default;
INFIGRAPH_DEDUP=0disables
- Integration test
phase3_dedup_evalin compression_eval.rs - Tests 4 tools with duplicate calls: search, get_doc_context, find_all_references, get_architecture
- Results: 42.6% additional savings on repeat calls, 67% dedup rate on eligible outputs (>50 tokens)
- D2 (get_doc_context 49 tokens) exempt — below 50-token dedup threshold
- D4 (get_architecture) not deduped — content key format issue (no primary arg)
- Quality preserved: dedup only on identical content (hash-verified)
- Gate: PASSED — dedup rate ≥ 50%, no quality loss
- When Claude Code triggers context compaction (conversation too long), provide a better summary than generic LLM summarization
- Hook: detect compaction event (conversation history suddenly shorter)
- Generate graph-aware summary of what was learned:
Session context (graph-aware compaction): Files modified: auth.rs (login, verify_token), routes/auth.rs (handler) Symbols explored: login (12 callers traced), verify_token (5 callers) Decisions: "use JWT not session cookies" (turn 5) Pending: update 3 remaining callers of old session API Graph state: 47 symbols touched, 12 edges traversed - This replaces ~5000 tokens of conversation replay with ~200 tokens of structured context
- Store compacted summary in SessionContext for cross-compaction continuity
- Persist dedup hashes to
.infigraph/dedup_state.json(FNV-1a content hashes) - Load prior hashes on SessionContext init for cross-session continuity
- Content-verified dedup (Option B): compare fresh hash against stored — zero false-dedup risk
- Stale hash handling: if content changed, remove prior hash and show full output
- Migrate prior hits to seen map on match (no double-lookup)
- Auto-persist every 5 tool calls via
maybe_persist() - 4 unit tests: matching hash, stale hash, migration to seen, persist interval
- Session context tracking with seen-dedup (session_context.rs, 8 tests)
- Phase 3 eval: 42.6% additional savings, 67% dedup rate, quality preserved
- LM2 session integration: cross-session dedup via persisted content hashes (3.7, 4 tests)
- Focus-aware compression (3.3 —
record_focus/is_in_focusbypasses compress + dedup) - Graph-aware context compaction (deferred: 3.6 — undetectable from MCP server)
Goal: Compress non-Infigraph content (bash output, file reads, JSON blobs, prose text). Target: 50-80% reduction on these content types.
Outcome (Phase 4a): Added tool-specific compressors: list_files (dir tree collapse), detect_dead_code (group by file), get_api_surface (collapse symbols, keep routes), git_summary (truncate symbol lists >5), later search_sessions (truncate Decisions pipes; level-aware Files Touched).
Outcome (Phase 4b): Content classifier (8 types: Json, JsonArray, LogOutput, StackTrace, BuildOutput, FileTree, Table, PlainText) + 7 generic compressors (JSON schema+sample, log dedup, stack trace framework collapse, build output compile collapse, file tree node collapse, table truncation, PlainText passthrough). compress MCP tool wired up for arbitrary text compression. ML prose (Task 4.9/4.10) deferred — extractive summarizer needs no new deps but PlainText is passthrough for now.
-
classify_content()in compress.rs — 8 types: Json, JsonArray, LogOutput, StackTrace, BuildOutput, FileTree, Table, PlainText - Log/build checked before JSON to avoid false positives on
[INFO]lines
- Schema inference + count + 2 samples for arrays; top-level structure for objects
- Pattern dedup, error/warning preservation, count annotation
- App frame preservation, framework frame collapse
- Node collapse with file counts
- Header + row count + first/last rows
- Compile line collapse, error/warning preservation
- Integration test
phase4_generic_compressor_evalin compression_eval.rs - Tests 5 content types: JSON array (50 items), logs (100 lines), stack trace, build output (30 crates), prose
- Results: 78.7% overall savings — Log 90.7%, Build 83.6%, Prose 55.6%, Stack 7.1%
- JSON: char-length reduction confirmed (word-count misleading for compact JSON)
- Gate: PASSED — >30% overall savings, all compressors functional
- Implemented
compress_proseincompress.rs(no separate ml.rs needed) - Primary: Potion8M embedding-based sentence scoring (cosine similarity to document centroid)
- Fallback: TF-IDF sentence scoring when embedder unavailable
- Position bonus (first 1.5x, last 1.2x) on both scorers
- Keeps top 40% of sentences (min 2), preserves original order
- Caveman-style filler word stripping (articles, hedging, verbose phrases) — ~15% additional reduction
- Kompress/ONNX deferred — Potion + filler stripping is sufficient and zero new deps
- Wired into
compress_genericforPlainTextcontent type (was passthrough) -
compress_prose(text: &str) -> Stringwith markdown-aware parsing - Preserves: headings, code blocks, list items, blockquotes, table rows, blank lines
- Skips compression for text < 200 tokens
- 8 tests: small passthrough, heading preservation, code block preservation, paragraph reduction, generic dispatch, sentence splitting, filler word stripping, filler stripping in prose
- Added
ort(ONNX Runtime) +ndarray+tokenizersdeps to infigraph-mcp -
kompressmodule incompress.rs: download-on-first-use from HuggingFace, ONNX Session with Mutex - Uses
kompress-small(70M params, ModernBERT + dual head: token classifier + span conv) - Model (~275MB) downloaded to
~/.infigraph/models/kompress-small/on first use via curl - Wired into
compress_prose: tries kompress first whenml_compression="kompress", falls back to extractive - Config:
ml_compressionfield in[compression]section of config.toml,INFIGRAPH_ML_COMPRESSIONenv var - Long text chunking (350 words per chunk, 20-word overlap) for texts exceeding 8192 tokens
- Subword token reconstruction: Ġ prefix handling for clean output
- Integration test in
compression_eval.rs: 33.4% savings on prose, key content preserved - Unit test
test_kompress_directverifying model inference and token reconstruction
- Content classifier + 6 compressors
- Content classifier + 8 generic compressors (JSON, log, stack, build, file tree, table, prose extractive)
-
compressMCP tool for arbitrary text compression - 4 more tool compressors: list_files, detect_dead_code, get_api_surface, git_summary
-
search_sessionscompressor (truncate Decisions; level-aware Files Touched) - Extractive prose compressor (TF-IDF/Potion sentence scoring + filler stripping)
- Kompress ML token compression (opt-in, download-on-first-use)
- Phase 4 eval: 78.7% generic savings, 33.4% kompress prose savings
Goal: When subagents are spawned (via Agent tool or workflows), pass compressed context instead of full replay. Eliminate redundant work across agents working on the same codebase.
Why skipped: Not feasible server-side. MCP protocol carries no agent identifier (only tool name + arguments). Server cannot distinguish which agent is calling, making provenance tracking (5.3) and agent-specific dedup (5.4) impossible. Additionally, we don't control subagent spawning (5.2) — Claude Code does. Process model (shared vs separate MCP instances per agent) is also unclear, making in-memory SharedContext unreliable.
- Create
crates/infigraph-mcp/src/compress/agents.rs - Implement shared compressed context store:
struct SharedContext { compressed_snapshots: HashMap<String, CompressedSnapshot>, agent_provenance: HashMap<AgentId, Vec<String>>, // which snapshots each agent produced } struct CompressedSnapshot { key: String, // e.g. "arch:project_root" or "file:auth.rs" compressed: String, // compressed content content_hash: String, // detect staleness created_by: AgentId, created_at_turn: usize, token_count: usize, }
- Store persists across agent spawns within a session
- Eviction: LRU with max 50 snapshots or 100k compressed tokens
- When spawning a subagent, package relevant context:
- Architecture snapshot (compressed
get_architectureoutput) - File focus set (which files are being edited)
- Relevant symbol summaries (compressed
get_doc_contextfor symbols in scope) - Session decisions and constraints
- Architecture snapshot (compressed
- Package format: single compressed block < 2000 tokens
- Subagent gets oriented without repeating the 5-10 tool calls the parent already made
- Track which agent produced which findings/edits
- When agent B works on same file as agent A, pass A's compressed findings
- Prevent duplicate analysis: if agent A already traced callers of
login(), agent B gets the compressed result - Provenance metadata:
{agent_id, task_description, files_touched, findings_summary}
- Before spawning agent, check SharedContext for relevant existing analysis
- If 80%+ of requested context already exists compressed → inject, don't re-analyze
- Dedup key:
(file_path, analysis_type, content_hash)— same file + same analysis + same content = cache hit - Log dedup hits and token savings in metrics
- Design 5 multi-agent eval tasks:
- Parallel code review (3 agents reviewing different files)
- Workflow: find → verify → fix pipeline
- Architecture exploration then targeted edit
- Cross-file refactor with blast radius check
- Bug investigation with trace + fix
- Measure: tokens per agent (with/without sharing), total session tokens, answer quality
- Gate: sharing must save ≥ 30% tokens in multi-agent scenarios with zero quality loss
- SharedContext store with agent provenance
- Context packaging for subagent spawns
- Auto-dedup across concurrent agents
- Phase 5 eval results
Goal: Dynamically adjust compression aggressiveness based on remaining token budget.
- Add
token_budget+total_tokens_sentfields toSessionContext - Track cumulative tokens sent via
track_tokens()called fromhandle_tools_call - Estimate remaining budget:
budget - total_sent - Default budget: 150k tokens (configurable via
INFIGRAPH_TOKEN_BUDGETenv var)
- Define budget thresholds:
> 70% remaining: level = Off (no compression needed) 50-70% remaining: level = Summary (default compression) 20-50% remaining: level = Aggressive (shorter summaries, more dedup) < 20% remaining: level = Minimal (one-line per result, max dedup) - Implement
auto_level()onSessionContext - Public
get_compression_level()andtrack_tokens()API
- Off: pass through raw output (bypass all compressors)
- Summary: existing Phase 2/4 compression (all callers/callees, all results)
- Aggressive:
- Search: top-3 results, drop text/doc matches
- Doc context: top-3 callers/callees
- Architecture: top-3 languages/hotspots/hubs
- References: grouped by file (unchanged from Summary)
- API surface: collapsed per-file (unchanged from Summary)
- Seen-dedup window: 8 calls (vs default 6)
- Minimal:
- Search: top-1 result only, no text/doc matches
- Doc context: 0 callers/callees (count only)
- Architecture: top-2 languages, no hotspots/hubs
- References: count + file count only
- API surface: count + file count only
- Seen-dedup window: 12 calls
- Level logged in compression metrics as
compression_level
- Added
INFIGRAPH_COMPRESSION_LEVELenv override for eval (session_context.rs) - Rewrote
run_eval.pyfor 4-level sweep with must_contain quality assertions - Fixed tasks.json: symbol_ids, structural must_contain checks
- Results (14 level-sensitive tasks across search, get_doc_context, find_all_references, get_architecture):
- Off: 0% savings, 100% quality
- Summary: 68.7% savings, 100% quality ← safe maximum
- Aggressive: 86.7% savings, quality cliff on search (33-67% retention, top-3 drops important results)
- Minimal: 89.7% savings, search unusable (0-33%), other tools still 100%
- Quality cliff: Aggressive level, search-only (top-3 result limit)
- Current
auto_level()thresholds validated as safe defaults - Future optimization: per-tool level overrides (search stays Summary while others go Aggressive)
- MCP protocol doesn't expose provider metadata, making auto-detection impossible
- Deferred until MCP spec adds client capability negotiation
- Budget-aware auto-scaling with 4 compression levels
- Per-tool level caps: search capped at Summary, others uncapped (commit
651ec59) INFIGRAPH_COMPRESSION_LEVELenv override for testing/eval- 63 tests passing (session_context + compress level tests + effective_level test)
- Phase 6.4 eval: Summary=68.7% savings/100% quality, quality cliff at Aggressive for search only
Goal: Expose compression as a standalone MCP tool for non-Infigraph content.
-
compresstool in MCP registry withtextparam + content type auto-detection - Returns compressed content (routed through content classifier → generic compressors)
- Bypassed from compression itself (in BYPASS_TOOLS list)
- Added Context Compression section to auto-generated CLAUDE.md template (claude_md.rs)
- Bumped VERSION 1→2 so existing installs get updated
- Documents: auto-scaling, search cap, security bypass, code snippet passthrough
- Tool description in
build_tools_listalready present - Full docs in
docs/CONTEXT-COMPRESSION.md - README.md: TOC entry, Key Highlights bullet, Technical Deep Dives link
compressMCP tool available for any content- CLAUDE.md integration instructions (auto-generated v2)
- Full documentation in CONTEXT-COMPRESSION.md + README
Outcome: Config.toml support, get_compression_stats MCP tool, quality monitoring with auto-tuning, dedup enabled by default. Commit: abb34ca.
- Added
[compression]section to.infigraph/config.toml:[compression] enabled = true # false to disable all compression level = "auto" # off | summary | aggressive | minimal | auto dedup = true # false to disable session dedup token_budget = 150000 # total token budget for auto-scaling staleness_window = 6 # dedup staleness window (calls)
- Walk-up search from cwd to find config file
- Priority: env var > config.toml > defaults
-
get_compression_statsMCP tool — shows current session metrics:- Compression level (with source: auto/env/config)
- Token budget, tokens sent, remaining %
- Tool calls tracked, dedup entries
- Per-tool detail-request rates with ⚠ flag when >30%
-
record_tool_call()tracks total + detail=true requests per tool -
should_reduce_compression()returns true when detail-rate >30% (min 5 calls) -
effective_level()auto-caps tools to Summary when rate too high - Log when LLM asks follow-up questions that suggest information was lost (not feasible from MCP server)
- Weekly quality audit (manual process, not automated)
- Phase A: Compression enabled by default (auto_level scales with budget)
- Phase B: Dedup enabled by default (
INFIGRAPH_DEDUP=0to disable) - Phase C: Detail-rate monitoring with auto-cap at >30%
- Phase D: Full production with budget-aware auto-scaling + config.toml
- Production-ready compression with config.toml, metrics, quality monitoring
- Dedup on by default, compression auto-scales with budget
get_compression_statsMCP tool for observability
Each compressor must have dedicated unit tests with explicit input → expected output pairs.
| Test | Input | Expected output |
|---|---|---|
test_json_array_basic |
[{"id":1,"name":"alice"},{"id":2,"name":"bob"},...{"id":100,"name":"zoe"}] |
JSON array (100 items), schema: {id: int, name: str}\nSample: {"id":1,"name":"alice"}\nSample: {"id":100,"name":"zoe"} |
test_json_object_nested |
{"config":{"db":{"host":"localhost","port":5432,"pool":{"min":5,"max":20,"timeout":30000}}}} |
Top-level keys preserved, nested values truncated at depth 3 |
test_json_empty_array |
[] |
JSON array (0 items) — no compression, pass through |
test_json_single_item |
[{"id":1}] |
Pass through raw — too small to compress |
test_json_mixed_types |
Array with heterogeneous objects | Schema shows union of all keys with ? for optional |
test_json_malformed |
{"broken": tru |
Fallback to raw output (not valid JSON) |
test_json_large_values |
Object with 10KB string values | Values truncated to 100 chars with ...(truncated) |
| Test | Input | Expected output |
|---|---|---|
test_log_repeated_lines |
500 identical [INFO] Processing... lines |
[INFO] Processing...\n... (498 similar lines)\n[INFO] Processing... |
test_log_errors_preserved |
100 INFO + 1 ERROR + 100 INFO | All INFO collapsed, ERROR shown in full with 1 line before/after |
test_log_warnings_preserved |
Mix of INFO/WARN/ERROR | All WARN and ERROR preserved, INFO collapsed |
test_log_no_timestamps |
Plain text that looks like logs but no timestamps | Falls back to PlainText classifier |
test_log_mixed_formats |
syslog + JSON-structured logs mixed | Handles both formats, collapses each pattern independently |
test_log_empty |
"" |
Pass through (bypass rule: < 100 tokens) |
test_log_single_error |
One ERROR line | Pass through (nothing to collapse) |
| Test | Input | Expected output |
|---|---|---|
test_stack_rust_backtrace |
Rust panic with 30 frames | App frames kept, std/tokio frames collapsed to ... (N framework frames) |
test_stack_python_traceback |
Python traceback with site-packages | App frames kept, site-packages collapsed |
test_stack_java_stacktrace |
Java NPE with spring/hibernate frames | App frames kept, framework collapsed |
test_stack_nested_cause |
Error with 3-level cause chain | All cause messages preserved, frames collapsed per cause |
test_stack_single_frame |
One-frame error | Pass through |
test_stack_no_app_frames |
All framework frames | Keep all (can't determine app frames) with warning |
| Test | Input | Expected output |
|---|---|---|
test_tree_deep_nesting |
5-level deep tree with 200 files | Leaf dirs collapsed: models/ (7 files), intermediate dirs preserved |
test_tree_flat |
3 files, no dirs | Pass through |
test_tree_single_deep_path |
src/a/b/c/d/file.rs |
Full path preserved (only 1 file) |
test_tree_mixed_depth |
Some dirs deep, some shallow | Collapse only leaf dirs with >3 files |
| Test | Input | Expected output |
|---|---|---|
test_table_markdown |
50-row markdown table | Header + (50 rows) + first 3 rows + last row |
test_table_tsv |
Tab-separated 100 rows | Same strategy, column alignment preserved |
test_table_small |
3-row table | Pass through (too small) |
test_table_wide |
20 columns | Keep all columns, compress rows |
| Test | Input | Expected output |
|---|---|---|
test_build_cargo_success |
Compiling 47 crates, no errors |
Compiling 47 crates...\nBuild succeeded |
test_build_cargo_errors |
45 Compiling + 2 errors + 1 warning | Compiling collapsed, errors/warnings in full |
test_build_npm_install |
200 added lines |
Installed 200 packages + any warnings |
test_build_mixed_output |
Compile + link + test output | Each phase collapsed separately |
test_build_all_errors |
Only errors, no success | Pass through all errors |
| Test | Input | Expected output |
|---|---|---|
test_prose_extractive_basic |
1000-word markdown doc | Top-K sentences by TF-IDF score, headings preserved |
test_prose_preserves_code_blocks |
Markdown with code fences | Code blocks passed through untouched, surrounding prose compressed |
test_prose_preserves_links |
Text with URLs and references | All URLs preserved in output |
test_prose_preserves_lists |
Bulleted/numbered lists | Lists preserved, prose paragraphs compressed |
test_prose_short_text |
50-word paragraph | Pass through (< 200 tokens threshold) |
test_prose_headings_only |
Just headings, no body | Pass through |
| Test | Input | Expected output |
|---|---|---|
test_search_summary_format |
Raw search output with 10 results | One-liner per result with score, kind, line range, edge counts |
test_search_detail_passthrough |
Same input with detail=true |
Raw output unchanged |
test_doc_context_summary |
Full get_doc_context output | Signature + edge summary + complexity, no source |
test_doc_context_edit_mode |
Same with for_edit=true |
Full source of target, summary of callers |
test_trace_summary |
trace_callers output depth=3 | Tree of names grouped by level, no source |
test_architecture_summary |
Full architecture output | Language breakdown + top-5 hotspots only |
test_refs_summary |
find_all_references output | File:line list grouped by file, no source |
#[cfg(test)]
mod edge_cases {
// Input size edge cases
#[test] fn test_empty_input() // → pass through, no crash
#[test] fn test_single_char() // → pass through
#[test] fn test_single_line() // → pass through (< 100 tokens)
#[test] fn test_exactly_100_tokens() // → boundary: bypass threshold
#[test] fn test_101_tokens() // → compress
#[test] fn test_50k_tokens() // → compress, verify no OOM or timeout
#[test] fn test_100k_tokens() // → compress within 50ms budget
// Content edge cases
#[test] fn test_unicode_cjk() // → token count differs from word count
#[test] fn test_unicode_emoji() // → preserve in output
#[test] fn test_binary_content() // → detect and pass through
#[test] fn test_mixed_content_json_in_log() // → classify as LogOutput, preserve JSON errors
#[test] fn test_mixed_content_code_in_markdown() // → classify as Markdown, preserve code blocks
#[test] fn test_ansi_color_codes() // → strip before classifying, preserve in output if configured
#[test] fn test_null_bytes() // → handle gracefully, don't panic
#[test] fn test_very_long_single_line() // → handle (some outputs have no newlines)
// Compression correctness
#[test] fn test_all_entity_names_preserved() // → every symbol/file name in raw appears in compressed
#[test] fn test_all_error_messages_preserved() // → errors never compressed away
#[test] fn test_line_numbers_preserved() // → file:line references intact
#[test] fn test_no_hallucinated_content() // → compressed output is subset of raw, never adds text
}- Create
tests/compression_eval/golden/directory - For each compressor, store input/output pairs as golden files:
golden/ json/ array_100.input.json array_100.expected.txt object_nested.input.json object_nested.expected.txt log/ repeated_info.input.txt repeated_info.expected.txt stack/ rust_panic.input.txt rust_panic.expected.txt ... - Test runner loads each pair, runs compressor, asserts output matches expected
- On intentional format change: update golden files, require review of diff
- CI gate: golden file test failures block merge
Verify that summary → detail=true returns ALL information from raw output.
#[cfg(test)]
mod round_trip {
// For every tool with summary/detail modes:
#[test]
fn test_search_round_trip() {
let raw = get_real_search_output();
let summary = compress_search(raw, detail=false);
let detail = compress_search(raw, detail=true);
// detail must equal raw (passthrough)
assert_eq!(detail, raw);
// summary must contain all result identifiers (file::symbol)
for result in parse_results(raw) {
assert!(summary.contains(&result.identifier));
}
}
#[test]
fn test_doc_context_round_trip() {
let raw = get_real_doc_context_output();
let summary = compress_doc_context(raw, detail=false);
// summary must contain: function name, all caller names, all callee names
let parsed = parse_doc_context(raw);
assert!(summary.contains(&parsed.function_name));
for caller in &parsed.callers {
assert!(summary.contains(&caller.name));
}
}
// Repeat for trace_callers, trace_callees, get_architecture, find_all_references
}// Using criterion for microsecond-accurate benchmarks
use criterion::{criterion_group, criterion_main, Criterion};
fn bench_compressors(c: &mut Criterion) {
let json_input = load_fixture("json_array_1000.json"); // ~10KB
let log_input = load_fixture("server_log_5000.txt"); // ~50KB
let stack_input = load_fixture("java_stacktrace.txt"); // ~5KB
let tree_input = load_fixture("large_repo_tree.txt"); // ~20KB
let build_input = load_fixture("cargo_build_output.txt"); // ~30KB
let prose_input = load_fixture("readme_large.md"); // ~15KB
c.bench_function("compress_json_10kb", |b| b.iter(|| compress_json(&json_input)));
c.bench_function("compress_log_50kb", |b| b.iter(|| compress_log(&log_input)));
c.bench_function("compress_stack_5kb", |b| b.iter(|| compress_stack_trace(&stack_input)));
c.bench_function("compress_tree_20kb", |b| b.iter(|| compress_file_tree(&tree_input)));
c.bench_function("compress_build_30kb", |b| b.iter(|| compress_build_output(&build_input)));
c.bench_function("compress_prose_15kb", |b| b.iter(|| compress_prose(&prose_input)));
c.bench_function("classify_content", |b| b.iter(|| classify_content(&json_input)));
c.bench_function("full_pipeline_50kb", |b| b.iter(|| compress_tool_output(&log_input, "bash", &args, &config)));
}
// Assertions (run as #[test], not benchmark):
// - Each individual compressor: < 10ms for typical input
// - Full pipeline: < 50ms for 50KB input
// - classify_content: < 1ms
// - No compressor allocates > 2x input size#[cfg(test)]
mod classifier {
#[test] fn test_classify_json_object() { assert_eq!(classify("{}"), Json); }
#[test] fn test_classify_json_array() { assert_eq!(classify("[1,2]"), JsonArray); }
#[test] fn test_classify_json_nested() { assert_eq!(classify(r#"{"a":{"b":1}}"#), Json); }
#[test] fn test_classify_log_syslog() { assert_eq!(classify("2026-07-10 12:00:00 INFO ..."), LogOutput); }
#[test] fn test_classify_log_json_structured() { assert_eq!(classify(r#"{"level":"info","ts":"..."}"#), LogOutput); } // JSON-structured logs → LogOutput not Json
#[test] fn test_classify_stack_rust() { assert_eq!(classify("thread 'main' panicked at ..."), StackTrace); }
#[test] fn test_classify_stack_python() { assert_eq!(classify("Traceback (most recent call last):"), StackTrace); }
#[test] fn test_classify_stack_java() { assert_eq!(classify("Exception in thread \"main\" java.lang.NullPointerException\n\tat com.foo.Bar.baz(Bar.java:42)"), StackTrace); }
#[test] fn test_classify_file_tree() { assert_eq!(classify("├── src/\n│ ├── main.rs"), FileTree); }
#[test] fn test_classify_table_markdown() { assert_eq!(classify("| col1 | col2 |\n| --- | --- |"), Table); }
#[test] fn test_classify_source_rust() { assert_eq!(classify("fn main() {\n println!(\"hello\");\n}"), SourceCode { language: "rust".into() }); }
#[test] fn test_classify_source_python() { assert_eq!(classify("def main():\n print('hello')"), SourceCode { language: "python".into() }); }
#[test] fn test_classify_markdown() { assert_eq!(classify("# Title\n\nSome paragraph text."), Markdown); }
#[test] fn test_classify_plain_text() { assert_eq!(classify("just some random text"), PlainText); }
#[test] fn test_classify_build_cargo() { assert_eq!(classify(" Compiling foo v0.1.0\n Compiling bar v0.2.0"), BuildOutput); }
#[test] fn test_classify_build_npm() { assert_eq!(classify("added 150 packages in 3s"), BuildOutput); }
#[test] fn test_classify_ambiguous_prefers_specific() // JSON-like log line → LogOutput wins over Json
#[test] fn test_classify_empty() { assert_eq!(classify(""), PlainText); }
#[test] fn test_classify_whitespace_only() { assert_eq!(classify(" \n\n "), PlainText); }
#[test] fn test_classify_with_hint() { assert_eq!(classify_with_hint("...", "json"), Json); } // hint overrides heuristic
}#[cfg(test)]
mod session_dedup {
#[test]
fn test_first_seen_passes_through() {
let mut ctx = SessionContext::new();
let output = "auth.rs::login source code...";
let result = apply_dedup(output, "auth.rs", "login", &mut ctx);
assert_eq!(result, output); // first time → full output
}
#[test]
fn test_same_hash_deduped() {
let mut ctx = SessionContext::new();
let output = "auth.rs::login source code...";
apply_dedup(output, "auth.rs", "login", &mut ctx); // first time
ctx.increment_turn();
let result = apply_dedup(output, "auth.rs", "login", &mut ctx);
assert!(result.contains("(seen in turn 1: auth.rs::login)"));
assert!(!result.contains("source code")); // full source removed
}
#[test]
fn test_different_hash_shows_full() {
let mut ctx = SessionContext::new();
apply_dedup("version 1", "auth.rs", "login", &mut ctx);
ctx.increment_turn();
let result = apply_dedup("version 2", "auth.rs", "login", &mut ctx);
assert_eq!(result, "version 2"); // content changed → show full
}
#[test]
fn test_stale_after_threshold_shows_full() {
let mut ctx = SessionContext::new();
let output = "auth.rs::login source code...";
apply_dedup(output, "auth.rs", "login", &mut ctx);
for _ in 0..11 { ctx.increment_turn(); } // 11 turns later
let result = apply_dedup(output, "auth.rs", "login", &mut ctx);
assert_eq!(result, output); // stale → show full again
}
#[test]
fn test_focus_set_never_deduped() {
let mut ctx = SessionContext::new();
ctx.set_focus(vec!["auth.rs".to_string()]);
let output = "auth.rs::login source code...";
apply_dedup(output, "auth.rs", "login", &mut ctx);
ctx.increment_turn();
let result = apply_dedup(output, "auth.rs", "login", &mut ctx);
assert_eq!(result, output); // in focus set → always full
}
#[test]
fn test_dedup_token_savings_logged() {
let mut ctx = SessionContext::new();
let output = "x".repeat(1000);
apply_dedup(&output, "big.rs", "func", &mut ctx);
ctx.increment_turn();
apply_dedup(&output, "big.rs", "func", &mut ctx);
assert!(ctx.dedup_tokens_saved > 900); // nearly all tokens saved
}
}#[cfg(test)]
mod concurrent_agents {
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::test]
async fn test_shared_context_concurrent_writes() {
let ctx = Arc::new(Mutex::new(SharedContext::new()));
let mut handles = vec![];
for i in 0..10 {
let ctx = ctx.clone();
handles.push(tokio::spawn(async move {
let mut ctx = ctx.lock().await;
ctx.store_snapshot(format!("key_{i}"), format!("data_{i}"), format!("agent_{i}"));
}));
}
for h in handles { h.await.unwrap(); }
let ctx = ctx.lock().await;
assert_eq!(ctx.snapshot_count(), 10);
}
#[tokio::test]
async fn test_shared_context_concurrent_read_write() {
let ctx = Arc::new(Mutex::new(SharedContext::new()));
ctx.lock().await.store_snapshot("key_0", "data_0", "agent_0");
let mut handles = vec![];
// 5 readers + 5 writers simultaneously
for i in 0..5 {
let ctx = ctx.clone();
handles.push(tokio::spawn(async move {
let ctx = ctx.lock().await;
ctx.get_snapshot("key_0") // reader
}));
let ctx2 = ctx.clone();
handles.push(tokio::spawn(async move {
let mut ctx = ctx2.lock().await;
ctx.store_snapshot(format!("key_{}", i+1), format!("data_{}", i+1), format!("agent_{}", i+1));
}));
}
for h in handles { h.await.unwrap(); }
}
#[tokio::test]
async fn test_dedup_across_agents() {
let ctx = Arc::new(Mutex::new(SharedContext::new()));
// Agent A analyzes auth.rs
{
let mut ctx = ctx.lock().await;
ctx.store_snapshot("trace:auth.rs:login", "12 callers in 5 files", "agent_a");
}
// Agent B requests same analysis
{
let ctx = ctx.lock().await;
let existing = ctx.get_snapshot("trace:auth.rs:login");
assert!(existing.is_some()); // cache hit, no re-analysis needed
assert_eq!(existing.unwrap().created_by, "agent_a");
}
}
#[tokio::test]
async fn test_lru_eviction() {
let mut ctx = SharedContext::with_max_snapshots(5);
for i in 0..10 {
ctx.store_snapshot(format!("key_{i}"), format!("data_{i}"), "agent");
}
assert_eq!(ctx.snapshot_count(), 5); // oldest 5 evicted
assert!(ctx.get_snapshot("key_0").is_none()); // evicted
assert!(ctx.get_snapshot("key_9").is_some()); // still there
}
}#[cfg(test)]
mod ratio_regression {
// These tests FAIL if compression ratio drifts beyond acceptable bounds.
// Update expected ratios intentionally when changing compressor logic.
#[test]
fn test_json_array_ratio() {
let input = load_fixture("json_array_1000.json"); // 1000-item array
let output = compress_json(&input);
let ratio = output.len() as f64 / input.len() as f64;
assert!(ratio < 0.15, "JSON array compression ratio {ratio:.2} exceeds 0.15 (>85% reduction expected)");
assert!(ratio > 0.01, "JSON array compression ratio {ratio:.2} suspiciously low — data loss?");
}
#[test]
fn test_log_repeated_ratio() {
let input = "[INFO] Processing item\n".repeat(500);
let output = compress_log(&input);
let ratio = output.len() as f64 / input.len() as f64;
assert!(ratio < 0.05, "Log compression ratio {ratio:.2} exceeds 0.05 (>95% reduction expected for repeated lines)");
}
#[test]
fn test_search_output_ratio() {
let input = load_fixture("search_20_results.txt"); // typical search output
let output = compress_search(&input, false);
let ratio = output.len() as f64 / input.len() as f64;
assert!(ratio < 0.25, "Search compression ratio {ratio:.2} exceeds 0.25 (>75% reduction expected)");
}
#[test]
fn test_trace_callers_ratio() {
let input = load_fixture("trace_callers_depth3.txt"); // 47 callers
let output = compress_trace(&input, false);
let ratio = output.len() as f64 / input.len() as f64;
assert!(ratio < 0.20, "Trace compression ratio {ratio:.2} exceeds 0.20 (>80% reduction expected)");
}
#[test]
fn test_stack_trace_ratio() {
let input = load_fixture("rust_panic_30_frames.txt");
let output = compress_stack_trace(&input);
let ratio = output.len() as f64 / input.len() as f64;
assert!(ratio < 0.30, "Stack trace compression ratio {ratio:.2} exceeds 0.30 (>70% reduction expected)");
}
// Smoke test: NO compressor should ever INCREASE output size
#[test]
fn test_no_compressor_increases_size() {
let fixtures = load_all_fixtures();
for (name, input) in fixtures {
let output = compress_tool_output(&input, "test", &default_args(), &config());
assert!(output.len() <= input.len() + 50, // +50 for metadata headers
"Compressor increased size for {name}: {input_len} → {output_len}",
input_len = input.len(), output_len = output.len());
}
}
}#[cfg(test)]
mod integration {
// End-to-end: call tool → compress → verify output is useful
#[tokio::test]
async fn test_search_compressed_still_parseable() {
let server = start_test_mcp_server(compression_enabled=true);
let result = server.call_tool("search", json!({"query": "login", "path": "/test/repo"})).await;
// Compressed output must contain:
assert!(result.contains("results for")); // header
assert!(result.contains("::")); // symbol references
assert!(result.contains("detail=true")); // retrieval hint
}
#[tokio::test]
async fn test_detail_retrieval_works() {
let server = start_test_mcp_server(compression_enabled=true);
let summary = server.call_tool("search", json!({"query": "login"})).await;
assert!(!summary.contains("fn login")); // no source in summary
let detail = server.call_tool("search", json!({"query": "login", "detail": true})).await;
assert!(detail.contains("fn login")); // full source in detail
}
#[tokio::test]
async fn test_compression_metrics_logged() {
let server = start_test_mcp_server(compression_enabled=true);
server.call_tool("search", json!({"query": "login"})).await;
let metrics = read_metrics_file();
assert_eq!(metrics.len(), 1);
assert!(metrics[0].compression_ratio < 1.0);
assert!(metrics[0].raw_tokens > metrics[0].compressed_tokens);
}
#[tokio::test]
async fn test_bypass_on_error() {
let server = start_test_mcp_server(compression_enabled=true);
let result = server.call_tool("search", json!({"query": "login", "path": "/nonexistent"})).await;
// Error output should NOT be compressed
let metrics = read_metrics_file();
assert!(metrics.last().unwrap().bypassed);
}
#[tokio::test]
async fn test_session_dedup_across_calls() {
let server = start_test_mcp_server(compression_enabled=true);
let r1 = server.call_tool("search", json!({"query": "login"})).await;
let r2 = server.call_tool("search", json!({"query": "login"})).await;
// Second call should have "(seen)" markers
assert!(r2.contains("(seen"));
assert!(r2.len() < r1.len()); // dedup made it shorter
}
#[tokio::test]
async fn test_full_pipeline_under_50ms() {
let server = start_test_mcp_server(compression_enabled=true);
let start = Instant::now();
server.call_tool("search", json!({"query": "login"})).await;
let elapsed = start.elapsed();
let baseline = measure_without_compression();
let overhead = elapsed - baseline;
assert!(overhead < Duration::from_millis(50), "Compression overhead {overhead:?} exceeds 50ms");
}
}Update the file structure section to add test files. Find the existing tests/compression_eval/ block and replace with:
tests/compression_eval/
tasks.json — 20 eval task definitions
baseline.json — Phase 0 baseline results
phase1.json — Phase 1 results
phase2.json — Phase 2 results
phase3.json — Phase 3 results
run_eval.rs — eval harness
golden/ — golden file snapshot tests
json/ — JSON compressor input/expected pairs
log/ — Log compressor input/expected pairs
stack/ — Stack trace compressor input/expected pairs
tree/ — File tree compressor input/expected pairs
table/ — Table compressor input/expected pairs
build/ — Build output compressor input/expected pairs
prose/ — Prose compressor input/expected pairs
tools/ — Per-tool compressor input/expected pairs
fixtures/ — Large realistic test inputs
json_array_1000.json
server_log_5000.txt
rust_panic_30_frames.txt
large_repo_tree.txt
cargo_build_output.txt
readme_large.md
search_20_results.txt
trace_callers_depth3.txt
crates/infigraph-mcp/src/compress/tests/
mod.rs — test module root
json_tests.rs — JSON compressor unit tests
log_tests.rs — Log compressor unit tests
stack_tests.rs — Stack trace compressor unit tests
tree_tests.rs — File tree compressor unit tests
table_tests.rs — Table compressor unit tests
build_tests.rs — Build output compressor unit tests
prose_tests.rs — Prose/ML compressor unit tests
tool_tests.rs — Per-tool compressor unit tests
classify_tests.rs — Content classifier unit tests
session_tests.rs — Session dedup unit tests
agent_tests.rs — Concurrent agent / SharedContext tests
ratio_tests.rs — Compression ratio regression tests
edge_cases.rs — Edge case test matrix
round_trip.rs — Summary → detail round-trip tests
| ID | Task | Quality check |
|---|---|---|
| S1 | Find where authentication is handled | Correct file + function identified |
| S2 | Find all API route definitions | All routes found (count match) |
| S3 | Find configuration loading code | Correct config source identified |
| S4 | Find test files for auth module | All test files found |
| S5 | Find error handling patterns | Correct pattern identified |
| ID | Task | Quality check |
|---|---|---|
| E1 | Fix null check bug in specific function | Same diff as uncompressed |
| E2 | Add parameter to function + update callers | All callers updated |
| E3 | Change return type of function | All type references updated |
| E4 | Add error handling to function | Correct error handling added |
| E5 | Add input validation | Correct validation logic |
| ID | Task | Quality check |
|---|---|---|
| R1 | Rename function across codebase | All references updated |
| R2 | Extract helper function | Correct extraction, callers updated |
| R3 | Move function to different module | Imports updated everywhere |
| R4 | Change function signature | All call sites updated |
| R5 | Remove dead code | Correct code removed, nothing else |
| ID | Task | Quality check |
|---|---|---|
| U1 | Explain authentication flow | Key steps mentioned (checklist) |
| U2 | Trace data from API to database | Correct path identified |
| U3 | Identify downstream dependencies | All deps found (count match) |
| U4 | Architecture overview | Key components mentioned |
| U5 | Blast radius of changing function X | Correct impact set |
For each task, record:
{
"task_id": "S1",
"phase": "baseline",
"tokens_input": 12500,
"tokens_output": 800,
"tool_calls": 3,
"tool_tokens": [4200, 3800, 2100],
"answer_correct": true,
"answer_quality": 5,
"detail_retrievals": 0,
"time_to_answer_ms": 8500
}| Score | Meaning |
|---|---|
| 5 | Perfect match with uncompressed answer |
| 4 | Same conclusion, minor detail difference |
| 3 | Correct direction, missing some context |
| 2 | Partially correct, important info lost |
| 1 | Wrong answer due to missing context |
Threshold: mean quality score ≥ 4.5 across all tasks per phase.
| Metric | Phase 2 | Phase 3 | Phase 4 | Phase 5 | Full |
|---|---|---|---|---|---|
| Token savings (mean) | ≥40% | ≥55% | ≥65% | ≥70% | ≥75% |
| Answer quality (mean) | ≥4.5/5 | ≥4.5/5 | ≥4.5/5 | ≥4.5/5 | ≥4.5/5 |
| Quality match rate | ≥95% | ≥95% | ≥95% | ≥95% | ≥95% |
| Detail retrieval rate | ≤25% | ≤20% | ≤20% | ≤15% | ≤10% |
| Latency overhead | ≤30ms | ≤40ms | ≤50ms | ≤50ms | ≤60ms |
| Multi-agent savings | — | — | — | ≥30% | ≥40% |
| Compression failures | ≤5% | ≤3% | ≤2% | ≤1% | ≤0.5% |
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| LLM produces wrong edits due to missing context | High | Medium | Never compress edit targets; progressive disclosure |
| Detail retrieval creates more tokens than savings | Medium | Low | Monitor retrieval rate; reduce compression if > 30% |
| Session context gets out of sync | Medium | Medium | Content hash verification; staleness threshold |
| Compression latency impacts responsiveness | Low | Low | All compression is simple string ops, < 50ms |
| Different LLMs need different compression levels | Medium | Medium | Make compression level configurable per model |
| Build output compression hides real errors | High | Low | Always preserve errors + warnings; test with real failures |
| JSON schema inference incorrect | Low | Medium | Fall back to truncation if schema can't be inferred |
| ML model dependency (Kompress) | Medium | Low | Default to extractive (no model); Kompress opt-in only |
| Cross-agent sync race conditions | Medium | Medium | SharedContext behind Mutex; content-hash based dedup is idempotent |
| Cache alignment breaks on provider changes | High | Low | Monitor cache_read_tokens; alert if cache hit rate drops below 80% |
| Context compaction loses compression state | Medium | High | Persist SessionContext to LM2; restore on session resume |
| Token counting inaccuracy skews budget decisions | Medium | Medium | Calibrate tokenizer per content type; use tiktoken-rs for accuracy |
| Smart prefetch wastes tokens on wrong predictions | Low | Medium | Track prefetch accuracy; auto-disable if < 50% hit rate |
crates/infigraph-mcp/src/
compress/
mod.rs — CompressionConfig, compress_tool_output()
classify.rs — ContentType enum, classify_content()
tools.rs — per-tool compressors (search, doc_context, etc.)
generic.rs — generic compressors (json, log, stack, tree, table)
session.rs — SessionContext, seen-dedup logic
metrics.rs — CompressionMetrics, logging, stats
budget.rs — budget tracking, auto-level selection
cache.rs — CacheAligner, live-zone gate, frozen prefix detection
ml.rs — ML extractive summarizer, optional Kompress integration
agents.rs — SharedContext, agent provenance, cross-agent dedup
tests/compression_eval/
tasks.json — 20 eval task definitions
baseline.json — Phase 0 baseline results
phase1.json — Phase 1 results
phase2.json — Phase 2 results
phase3.json — Phase 3 results
run_eval.rs — eval harness
golden/ — golden file snapshot tests
json/ — JSON compressor input/expected pairs
log/ — Log compressor input/expected pairs
stack/ — Stack trace compressor input/expected pairs
tree/ — File tree compressor input/expected pairs
table/ — Table compressor input/expected pairs
build/ — Build output compressor input/expected pairs
prose/ — Prose compressor input/expected pairs
tools/ — Per-tool compressor input/expected pairs
fixtures/ — Large realistic test inputs
json_array_1000.json
server_log_5000.txt
rust_panic_30_frames.txt
large_repo_tree.txt
cargo_build_output.txt
readme_large.md
search_20_results.txt
trace_callers_depth3.txt
crates/infigraph-mcp/src/compress/tests/
mod.rs — test module root
json_tests.rs — JSON compressor unit tests
log_tests.rs — Log compressor unit tests
stack_tests.rs — Stack trace compressor unit tests
tree_tests.rs — File tree compressor unit tests
table_tests.rs — Table compressor unit tests
build_tests.rs — Build output compressor unit tests
prose_tests.rs — Prose/ML compressor unit tests
tool_tests.rs — Per-tool compressor unit tests
classify_tests.rs — Content classifier unit tests
session_tests.rs — Session dedup unit tests
agent_tests.rs — Concurrent agent / SharedContext tests
ratio_tests.rs — Compression ratio regression tests
edge_cases.rs — Edge case test matrix
round_trip.rs — Summary → detail round-trip tests
| Phase | Effort | Depends on |
|---|---|---|
| Phase 0: Baseline | 1 day | — |
| Phase 1: Cache alignment | 1-2 days | — |
| Phase 2: Tool output shaping | 3-4 days | Phase 0 |
| Phase 3: Session tracking | 2 days | Phase 2 |
| Phase 4: Generic compressors + ML | 3-4 days | Phase 2 |
| Phase 5: Cross-agent sharing | 2-3 days | Phase 3 |
| Phase 6: Budget-aware | 1-2 days | Phase 3 |
| Phase 7: Compress MCP tool | 1 day | Phase 4 |
| Phase 8: A/B + rollout | 3 weeks (gradual) | All phases |
Total build time: ~15-18 days + 3 weeks rollout monitoring