Skip to main content
Two systems handle conversations that exceed model context limits:
  1. Context compaction — automatically summarizes conversation history when the context window fills up, preserving recent turns and discarding older ones.
  2. Semantic memory — indexes discarded messages so the agent can retrieve past context on demand via the memory_search tool.
Together they allow agents to maintain coherent multi-turn sessions that exceed any single model’s context limit.

What this guide is for

Use this guide when you want to understand or configure:
  • long-horizon conversation behavior
  • semantic recall via memory_search
  • compaction thresholds
  • the relationship between compaction and semantic memory

Feature flags

Compaction and semantic memory have separate build-time and runtime switches.
The CLI’s default features include the memory backend/session wiring, but not session-compaction. For no-default/custom source builds, enable the memory features explicitly when you need memory_search.
At runtime, AgentFactory::memory(true) enables the semantic memory path (HnswMemoryStore + memory_search). Compaction is controlled independently by whether the binary is built with session-compaction and whether a compactor is wired into the session path.
That means these cases are all valid:
  • compaction enabled, semantic memory disabled
  • semantic memory enabled, compaction unavailable
  • both enabled together

CompactionConfig

Controls when and how compaction runs.

How compaction triggers

Compaction is checked at every turn boundary, just before the next LLM call. The decision flow:
1

Skip turn 0

The first turn always skips compaction.
2

Loop guard check

If compaction occurred at turn N, no compaction until turn N + min_turns_between_compactions.
3

Dual threshold evaluation

Compaction triggers if EITHER:
  • last_input_tokens >= auto_compact_threshold (input tokens from the last LLM response), OR
  • estimated_history_tokens >= auto_compact_threshold (JSON bytes of all messages / 4).

What happens during compaction

When compaction triggers:
1

Emit CompactionStarted event

Emitted with input/estimated token counts and message count.
2

Send compaction prompt to LLM

The current conversation history plus a compaction prompt is sent to the LLM with no tools and max_summary_tokens as the response limit.
3

Handle result

On failure: a CompactionFailed event is emitted and the session is not mutated (safe failure).On success: DefaultCompactor::rebuild_history produces new messages:
  • System prompt is preserved verbatim (if present).
  • A summary message is injected as a User message with the prefix [Context compacted].
  • The last recent_turn_budget complete turns are retained.
  • All other messages become discarded.
4

Index discarded memory

If semantic memory is enabled, discarded messages are indexed before the compacted history is committed.
5

Replace session messages

The session messages are replaced with the rebuilt history after memory indexing accepts the discarded content.
6

Record usage and emit completion event

Compaction usage is recorded against the session and budget. A CompactionCompleted event is emitted with summary token count and before/after message counts.
The compactor sends this prompt to the LLM:
You are performing a CONTEXT COMPACTION. Your job is to create a handoff summary so work can continue seamlessly. Include:
  • Current progress and key decisions made
  • Important context, constraints, or user preferences discovered
  • What remains to be done (clear next steps)
  • Any critical data, file paths, examples, or references needed to continue
  • Tool call patterns that worked or failed
Be concise and structured. Prioritize information the next context needs to act, not narrate.

Memory indexing after compaction

When both a Compactor and a MemoryStore are wired into the agent, discarded messages are indexed into semantic memory before compacted history is committed. If the memory store rejects indexing, Meerkat preserves the original history, emits a CompactionFailed event, and skips that compaction attempt instead of dropping the only authoritative copy of the discarded text. For each discarded message:
  • The message’s indexable text content is extracted via message.as_indexable_text().
  • If non-empty, it is indexed with MemoryMetadata containing the session ID, the typed source handle (the offset range of the source message(s)), and a timestamp.
This means previously discarded conversation content becomes searchable via the memory_search tool.

The memory_search tool

When memory is enabled, the agent gains a memory_search tool.

Tool definition

Parameters

query
string
required
Natural language search query describing what you want to recall.
limit
integer
default:"5"
Maximum number of results to return. Capped at 20.

Response format

Returns a JSON array of result objects:
content
string
The text content of the memory entry.
score
number
Similarity score from 0.0 (no match) to 1.0 (exact match). Typical useful matches are above 0.7.
source_range
object
The half-open [start, end) offset range of the source message(s) the entry was indexed from. Memory is scoped to a single session; results do not carry a session_id, and there is no cross-session recall.

Memory store implementations

Uses:
  • hnsw_rs (v0.3) for approximate nearest-neighbor search with cosine distance.
  • SQLite for persistent metadata and text storage.
Storage layout: {store_path}/memory/memory.sqlite3Key characteristics:
  • Embedding: Bag-of-words TF with hash-based dimensionality reduction (4096-dimensional vectors, L2-normalized). Each word is hashed to a bucket and its presence increments that dimension.
  • Persistence: Data survives process restart. open() runs a one-time in-place schema migration (indexed session_id projection column + durable point-ID allocator table, inside one BEGIN IMMEDIATE transaction) and an idempotent heal of legacy rows; it scans and embeds nothing. A scope’s HNSW graph is built lazily on first use (search / index / enumerate / drop), so opening the store no longer pays for every session in the realm.
  • Scoping: One HNSW index per session owner, built on demand from the scope’s durable rows.
  • Lifecycle: drop_scope(owner) deletes a scope’s durable rows all-or-nothing and drops its live index (dropped point IDs are never reused); enumerate_scoped(scope, request) pages raw scope rows in durable-id order with optional source_range-overlap and indexed_after filters (a zero limit is rejected with a typed error).
  • Score conversion: HNSW cosine distance (0 = identical, 2 = opposite) is converted to a 0..1 similarity score: score = 1.0 - (distance / 2.0).
  • Thread safety: Point IDs are allocated transactionally from the durable allocator table (never reused, collision-free across concurrent store instances). Insertions, scope drops, and lazy scope loads are serialized via a Mutex; the scoped-index map sits behind a RwLock for concurrent searches.
  • Parameters (HnswParams defaults): max_nb_connection = 16, max_layer = 16, ef_construction = 200, ef_search = 200.

How memory gets wired

When the memory-store-session feature is compiled in and memory is enabled:
  1. An HnswMemoryStore is opened at {store_path}/memory/.
  2. The memory_search tool is added to the agent’s tool set.
  3. A DefaultCompactor is attached only if session-compaction is also enabled.
  4. A built-in memory-retrieval skill is injected into the system prompt, teaching the agent how to use memory search.

Examples

Custom CompactionConfig

See also