T8 — Tail-turn preserving compaction
Tổng quan Context
opencode tính tailBudget = min(max(usable * 0.25, 2000), 8000)
— tức là tail chiếm 25% context usable, tối thiểu 2 000 token, tối đa 8 000 token.
Giới hạn trên 8 000 ngăn tail ăn quá nhiều không gian khi model có context window lớn.
Sau khi xác định tail, phần head (tất cả messages trước tail) được gửi cho một LLM call riêng để tóm tắt theo T12 template. Kết quả là một message hệ thống duy nhất chứa summary, theo sau bởi các tail messages. Session tiếp tục từ đây thông qua T11 continuation.
Phân tích code chi tiết Anatomy
Bước 1 — Tính tail budget
Budget được clamp vào khoảng [2 000, 8 000] để hoạt động đúng cả với model nhỏ (8k context) lẫn model lớn (200k context).
session/compaction.ts — tail budget
{`
const MIN_TAIL_TURNS = 2
const TAIL_RATIO = 0.25
const MIN_TAIL_TOKENS = 2_000
const MAX_TAIL_TOKENS = 8_000
function computeTailBudget(usable: number): number {
return Math.min(
MAX_TAIL_TOKENS,
Math.max(MIN_TAIL_TOKENS, Math.floor(usable * TAIL_RATIO))
)
}
`}Bước 2 — Backward walk để tìm tail
Đi ngược từ cuối messages, tích lũy token count vào tail[] cho đến
khi vượt tailBudget và đã có ít nhất MIN_TAIL_TURNS
turns. Điều kiện && đảm bảo luôn giữ tối thiểu 2 turns dù budget nhỏ.
session/compaction.ts — backward walk
{`
function splitHeadTail(
messages: Message[],
tailBudget: number
): { head: Message[]; tail: Message[] } {
const tail: Message[] = []
let accumulated = 0
for (let i = messages.length - 1; i >= 0; i--) {
const msg = messages[i]
const tokens = estimateTokens(msg) // rough estimate per message
tail.unshift(msg)
accumulated += tokens
if (accumulated >= tailBudget && tail.length >= MIN_TAIL_TURNS) {
// đã đủ tail — phần còn lại là head
return {
head: messages.slice(0, i),
tail,
}
}
}
// Không đủ messages để compact — trả về nguyên vẹn
return { head: [], tail: messages }
}
`}Bước 3 — Gọi LLM compact head
Head được tóm tắt bằng LLM call riêng với T12 structured template. Output là một string markdown với 5 sections cố định.
session/compaction.ts — LLM summary call
{`
async function compactHead(
head: Message[],
session: Session
): Promise<string> {
const template = session.plugins.compactionTemplate?.()
?? DEFAULT_COMPACTION_TEMPLATE // T12
const response = await session.llm.complete({
messages: [
...head,
{ role: 'user', content: template },
],
// Dùng model nhẹ hơn nếu available (tiết kiệm cost)
model: session.config.compactionModel ?? session.config.model,
})
return response.content
}
`}Bước 4 — Ghép summary + tail
Summary được đặt làm system message đặc biệt với prefix
<prior-conversation-summary>, theo sau là các tail messages.
session/compaction.ts — reassemble
{`
function reassemble(summary: string, tail: Message[]): Message[] {
return [
{
role: 'system',
content: \`<prior-conversation-summary>\n\${summary}\n</prior-conversation-summary>\`,
},
...tail,
]
}
`}Tương tác với các kỹ thuật khác Interaction
T8 đóng vai trò orchestrator của toàn bộ compaction pipeline: nó gọi T9 (tool pruning) trước, dùng T12 (structured template) trong LLM call, và sau đó T11 (continuation) inject message để T1 loop không bị stall.
Failure modes Risk
FM-1: Compact toàn bộ messages
Không giữ tail — summary thay thế toàn bộ conversation history:
Anti-pattern — no tail preservation
{`
// ❌ Compact all — mất short-term memory
async function compactAll(messages, session) {
const summary = await llm.complete({
messages: [...messages, { role: 'user', content: template }]
})
// Agent không còn biết mình vừa gọi tool nào,
// kết quả ra sao → dễ lặp lại hành động đã làm
return [{ role: 'system', content: summary }]
}
`}FM-2: Tail quá nhỏ
Nếu tail chỉ giữ 1 turn, model có thể không thấy lệnh gần nhất của user. Ví dụ: user vừa nói "hãy check file X" → compact → tail chỉ giữ tool result → model không biết tại sao lại có tool result đó.
FM-3: Không compact kịp thời
Nếu T7 threshold quá cao (compact quá muộn), API call lần đó thành công
nhưng lần tiếp theo fail ngay. Giữa hai call này không có cơ hội compact
→ crash. opencode tránh điều này bằng >= (không phải >)
trong T7.
Ưu điểm
- Giữ short-term memory (tail) — agent biết mình đang làm gì
- LLM summary cho head — không mất thông tin quan trọng
- MIN_TAIL_TURNS = 2 — đảm bảo đủ ngữ cảnh tối thiểu
- Plugin override template — linh hoạt per use-case
Nhược điểm
- Thêm 1 LLM call để compact → tăng latency và cost
- Tail budget cứng — không adaptive theo loại conversation
estimateTokenschỉ là rough estimate, không chính xác 100%
So sánh với các harness khác Compare
| Harness | Chiến lược compaction | Tail preservation? | LLM summary? |
|---|---|---|---|
| opencode | Tail 25% + LLM summary cho head | Có (25%, clamp 2k–8k) | Có (T12 template) |
| Claude Code | Tail-preserving (tương tự) | Có | Có |
| LangChain | ConversationSummaryBufferMemory — giữ N token gần nhất | Có (token-based, không turn-based) | Có |
| Aider | Không có auto compaction — user phải /clear thủ công | Không | Không |
Implementation recipe Recipe
compaction.ts — tail-preserving compaction recipe
{`
// compaction.ts — tail-turn preserving compaction
const MIN_TAIL_TURNS = 2
const TAIL_RATIO = 0.25
const MIN_TAIL_TOKENS = 2_000
const MAX_TAIL_TOKENS = 8_000
export async function compact(
messages: Message[],
modelLimit: number,
llm: LLMClient,
template: string,
reserved = 20_000
): Promise<Message[]> {
const usable = Math.max(0, modelLimit - reserved)
const tailBudget = Math.min(
MAX_TAIL_TOKENS,
Math.max(MIN_TAIL_TOKENS, Math.floor(usable * TAIL_RATIO))
)
// 1. Backward walk để tìm tail
const tail: Message[] = []
let accumulated = 0
let splitIdx = 0
for (let i = messages.length - 1; i >= 0; i--) {
tail.unshift(messages[i])
accumulated += estimateTokens(messages[i])
if (accumulated >= tailBudget && tail.length >= MIN_TAIL_TURNS) {
splitIdx = i
break
}
}
const head = messages.slice(0, splitIdx)
if (head.length === 0) return messages // nothing to compact
// 2. LLM compact head
const summaryResp = await llm.complete({
messages: [...head, { role: 'user', content: template }],
})
const summary = summaryResp.content
// 3. Reassemble
return [
{
role: 'system',
content: \`<prior-conversation-summary>\n\${summary}\n</prior-conversation-summary>\`,
},
...tail,
]
}
`}