Package: internal/session/compaction.go
Context compaction summarizes conversation history when the token count approaches the model's context limit, allowing sessions to continue indefinitely.
type CompactionConfig struct {
MaskObservations bool // replace old tool outputs with "[output masked]"
MinPreserve int // minimum tokens to keep uncompacted (2000)
MaxPreserve int // maximum tokens to keep uncompacted (15000)
MaxMessages int // trigger compaction when message count exceeds this (80)
}| Constant | Value | Description |
|---|---|---|
MinPreserveRecentTokens |
2,000 | Floor for preserve boundary |
MaxPreserveRecentTokens |
15,000 | Ceiling for preserve boundary |
defaultMaxMessages |
80 | Message count trigger |
compactionCircuitBreakerThreshold |
3 | Max compaction attempts before giving up |
maxTextChars |
2,000 | Truncation limit for text parts in compaction prompt |
maxToolArgsChars |
500 | Truncation limit for tool args in compaction prompt |
maxToolOutputChars |
2,000 | Truncation limit for tool output in compaction prompt |
preserveRecentOutputs |
5 | Tool results preserved when masking observations |
The processor calls checkCompaction() after each LLM step. Compaction triggers when:
- The message count exceeds
MaxMessages(default 80), OR - The estimated token count approaches the model's context limit
A circuit breaker prevents infinite compaction loops — after compactionCircuitBreakerThreshold (3) compactions within a single Process() call, no further compaction attempts are made.
When MaskObservations is true (default), older tool outputs are replaced with "[output masked for compaction]" before building the compaction prompt. The most recent preserveRecentOutputs (5) tool results are preserved verbatim.
This reduces the compaction prompt size while keeping recent context intact.
The compaction prompt is built by buildCompactionPrompt():
<task>
Summarize the conversation so far...
(9-section summary template)
</task>
<conversation>
<prior-summary>
(previous summary, if any)
</prior-summary>
<user>
[text content, truncated to 2000 chars]
[tool_call: name(args truncated to 500 chars)]
</user>
<assistant>
[text content, truncated to 2000 chars]
[tool_result: name = output truncated to 2000 chars]
[reasoning: content truncated to 2000 chars]
</assistant>
</conversation>
<read-files>
/path/to/file1.go
/path/to/file2.go
</read-files>
<modified-files>
/path/to/changed.go
</modified-files>The compaction prompt requests these sections:
- Primary Request and Intent — User's ultimate goal
- Key Technical Concepts — Architecture, patterns, decisions
- Files and Code Sections — Paths, functions, code discussed/modified
- Errors and fixes — Problems and solutions
- Problem Solving — Key decisions and rationale
- All user messages — Every user instruction, in order
- Pending Tasks — Remaining work
- Current Work — What was being worked on at compaction time
- Optional Next Step — Most logical next action
trackFiles() scans messages for file paths:
| Tool | Tracked As |
|---|---|
read |
Read file (from file_path arg) |
write, edit, patch, apply_patch |
Modified file (from file_path/path/file arg) |
shell, bash |
Both (regex extraction from command string) |
LazyEstimator provides cheap token estimates without a tokenizer:
type LazyEstimator struct {
tokensPerChar float64 // default: 0.25 (~4 chars per token)
}FindPreserveBoundary() walks messages from newest to oldest, spending up to budgetTokens (clamped to [MinPreserve, MaxPreserve]). Returns the index where the "preserve" zone starts — messages before this index are candidates for compaction.
type CompactionResult struct {
Summary string // LLM-generated summary
ReadFiles []string // files that were read during the session
ModifiedFiles []string // files that were modified
PreTokens int // estimated tokens before compaction
PostTokens int // estimated tokens after compaction
CompactionNum int // 1-indexed compaction count for this session
}After compaction, the processor replaces old messages with the summary as a user-role message, preserving only the "preserve boundary" tail of recent messages. The prior summary is passed to subsequent compactions for incremental refinement. A blank summary (empty or whitespace-only) is treated as a compaction failure — the original messages are left in place.
Manual compaction uses the same Processor.Compact path:
- TUI:
/compact(optional focus instructions) - HTTP:
POST /session/{id}/summarize— returns200with{"compacted": bool}(implemented; not501)