?

The problem

A session that runs for weeks accumulates hundreds of messages. Every one is re-sent to the provider on every turn. Cost grows linearly with turn count; latency grows too. Rousseau's LLMCompressor (internal/agent/compressor.go) trades a small one-off cost — one summarisation call per compression — for permanent savings on every subsequent turn.

Compression is off by default because the reference deployment uses claudecli on a subscription tier, where token count is not billed. Turn it on when running against Anthropic direct, Bedrock, Vertex, or OpenAI-compatible pay-per-token providers.

The knobs

From CompressionConfig in internal/config/config.go:

agent:
  compression:
    enabled: true
    trigger_messages: 60        # zero uses the default 60
    keep_recent: 8              # zero uses the default 8
    prompt: ""                  # overrides the default summariser prompt

Meanings:

Field What it does
enabled Turn compression on. When false, the agent uses NoopCompressor and this whole section is a no-op.
trigger_messages Compression fires once len(session.Messages) >= trigger_messages.
keep_recent Number of most-recent messages preserved verbatim after compression.
prompt Overrides the default summariser prompt. Set only if you need custom instructions (e.g. preserve JSON output, always cite file paths).

The default summariser prompt

Summarise the following conversation in <=200 words. Preserve every
commitment, TODO, credential, filename, and quoted output. Skip
pleasantries. Return only the summary — no preamble.

Defined as defaultSummaryPrompt in internal/agent/compressor.go. Override with agent.compression.prompt in config.yaml.

Before / after

A session of 68 messages, trigger_messages: 60, keep_recent: 8:

Before compression:                        After compression:

┌──────────────────────────┐              ┌──────────────────────────────┐
│ msg[0]  user             │              │ msg[0]  user (synthetic)     │
│ msg[1]  assistant        │              │   [rousseau-compressed]      │
│ msg[2]  user             │              │   (summary of prior 60       │
│  …  (60 messages)        │      →       │    messages): …              │
│ msg[59] assistant        │              ├──────────────────────────────┤
├──────────────────────────┤              │ msg[1]  user       — verbatim │
│ msg[60] user   verbatim  │              │ msg[2]  assistant  — verbatim │
│ msg[61] assistant        │              │ msg[3]  user       — verbatim │
│  …                       │              │ msg[4]  assistant  — verbatim │
│ msg[67] assistant        │              │ msg[5]  user       — verbatim │
└──────────────────────────┘              │ msg[6]  assistant  — verbatim │
                                          │ msg[7]  user       — verbatim │
                                          │ msg[8]  assistant  — verbatim │
                                          └──────────────────────────────┘
Total messages: 68                        Total messages: 9
Input tokens: ~5000 per turn              Input tokens: ~800 per turn

The marker

The compressor prefixes the synthetic user message with [rousseau-compressed] (constant DefaultCompressorMarker in internal/agent/compressor.go). On subsequent turns, headAlreadyCompressed() uses the marker to detect an already-compressed prefix and skips repeat compression unless the session has grown to 2 * trigger_messages.

This is what keeps compression bounded — you don't pay to re-summarise the summary every 60 messages.

Choosing values

Situation Recommended
Long-running transport daemon on a paid provider. trigger_messages: 60, keep_recent: 8. Defaults are tuned for this.
Interactive TUI where you want everything in context. enabled: false.
Highly technical sessions with lots of quoted code / logs. trigger_messages: 40, keep_recent: 12. Preserve more recent context; compress sooner.
Cost-critical batch summariser (cron). Each cron run is a fresh session, so compression rarely triggers. Leave defaults on.

Cost of a compression pass

One summarisation call per firing. The Provider used is whatever Config.Provider selects — the same one the agent uses. That means:

  • Sonnet-class compressor call: ~1-2 seconds, roughly the cost of ~2 turns' worth of input tokens.
  • Break-even after ~5-10 subsequent turns depending on session shape.

For a cheaper compressor, run rousseau in the two-daemon multi-provider pattern with a Haiku-class model for the compressor daemon. See Guides: Multi-provider.

Emergency: session is too large to load

If a session's payload grows past the model's context window before compression fires — rare but possible with a very small trigger_messages and large tool outputs — the next turn will fail with a provider "context length exceeded" error. Recovery:

rousseau session delete <id> --yes

Then start fresh. Or manually shrink via SQLite:

sqlite3 ~/.local/share/rousseau/sessions.db <<'SQL'
UPDATE sessions SET payload = json_set(payload, '$.messages',
  json_extract(payload, '$.messages[-8:]'))
WHERE id = '<session-id>';
SQL

Note: the exact JSON path syntax depends on SQLite version. Confirm with a SELECT payload first.

Related

Type to search 150+ pages. Ranking: BM25 with title/description boost.