Conversation compression
This page covers the history cascade — one of four context-management mechanisms that keep a long-running conversation inside a model-readable size. The cascade is a four-layer pipeline that runs on every turn to shrink the message buffer while preserving every decision, fact, name, number, and constraint that matters. The layers fire only when the buffer is still over budget after the previous one; cheap layers run first, and a model pass is a last resort. The other three mechanisms — structured replay, working-set trimming, and a whole-history digest — are introduced further down this page.
Overview
- Module
src/lib/skills/memory.ts- Entry point
cascadeCompress(messages, options)- Layers
- 4, cascading, each only fires if still over budget
- Token budget
- Scales with the tier in use, not a fixed number
- Tool result threshold
- 1,500 characters
- Recent messages kept
- 10 by default
- Summary group size
- 6 messages
- Model compression tier
- Allegro
Long conversations break models. Once the buffer exceeds the model's context window the request fails outright; well before that, quality degrades as the model spends attention on stale content. The cascade exists so the chat handler can hand the model a clean, compact buffer on every turn, regardless of how long the underlying conversation has been running.
The order is intentional. The cheapest layer that fixes the problem runs first. If the buffer is bloated by a single huge tool result, layer 1 fixes it without touching anything else. If the buffer is long but mostly recent, layer 2 fixes it by dropping old messages. Only when the conversation is genuinely too rich to keep in full does the cascade reach layer 4 and spend a model call to write a structured summary.
Four mechanisms, not one
The history cascade documented on this page is one of four context-management mechanisms running today. They attack different shapes of the same problem.
| Mechanism | What it does | When it runs |
|---|---|---|
| History cascade (this page) | Progressively trims and summarizes older turns, cheapest step first, with a model pass only as a last resort. Recent, explicit instructions are always kept verbatim. | On a turn, only as needed |
| Structured replay | An append-only, faithful reconstruction of exactly what happened in a conversation, including tool calls and their results, with the stable parts of the prompt kept byte-identical turn to turn so repeated context is cheap to reprocess. | Not on for every conversation type |
| Working-set trimming | Keeps the most recent chunk of tool output in full detail and distills everything older into a one-line outcome. The detail is recoverable — the model can re-fetch it if it actually needs it again. | Automatically, every tool round |
| Whole-history digest | A hard ceiling backstop: if a conversation still risks overflowing, the oldest middle portion collapses to one line per turn while the very first and most recent turns stay verbatim. | Only when a conversation risks overflow; also powers the manual "compact this conversation" action |
Why a cascade
One pipeline, four stages, picked to attack different causes of bloat.
| Cause of bloat | Layer that fixes it | Cost |
|---|---|---|
| A single tool result returned tens of thousands of characters | Layer 1, truncate by length | A handful of substring slices |
| The conversation is 100 turns long but each turn is small | Layer 2, drop the oldest | Array splice |
| A genuinely long thread with no obvious trim victims | Layer 3, rule-based groups | String concatenation, no model call |
| A long thread where every message carries decisions worth preserving | Layer 4, model compression | One model request, capped at 15 seconds |
Entry point
One async function. Four options. Four return values.
The function takes a shallow clone of the input so it never mutates the caller's array. The layersApplied array tracks which layers ran for the current call; the return value exposes it so the caller can log compression behaviour without re-deriving it. The maxTokenBudget shown here is a default the caller can override — in practice the chat handler passes a budget appropriate to the tier serving the conversation, which is why the effective ceiling scales with the tier in use rather than being one fixed number in production.
Token budget
The gate that controls whether a layer fires — sized to the tier in use, not a fixed constant.
totalTokens is a fast approximation, not a tokenizer. The ceiling itself scales with the context size of whichever tier is serving the conversation — a request on a larger-context tier gets a larger runway before layers start firing, rather than every conversation being held to the same fixed number. Skill prompts that need a strict bound for their own purposes should rely on the model's own response budget, not on this estimate.Layer 1: Tool result truncation
The cheapest fix. Most context blow-ups come from one runaway tool result.
Tool results are the biggest individual contributors to context bloat. A scrape of a content-heavy page can return tens of thousands of characters; a list of leads from a CRM can be hundreds of kilobytes; a generated document can dwarf the rest of the conversation by an order of magnitude. Layer 1 catches those by spotting messages that look like raw tool output (assistant role, starts with a JSON token or a code fence, or simply over 4,000 characters) and clipping them to the configured threshold.
The truncation appends an explicit marker so the model knows the content was cut. This matters: a silently truncated tool result reads as if the tool returned exactly that much, which can mislead the next turn into incorrect assumptions about completeness.
Layer 2: Drop oldest
The blunt fix when the buffer is long but truncation did not get us there.
Layer 2 keeps the most recent recentKeep messages and drops everything before them. The default is 10. The assumption is that the most recent interactions carry the most contextual weight for the next turn; older content can be reconstructed through retrieval against persistent memory or, if needed, by reading the chat history directly.
The drop is lossy and intentional. Anything that mattered from the dropped range should have been written to persistent memory by an earlier turn. Skills designed for long sessions write memory aggressively for this reason.
Layer 3: Rule-based grouping
When neither truncation nor dropping is enough, replace older groups with one-line summaries.
Layer 3 protects the last four messages, then iterates over the rest in chunks of groupSize (default 6). Each chunk becomes a single synthetic assistant message that lists the role and the first hundred characters of each member. The result keeps the timeline visible (the order of the bullets reflects the original order of the messages) without paying for full content storage.
No model call is involved. The summaries are mechanical and deterministic. That is the point: layer 3 has to be cheap enough to run frequently without affecting turn latency or cost.
Layer 4: Model compression
The last resort. Hand the remaining buffer to a fast model and ask for a dense summary.
Layer 4 calls compressConversation, which splits the buffer into recent messages to keep verbatim and older messages to compress, then sends the older portion to a model on the Allegro tier with a structured system prompt. The model produces a bullet list of decisions, facts, names, numbers, and constraints. The result replaces the older portion of the buffer and the recent messages stay attached.
If the compression call fails for any reason, the cascade does not abort. It falls back to keeping only the last four messages, which guarantees a buffer the next turn can read even in degraded conditions.
compressConversation
The function layer 4 calls. Also usable standalone.
| Default | Value |
|---|---|
| Compression tier | Allegro |
| DEFAULT_RECENT_MESSAGES | 6 (kept verbatim, the rest is compressed) |
| Per-message truncation before sending to the compression tier | 2,000 chars |
| Compression max_tokens | 1,024 |
| API timeout | 15,000 ms (AbortSignal) |
| Fallback if the tier's credentials are unavailable | Returns a placeholder system message; no compression performed |
The compression prompt
The system prompt the model compressor runs under.
The prompt is the contract between the cascade and the model. The model is told to retain every load-bearing fact and to drop everything that is decoration. The output is forced into bullet points so the downstream turn sees a structured block that fits comfortably above the recent messages.
Result shape
What the caller gets back.
compressedContext field is empty when only layers 1 through 3 fired. The buffer alone is enough to deliver the next turn. The field is populated only when layer 4 produced a free-form summary the next turn should consider as background.Failure modes
The cascade is engineered to degrade gracefully. Here is how each layer fails.
| Failure | What happens |
|---|---|
| totalTokens estimate is wrong | A layer may fire unnecessarily or skip when it should run. Buffer is still readable; the next turn's response may be slightly trimmed or slightly noisy. |
| A tool result was structured but not detected by the layer 1 heuristic | The result is kept verbatim. Later layers still compress around it. |
| recentKeep is set higher than the buffer length | Layer 2 is a no-op. The cascade proceeds. |
| The compression tier times out at layer 4 | The buffer is hard-trimmed to the last 4 messages. The next turn still gets readable context but loses the older summary. |
| The compression tier's credentials are unavailable in production | compressConversation returns a placeholder system message. Layer 4 is effectively disabled; the buffer flows through unchanged after layer 3. |
| Compression returns malformed text | The bullet output goes through unchecked. The model on the next turn handles imperfect input; it does not crash the cascade. |
Structured replay
An append-only reconstruction of what actually happened, kept cheap to reprocess turn over turn.
Structured replay is an append-only, faithful reconstruction of exactly what happened in a conversation — including tool calls and their results — rather than a paraphrased summary. The stable parts of the prompt are kept byte-identical from one turn to the next, so a long conversation's shared prefix is cheap to reprocess instead of being rebuilt from scratch every turn.
Structured replay is not on for every conversation type. Where the history cascade documented above is the general-purpose mechanism, structured replay is a more specific reconstruction used where a faithful, tool-call-accurate record of the conversation matters more than a compact summary of it.
Working-set trimming
Runs automatically, every tool round, independent of the cascade above.
Working-set trimming keeps the most recent chunk of tool output in full detail and distills everything older into a one-line outcome. Unlike the cascade's layer 1 truncation, which only fires once the whole buffer is over budget, working-set trimming runs automatically on every tool round regardless of overall buffer size — it is a standing discipline on tool output specifically, not a budget-triggered layer.
The distillation is recoverable: if the model actually needs the detail from an older tool result again, it can re-fetch it rather than being permanently cut off from it, which is a different trade-off than the cascade's layer 1 truncation described above.
Whole-history digest
A hard ceiling backstop, and the same mechanism behind the manual "compact this conversation" action.
The whole-history digest is a hard ceiling backstop: if a conversation still risks overflowing after everything else, the oldest middle portion of the conversation collapses to one line per turn, while the very first turn and the most recent turns stay verbatim. It exists for the case none of the other mechanisms are built to fully absorb — a conversation that is long enough, and dense enough throughout, that even cascading compression and working-set trimming leave it near the ceiling.
The same mechanism powers the manual "compact this conversation" action available to a user — triggering it runs the identical whole-history collapse on demand rather than waiting for the ceiling to be reached automatically.
Tuning the cascade
Defaults are good for most chat sessions. Skills that need different behaviour pass options.
| Option | Default | When to change |
|---|---|---|
| maxTokenBudget | Scales with the active tier's context size | Lower for budget-constrained skills, higher for skills running on a tier with a bigger context window |
| maxToolResultChars | 1,500 | Raise when working with tools whose useful payload is naturally large; lower for cheaper retention |
| recentMessagesToKeep | 10 | Higher for skills that need long verbatim recall; lower for tight loops with mostly summary state |
| summaryGroupSize | 6 | Smaller groups give finer-grained summaries at layer 3; larger groups compress more aggressively |
File map
Glossary
- History cascade
- The four-layer compression pipeline documented on this page. Each layer only fires if the buffer is still over budget after the previous one. One of four context-management mechanisms in use today.
- Layer 1
- Truncation of oversized tool results above the per-message character threshold.
- Layer 2
- Hard drop of the oldest messages, keeping only the configured number of recent ones.
- Layer 3
- Rule-based grouping that replaces older message groups with synthesised assistant messages listing the role and first 100 characters of each.
- Layer 4
- Full model compression that sends the remaining buffer to a fast model with a structured system prompt and returns a bullet summary.
- Protected count
- The number of most recent messages layer 3 refuses to compress. Four by default.
- Token budget
- The maximum estimated token count the buffer is allowed to reach before layers start firing. Scales with the tier in use rather than a single fixed number.
- Compressed context
- The free-form summary string layer 4 produces. Returned alongside the compressed buffer for the next turn to consume.
- Layers applied
- An array of layer numbers that fired during the current call. Returned so callers can log compression behaviour.
- Structured replay
- An append-only, faithful reconstruction of a conversation including tool calls and results, with stable prompt portions kept byte-identical turn to turn. Not on for every conversation type.
- Working-set trimming
- Keeps the most recent chunk of tool output in full detail and distills older output into a one-line outcome, recoverable on demand. Runs automatically every tool round.
- Whole-history digest
- A hard ceiling backstop that collapses the oldest middle portion of a conversation to one line per turn while the first and most recent turns stay verbatim. Also powers the manual "compact this conversation" action.