?

Two problems, two mechanisms

  • A single long session can outgrow the model's context window. Compression collapses old messages into a summary block so the loop keeps working.
  • A new session on a related topic loses the value of prior conversations. Recall queries the FTS5 index across sessions and splices excerpts into the system prompt.

Compression edits the current session in place. Recall never edits — it appends context to the system prompt for the current turn.

Compression

internal/agent/compressor.go implements an LLM-backed summariser. The agent loop consults it at the start of every Turn:

if changed, err := a.opts.Compressor.Compress(ctx, s); err != nil {
    a.logger.Warn("agent.compress_failed", slog.String("err", err.Error()))
} else if changed {
    a.logger.Info("agent.compressed", slog.Int("messages", len(s.Messages)))
}

If the session is short, nothing happens. Once the message count crosses trigger_messages, the compressor:

  1. Isolates the tail of the session — the most recent keep_recent messages — and preserves them verbatim.
  2. Feeds everything older to the provider with a summarisation prompt.
  3. Replaces the older block with a single synthetic RoleSystem message containing the summary.
  4. Marks the session so the summary block sits in the prompt-cache-eligible prefix on the very next provider call.

The loop then proceeds against the smaller message list. The user never sees the seam.

Enabling compression

agent:
  compression:
    enabled: true
    trigger_messages: 60      # zero → default 60
    keep_recent: 8            # zero → default 8
    prompt: ""                # zero → sensible default
Field Default Meaning
enabled false Off by default.
trigger_messages 60 Message count above which compression fires.
keep_recent 8 How many recent messages to preserve verbatim.
prompt built-in Overrides the summarisation instruction.

When to leave it off

Compression uses one provider round-trip per fire. On a subscription-tier claudecli account, that trip is free — enable freely. On a pay-per-token API, every fire has a cost, so tune trigger_messages upward or keep it disabled for short-lived sessions.

When to leave it on

  • Long-lived chat-transport daemons where a WhatsApp thread grows over weeks.
  • Cron-scheduled prompts whose replies feed a follow-up prompt.
  • Self-hosted providers where token cost is zero.

Semantics preserved across compression

  • Tool-use / tool-result pairs are never split. If a tool_use is in the compressed region and its tool_result in the preserved region, both are collapsed into the summary.
  • The compressor never rewrites the current in-flight user turn.
  • Prompt caching (internal/llm/anthropic cache_control markers) is placed on the summary block so the next call reads it from the cache.

Recall

internal/state/sqlite/ maintains an FTS5 virtual table indexing every message. A RecallProvider runs a query against this table and returns a system-prompt appendix.

The interface

type RecallProvider interface {
    SystemAppendix(ctx context.Context, s *Session) string
}

The agent loop calls this once per iteration. When it returns non-empty text, the text is appended to the base system prompt for that iteration.

The default provider

internal/agent/recall.go ships a heuristic that:

  1. Extracts salient tokens from the current session's last user message.
  2. Runs MATCH against the FTS5 index for those tokens across other sessions.
  3. Formats the top N excerpts as a Previously in another session: block.
  4. Bounds the appendix so it never exceeds a configured character budget.

Enabling recall

Recall is wired at agent construction. See internal/cli/chat.go and internal/cli/*.go for how each transport wires it. In your own embedding:

recall, err := sqlitestore.NewRecall(store)
if err != nil { /* ... */ }

ag := agent.New(provider, registry, logger, agent.Options{
    RecallProvider: recall,
})

Interaction with the approver

Recall reads from the session store; it never fires a tool call. The approver is not consulted. The store contents themselves are the trust boundary.

Session search from the CLI

Recall is a machine-facing feature. For humans, the same FTS5 index powers:

rousseau session search "kubectl"
rousseau session search "PVC not binding"

Same query engine, same results, minus the LLM re-ranking that a proper RecallProvider might add.

Interaction with skills

Skills (Skills) and recall both add to the system prompt. They are composed in a fixed order:

  1. Base system prompt (from agent.system_prompt or the default).
  2. Skills appendix (if any).
  3. Recall appendix (if any).

Everything is separated by two newlines. If nothing needs adding, the base prompt goes through unchanged.

Semantics of the summary block

The synthetic summary message is emitted with RoleSystem. It is not a user or assistant message, so it never appears in rousseau session show as a conversational turn — it shows as [compressed summary] metadata.

If you resume a compressed session with rousseau chat --session <id>, the summary is preserved. Deleting the summary block via a hypothetical schema edit is unsafe: the model may reference facts only known through it.

Verifying compression is firing

INFO agent.compressed messages=12

messages is the new session length after the summary block replaced the compressed prefix. A WARN agent.compress_failed err=... means the summarisation provider errored; the loop continued against the uncompressed session.

Caveats

  • Compression is lossy. The summary is model-generated text; important details can be dropped. For audit trails, keep the full session in the store — compression only affects what the model sees, not what SQLite persists.
  • Recall requires the FTS5 SQLite extension. modernc.org/sqlite builds it in by default; if you swap the store implementation, ensure FTS5 is available.
  • Both features assume UTF-8 text. Voice-note transcripts (see Voice mode) count as regular user messages once transcribed.

Next

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