Per-turn LLM compression runs as a post-dedup step inside
memory(action="capture"). The goal is roughly 3x token
reduction at >=0.9 fact retention so memory recall quality stays
intact while storage + retrieval costs drop.
Compression fires automatically when ALL of the following are true:
memory(action="capture", text=...)is called.- The dedup probe did NOT short-circuit (no near-duplicate exists).
COMPRESSION_ENABLED=true(the default).- At least one LLM provider env key is set
(
GEMINI_API_KEY/OPENAI_API_KEY/ANTHROPIC_API_KEY/XAI_API_KEY), ORCOMPRESSION_PROVIDERexplicitly names one.
If any of these is false, the row stores the raw text with
compressed=false and the pipeline gracefully skips. No exception is
raised; future rows can still compress once you configure a provider.
The compression prompt explicitly instructs the model to retain:
- Concrete facts (numbers, dates, prices)
- Decisions ("we picked X because Y")
- Preferences ("I always want Z")
- Names (people, products, places)
- File paths + URLs
- Code identifiers (function / variable / class names)
- Quoted phrases
Filler text (greetings, hedging, repeated context, transitional phrases) is dropped.
- Filler / transitional words ("so", "well", "you know")
- Repeated context within the same turn
- Hedging that does not change meaning
- Pleasantries
When the LLM call fails (SDK exception, empty response, rate limit, env missing), compression silently degrades to a raw store. The row is still captured -- you never lose data because the compression provider is flaky. Counts:
tokens_inalways reflects the original text.tokens_out == tokens_inon graceful skip (no rewrite happened).compressed == Falseon graceful skip.
Set LOG_LEVEL=DEBUG to see when compression skipped and why.
For rows captured before COMPRESSION_ENABLED was true (e.g. older
data carried through an upgrade), call:
memory(action="compress", memory_id="<id>")
This reruns the LLM compression pipeline on the existing row, updating
content + text_raw + compressed + compression_provider +
updated_at. Already-compressed rows return status=already_compressed
without burning an LLM call.
COMPRESSION_ENABLED=false
When disabled, capture stores the raw text always. Useful when:
- You want byte-exact transcripts for audit
- You are debugging a memory recall regression and want to rule out compression as a variable
- You want to avoid LLM API costs for high-volume captures
COMPRESSION_PROVIDER=openai
COMPRESSION_MODEL=gpt-5-mini
When set, these win over the auto-detected provider priority
(Gemini > OpenAI > Anthropic > xAI from llm.detect_provider). Use this
when you want compression on a different provider than the LLM you use
for graph extraction or importance scoring.
Default order (matches llm.detect_provider):
- Gemini (
GEMINI_API_KEYorGOOGLE_API_KEY) - OpenAI (
OPENAI_API_KEY) - Anthropic (
ANTHROPIC_API_KEY) - xAI (
XAI_API_KEY)
Free-tier-friendly default: Gemini Flash gives you reasonable compression quality at zero cost up to the free quota.
capture(text)
-> dedup probe
-> compress(text)
-> tiktoken.encode(text) -> tokens_in
-> llm.call_llm(COMPRESSION_PROMPT.format(text=text),
provider=resolved, model=resolved,
temperature=0.0,
max_tokens=tokens_in // 2)
-> tiktoken.encode(result) -> tokens_out
-> {text, text_raw, compressed=True, provider, tokens_in, tokens_out}
-> db.add_with_context_type(content=result.text,
text_raw=result.text_raw,
compressed=True,
compression_provider=...)
tiktoken cl100k_base is the tokenizer (matches OpenAI + Anthropic
estimates closely enough for the 3x metric). The prompt asks for "<= 1/3
of the original tokens" so a moderate compression ratio is the explicit
target -- the model is not asked to be maximally terse, just to drop
filler.