← opencode report

T8 — Tail-turn preserving compaction

Giữ 25% turn cuối cùng nguyên vẹn, tóm tắt phần còn lại bằng LLM với template 5-section — để agent không mất ngữ cảnh tức thời sau khi compact.
Nhóm: B — Context ManagementFile: session/compaction.tsLines: 33–170Status: Stable

Tổng quan Context

Tại sao quan trọng. Khi context overflow, agent có 2 lựa chọn tệ: (1) compact toàn bộ messages → mất "short-term memory", không biết mình vừa làm gì; (2) không compact → API crash với 400. Tail-preserving compaction là con đường thứ 3: giữ nguyên N turn gần nhất (nơi chứa lệnh vừa nhận, kết quả tool vừa xong) và chỉ tóm tắt phần lịch sử xa hơn.

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.

25% tail budget. Con số 25% không phải ngẫu nhiên: đủ để giữ lại 1-2 round-trip tool call + response (thường 1 000–4 000 token), nhưng không quá lớn để ảnh hưởng không gian cho LLM response mới.
MIN_TAIL = 2 turns. Compaction không xảy ra nếu tail chưa tích lũy đủ 2 turns. Điều này tránh compact quá sớm khi conversation mới bắt đầu và chỉ có 1-2 message.

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

TS
{`
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 đã 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

TS
{`
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

TS
{`
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

TS
{`
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

T7 shouldCompact = true │ ▼ ┌─────────────────────────────────────────────┐ │ T8 — Tail-turn Compaction │ │ │ │ 1. T9: prune tool outputs trong head │ │ (trước khi gửi head cho LLM) │ │ │ │ │ ▼ │ │ 2. tính tail budget (25% usable) │ │ │ │ │ ▼ │ │ 3. backward walk → split head/tail │ │ │ │ │ ▼ │ │ 4. T12 template → LLM compact(head) │ │ │ │ │ ▼ │ │ 5. reassemble: [summary_sys, ...tail] │ │ │ │ │ ▼ │ │ 6. T11: inject continuation message │ └─────────────────────────────────────────────┘ │ ▼ T1 Agent loop tiếp tục với messages mới

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.

Order matters. T9 (tool pruning) phải chạy trước khi split head/tail, không phải sau. Nếu prune sau khi split, có thể vô tình prune tool output trong tail — mất ngữ cảnh tức thời.

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

TS
{`
// ❌ 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
  • estimateTokens chỉ là rough estimate, không chính xác 100%

So sánh với các harness khác Compare

HarnessChiến lược compactionTail preservation?LLM summary?
opencodeTail 25% + LLM summary cho headCó (25%, clamp 2k–8k)Có (T12 template)
Claude CodeTail-preserving (tương tự)
LangChainConversationSummaryBufferMemory — giữ N token gần nhấtCó (token-based, không turn-based)
AiderKhông có auto compaction — user phải /clear thủ côngKhôngKhông
LangChain khác biệt. LangChain giữ token budget cuối (không phải turns). Điều này có thể cắt đứt giữa một turn nếu turn đó quá dài, gây ra partial context — thường tệ hơn không có gì.

Implementation recipe Recipe

compaction.ts — tail-preserving compaction recipe

TS
{`
// 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,
  ]
}
`}

Tham khảo Refs