Context limits

Long-running agent sessions accumulate conversation history. Compaction selects which turns to retain when that history exceeds the context budget.

Selection can use age, summaries, or relevance scores. chitta adds an estimate of coverage in the memory store to its scoring formula.

A debugging turn may contain a root cause recorded nowhere else. A turn with a similar stored memory receives a lower novelty contribution; embedding similarity does not establish that every detail is preserved.

Compaction inputs

Generic (e.g. Morph Compact)
Stateless scoring
  • Scores each turn purely by text relevance to next task
  • No knowledge of what the agent has already learned
  • Does not query a persistent memory store
  • 50–70% reduction, verbatim survivors
  • Filters conversation text
Chitta — Memory-aware
Memory coverage scoring
  • Checks each turn against what’s already in memory
  • High memory coverage → lower novelty score
  • Low memory coverage → higher novelty score
  • Recency and structural scoring when embedder unavailable
  • Integrated with PreCompact hook automatically

Scoring pipeline

The scoring formula

Every non-system message gets a score. System messages are always kept. The score balances three signals:

score(msg) = structural_weight × (
  0.3 × recency
  + 0.4 × semantic_sim(msg, query)
  + 0.3 × (1 − memory_coverage)
)

// memory_coverage: cosine sim vs nearest 3 soul memories
// high coverage → already learned → cheaper to drop
ComponentWhat it measuresWeight
structural_weightRole importance: system=2.0, user=1.0, assistant=0.8, tool=0.5multiplier
recencyLinear 0.1 (oldest) → 1.0 (most recent)0.3
semantic_simCosine similarity of turn embedding vs. upcoming query0.4
memory_coverageMax cosine sim vs. nearest 3 soul memories (higher = already learned)0.3 (inverted)
The algorithm
1
Estimate tokens
Word count × 1.3 per message; no tokenizer dependency.
2
Pre-embed all messages once
The configured daemon embedder produces vectors, normally through an in-process GGUF model and llama.cpp. Per-message embeddings are reused for semantic similarity and memory coverage. Dimensions follow the compiled model identity; there is no fixed one-millisecond guarantee.
3
Score each turn
Query the memory graph via field_store_→recall(emb, k=3) to get memory_coverage. Compute cosine similarity vs. query embedding for semantic_sim.
4
Greedy keep by budget
Sort by score descending. Add messages until target_ratio of original tokens is filled. System messages bypass the budget check entirely.
5
Re-sort by original index
Survivors are returned in their original conversation order — verbatim, unmodified.

Live scoring demo

Scroll to animate. Each bar shows the computed score. Green = kept, faded = memory-covered drop.

compact_context — scoring pass
before: — after: — dropped: —

compact_context tool

ParameterTypeRequiredDescription
messagesarrayyesConversation turns [{role, content}] — OpenAI/Claude format
querystringnoUpcoming task hint — biases semantic scoring toward what’s needed next
target_ratiofloatnoFraction of tokens to keep (default: 0.4, range: 0.05–1.0)
distill_novelbooleannoOptional daemon pass, default false; requires an embedder and store. Records a capped excerpt of qualifying dropped turns. The advertised schema still calls it reserved; the PreCompact hook does not enable it.
Response
{
  "messages": [...],          // kept messages, original order, verbatim
  "stats": {
    "before_tokens": 48200,
    "after_tokens":  18600,
    "dropped":       34,
    "dropped_pct":   61.4,
    "embedding":     true     // false if embedder unavailable (graceful degradation)
  }
}
Direct CLI (JSON-RPC thin client)
echo '{
  "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": {
    "name": "compact_context",
    "arguments": {
      "messages": [...],
      "query": "implement the new feature",
      "target_ratio": 0.4
    }
  }
}' | chitta

Hook integration

The PreCompact hook calls compact_context for memory-aware scoring and records statistics. It does not replace the harness transcript with returned messages or control the harness's own compaction. Its current flow:

1
Parses the transcript JSONL
Converts the last 60 user/assistant turns into a messages array.
2
Calls compact_context
Scores all turns via the memory graph. Uses the pre-compact session snapshot as the query to bias scoring toward what’s contextually important.
3
Stores a compaction episode
Saves token stats as an episode memory: “48200→18600 tokens, 61% dropped as memory-covered”. The soul tracks its own compaction history.
4
Returns to the harness
The hook finishes after recording valid statistics. Transcript distillation is a separate lifecycle path; this response does not rewrite the active conversation.

You can also trigger compaction manually from any hook or script:

# In UserPromptSubmit hook — compact before long-session turns
MESSAGES=$(jq -sc '[.[] | select(.type=="user" or .type=="assistant") | ...] | .[-60:]' "$TRANSCRIPT_PATH")
echo "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"compact_context\",\"arguments\":{\"messages\":$MESSAGES,\"query\":\"$QUERY\",\"target_ratio\":0.4}}}" \
  | chitta

Optional recording of novel drops

The daemon implements distill_novel: true in compact.cpp, default false. With an embedder and store, it classifies dropped turns by a substring heuristic, skips turns shorter than 40 characters or already covered by memory, and records at most the first 600 characters with a provenance prefix.

This is a bounded excerpt, not lossless preservation or LLM summarization. The hook does not enable the option. Retain the source transcript when exact evidence matters.