T7 — Overflow detection (cache-aware)
cache.read và cache.write — để phát hiện nguy cơ context overflow trước khi API call thất bại.Tổng quan Context
cache_read_input_tokens và cache_creation_input_tokens)
vẫn được tính vào context window. Nếu chỉ đếm input_tokens thuần,
agent sẽ bị overflow đột ngột khi cache tokens tích lũy lớn — gây lỗi
400 context_length_exceeded giữa chừng.
opencode giải quyết bằng 3 hàm nhỏ gọn trong session/overflow.ts:
computeUsable tính số token có thể dùng sau khi trừ buffer output,
totalContextTokens cộng tất cả loại token (input + cache.read + cache.write),
và shouldCompact quyết định có cần compaction chưa.
Buffer output mặc định là 20 000 token
(DEFAULT_RESERVED_OUTPUT_TOKENS). Con số này đảm bảo model luôn có
đủ không gian để sinh response, không bị cắt giữa câu. Khi
totalContextTokens >= usable, T8 (compaction) được kích hoạt.
input_tokens. opencode là một trong số ít harness tính đầy đủ
cả 3 loại token — khớp với cách Anthropic tính giới hạn context window.
Phân tích code chi tiết Anatomy
Bước 1 — Tính usable tokens
computeUsable trừ đi reserved buffer khỏi model limit.
Math.max(0, ...) xử lý edge case khi model limit nhỏ hơn reserve
(ví dụ: model thử nghiệm với context 8k nhưng reserve 20k).
session/overflow.ts — computeUsable
{`
export const DEFAULT_RESERVED_OUTPUT_TOKENS = 20_000
export function computeUsable(
modelLimit: number,
reserved = DEFAULT_RESERVED_OUTPUT_TOKENS
): number {
return Math.max(0, modelLimit - reserved)
// Math.max(0, ...) đảm bảo không trả về số âm
// khi reserved > modelLimit (edge case với model nhỏ < 20k)
}
`}Bước 2 — Đếm cache-aware tokens
Điểm khác biệt quan trọng nhất: cộng cả cache?.read và
cache?.write vào tổng. Anthropic tính cả hai vào context window.
Optional chaining (?.) đảm bảo tương thích với provider không có cache
(OpenAI, Groq) — khi đó cache là undefined, fallback về 0.
session/overflow.ts — totalContextTokens
{`
export type Usage = {
input?: number
cache?: { read?: number; write?: number }
output?: number
}
export function totalContextTokens(usage: Usage): number {
return (
(usage.input ?? 0) +
(usage.cache?.read ?? 0) +
(usage.cache?.write ?? 0)
// NOTE: output tokens KHÔNG được tính vào đây —
// output không chiếm context window của lần gọi tiếp theo
)
}
`}Bước 3 — Quyết định compact
shouldCompact kết hợp hai hàm trên thành 1 boolean đơn giản.
Hàm này được gọi sau mỗi LLM response trong T1 agent loop.
session/overflow.ts — shouldCompact
{`
export function shouldCompact(
usage: Usage,
modelLimit: number,
reserved?: number // optional, default = 20_000
): boolean {
const usable = computeUsable(modelLimit, reserved)
const used = totalContextTokens(usage)
return used >= usable
// >= (không phải >) để trigger compact sớm hơn 1 token
// tránh off-by-one race condition
}
`}Luồng gọi trong agent loop
Hàm này được gọi ngay sau khi nhận usage từ response streaming,
trước khi xử lý tool calls. Nếu shouldCompact trả về true,
session sẽ ngắt tool execution và đi vào T8.
Tương tác với các kỹ thuật khác Interaction
T7 là sensor — nó không thay đổi gì, chỉ đọc và quyết định. Kết quả của nó kích hoạt hoặc ảnh hưởng đến nhiều kỹ thuật khác trong nhóm B.
Ngoài ra, T7 gián tiếp ảnh hưởng T10 (cache-aware system prompt): khi
cache.write tăng đột biến (system prompt thay đổi → cache miss),
T7 sẽ báo động sớm hơn, nhắc nhở cần tối ưu T10 để giảm cache churn.
Failure modes Risk
FM-1: Không đếm cache tokens
Cách triển khai naïve phổ biến nhất — chỉ dùng input_tokens:
Anti-pattern — thiếu cache tokens
{`
// ❌ Naive: bỏ qua cache tokens
function shouldCompact(usage, modelLimit) {
return usage.input_tokens >= modelLimit - 20_000
// Khi cache.write = 80k và input = 40k:
// naive thấy 40k < 180k → không compact
// thực tế 120k < 180k → cũng ok lần này
// nhưng lần sau khi input tăng → đột ngột 400 error
}
`}Hệ quả: agent hoạt động bình thường cho đến khi input_tokens tăng đủ lớn, khi đó API trả về lỗi 400 mà không có cơ hội compact trước.
FM-2: Reserve buffer quá nhỏ
Nếu reserve là 4 096 (mặc định của một số harness), với model 200k: usable = 195 904. Session sẽ compact muộn hơn. Khi LLM cần sinh response dài (chain-of-thought), 4k output space không đủ → response bị truncate, gây tool call malformed JSON.
Anti-pattern — reserve quá nhỏ
{`
// ❌ Reserve nhỏ — response bị truncate
const RESERVE = 4_096 // quá thấp cho models dùng CoT dài
// opencode chọn 20_000 — đủ cho hầu hết response dài
// bao gồm cả cases model sinh XML tool call + explanation
`}FM-3: Không xử lý provider không có cache
Với OpenAI hoặc Groq, usage.cache là undefined.
Nếu không dùng optional chaining, hàm sẽ throw TypeError: Cannot read
properties of undefined. opencode dùng usage.cache?.read ?? 0
để safe.
Ưu điểm
- Phát hiện overflow sớm — trước khi API lỗi
- Cache-aware — chính xác với Anthropic models
- Optional chaining — tương thích multi-provider
- Reserve 20k — đủ cho output dài (CoT, XML tool calls)
Nhược điểm
- Reserve cứng 20k — không dynamic theo model/task
- Chỉ trigger sau LLM call — không predict trước
- Không tính
output_tokenscủa turn trước vào projection
So sánh với các harness khác Compare
| Harness | Cách đếm token | Cache-aware? | Reserve buffer |
|---|---|---|---|
| opencode | input + cache.read + cache.write | Có | 20 000 token cứng |
| Claude Code | Tương tự (cùng Anthropic) | Có | Không public chi tiết |
| Aider | tiktoken approximate, chỉ input | Không | ~1 000 token (configurable) |
| LangChain | ConversationTokenBufferMemory — chỉ message tokens | Không | max_token_limit tuỳ config |
tiktoken để estimate token
count client-side — nhanh hơn nhưng không chính xác 100% với tokenizer
thực tế của mỗi model. Với Anthropic models, sai lệch thường 5–10%.
Implementation recipe Recipe
Triển khai cache-aware overflow detection với per-provider switch:
overflow.ts — implementation recipe
{`
// overflow.ts — cache-aware token overflow detection
export const DEFAULT_RESERVED_OUTPUT_TOKENS = 20_000
export type Usage = {
input?: number
output?: number
cache?: {
read?: number
write?: number
}
}
export type Provider = 'anthropic' | 'openai' | 'groq' | 'google'
/** Token nào chiếm context window, theo provider */
export function totalContextTokens(
usage: Usage,
provider: Provider = 'anthropic'
): number {
const base = usage.input ?? 0
// Anthropic: cache tokens chiếm context window
if (provider === 'anthropic') {
return base
+ (usage.cache?.read ?? 0)
+ (usage.cache?.write ?? 0)
}
// OpenAI/Groq/Google: không có prompt caching
// (hoặc caching transparent — không ảnh hưởng limit)
return base
}
export function computeUsable(
modelLimit: number,
reserved = DEFAULT_RESERVED_OUTPUT_TOKENS
): number {
return Math.max(0, modelLimit - reserved)
}
export function shouldCompact(
usage: Usage,
modelLimit: number,
opts?: { reserved?: number; provider?: Provider }
): boolean {
const usable = computeUsable(modelLimit, opts?.reserved)
const used = totalContextTokens(usage, opts?.provider)
return used >= usable
}
`}