← opencode report

T23 — Overflow pattern detection & retry với server headers

25+ regex pattern detect ContextOverflowError qua mọi provider. Retry: tôn trọng retry-after-ms header. Skip retry khi overflow — trigger compaction thay vì waste calls.
Nhóm: D — Provider AbstractionFiles: provider/error.ts:8–193 + session/retry.ts:17–52ID: D.3 / T23Status: Stable

Tổng quan Provider

Tại sao quan trọng. Không có HTTP status code chuẩn cho "context too long" — mỗi provider trả khác nhau: Anthropic 400 "prompt is too long", OpenAI 400 "max_tokens_exceeded", Bedrock 413 no body, Gemini 400 "context_length_exceeded". Nếu không phân biệt overflow vs transient error → retry overflow = waste token + tiền + thời gian. opencode dùng 25+ regex để classify lỗi, sau đó: overflow → skip retry, trigger compaction; transient → retry với exponential backoff; rate limit → respect server retry-after.
Server-directed retry: Khi provider trả retry-after-ms hoặc retry-after header, opencode tôn trọng giá trị đó thay vì dùng backoff tự tính. Điều này tránh thunder-herd và giảm nguy cơ bị ban IP.

Phân tích code chi tiết Anatomy

Overflow pattern list — kinh nghiệm production

provider/error.ts — OVERFLOW_PATTERNS

TS
{`
// 25+ pattern từ thực tế production với nhiều provider
const OVERFLOW_PATTERNS: RegExp[] = [
  /prompt is too long/i,
  /exceeds the context window/i,
  /max.*context.*length/i,
  /context_length_exceeded/i,
  /token.*limit.*exceeded/i,
  /maximum context length/i,
  /input.*too long/i,
  /reduce your prompt/i,
  /no body.*(400|413)/i,        // Bedrock silent truncation → no body
  /request entity too large/i,  // HTTP 413
  // ... 15+ more từ Groq, Together, Ollama, Mistral...
]

export function isContextOverflow(err: unknown): boolean {
  const msg = extractErrorMessage(err)
  return OVERFLOW_PATTERNS.some((r) => r.test(msg))
}

function extractErrorMessage(err: unknown): string {
  if (err instanceof Error) return err.message
  if (typeof err === "string") return err
  // Extract từ response body nếu là HTTP error
  if ((err as any)?.body) return JSON.stringify((err as any).body)
  return String(err)
}
`}

Retry logic — server-directed + exponential backoff

session/retry.ts — withRetry

TS
{`
export async function withRetry<T>(
  fn:          () => Promise<T>,
  maxAttempts  = 5,
): Promise<T> {
  let attempt = 0

  while (true) {
    try {
      return await fn()
    } catch (err) {
      attempt++

      // 1. Context overflow: KHÔNG retry — trigger compaction thay vì waste calls
      if (isContextOverflow(err)) throw new ContextOverflowError(err)

      // 2. Max attempts reached
      if (attempt >= maxAttempts) throw err

      // 3. Server-directed delay: tôn trọng retry-after header
      const serverMs = extractRetryAfter(err)

      // 4. Exponential backoff fallback: 2^(attempt-1) * 2000ms, cap 30s
      const delayMs = serverMs ?? Math.min(2 ** (attempt - 1) * 2000, 30_000)

      await sleep(delayMs)
    }
  }
}

function extractRetryAfter(err: unknown): number | null {
  const headers = (err as any)?.response?.headers
  if (!headers) return null

  const ms  = headers.get("retry-after-ms")
  if (ms) return Number(ms)

  const sec = headers.get("retry-after")
  if (sec) return Number(sec) * 1000

  return null
}
`}
API call throws error │ ▼ isContextOverflow(err)? │ ┌────┴────┐ YES NO │ │ ▼ ▼ throw attempt < maxAttempts? ContextOverflowError │ │ ┌────────┴────────┐ ▼ YES NO trigger │ │ compaction ▼ ▼ (T8) extractRetryAfter(err) throw err │ ┌────┴─────┐ server fallback header exponential backoff │ 2^(n-1) * 2000ms │ cap: 30s └────┬───┘ ▼ sleep → retry

Tương tác với kỹ thuật khác Interaction

  • T7 (Overflow detection proactive): T7 detect overflow trước khi gọi API (đếm token). T23 xử lý overflow sau khi API đã reject. Hai lớp bổ sung nhau — T7 phòng ngừa, T23 xử lý fallback.
  • T8 (Tail-turn compaction): Khi T23 throw ContextOverflowError, agent loop bắt lỗi này và trigger compaction (T8) thay vì fail toàn bộ session.
  • T4 (Interruption-safe cleanup): Retry loop bên trong Effect.retry(...) — Effect giữ semantics đúng khi interrupt xảy ra giữa retry cycle (không để request treo).

Failure modes Failure

1. Regex miss khi provider đổi message

Provider update error message → regex không match → overflow được xử lý như transient error → retry → waste 5 attempts trước khi fail. Cần monitor miss rate và update patterns.

2. Exponential backoff không có jitter

Multiple client cùng nhận rate limit error → cùng sleep theo formula → cùng retry tại cùng thời điểm → thunder-herd nhẹ. Thêm random jitter (±20%) để stagger.

Best practice: delay = base * (1 + Math.random() * 0.2) — jitter 20% enough to stagger clients hiệu quả.

3. Không phân biệt 4xx vs 5xx

4xx (bad request) thường không nên retry; 5xx (server error) nên retry. Hiện tại retry logic không phân biệt rõ — chỉ dừng ở overflow. Cần thêm isRetryable(err) check.

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

HarnessOverflow detectionRetry policyServer-directed
opencode25+ regex, unified across providersExponential, skip on overflow✅ retry-after-ms
litellm (Aider)Built-in per-providerExponential + jitter
Claude CodeSDK-level detectionRetry with backoff
ClineBasic error checkSimple retry
OpenHarnessSDK-level (Anthropic)Basic retry⚠️

Implementation recipe Recipe

TS
{`
const OVERFLOW = [
  /prompt is too long/i,
  /context.?window/i,
  /token.?limit/i,
  /context_length_exceeded/i,
  /request entity too large/i,
]

class ContextOverflowError extends Error {
  constructor(public readonly cause: unknown) {
    super("Context window exceeded")
  }
}

async function retryable<T>(fn: () => Promise<T>, max = 5): Promise<T> {
  for (let i = 0; i < max; i++) {
    try {
      return await fn()
    } catch (e) {
      const msg = String((e as Error).message ?? e)

      // Overflow: không retry
      if (OVERFLOW.some(r => r.test(msg))) throw new ContextOverflowError(e)

      if (i === max - 1) throw e

      // Server-directed delay
      const serverMs = (e as any).headers?.["retry-after-ms"]
      const base     = serverMs ?? Math.min(2000 * 2 ** i, 30_000)
      // Add jitter ±20%
      const delay    = base * (0.8 + Math.random() * 0.4)
      await new Promise(r => setTimeout(r, delay))
    }
  }
  throw new Error("unreachable")
}
`}

Tham khảo Refs