← opencode report

T11 — Auto compaction continuation

Sau T8 compact xong, agent cần message mới để tiếp tục. T11 quyết định inject gì: replay user message, mô tả media bằng text, hay gửi synthetic continue khi đang mid-task.
Nhóm: B — Context ManagementFile: session/compaction.tsLines: 372–451Status: Stable

Tổng quan Context

Tại sao quan trọng. Sau khi T8 compact xong, messages array đã được thay thế bằng [summary_system, ...tail]. Nhưng T1 agent loop cần một "new user message" để tiếp tục — nếu không inject gì, agent dừng lại chờ input người dùng, dù giữa chừng đang thực hiện task quan trọng.

T11 giải quyết bằng logic 3-branch dựa trên trạng thái trước compaction:

  • Branch 1 — Media message: user turn cuối chứa image/attachment → inject text description thay thế (không replay media thật — có thể đã expire hoặc quá nặng).
  • Branch 2 — Unanswered user message: user vừa gửi lệnh nhưng agent chưa reply (đang pre-processing) → replay nguyên văn lệnh đó.
  • Branch 3 — Mid-task (default): agent đang giữa chuỗi tool calls → inject synthetic "continue" với metadata compaction_continue: true.
Synthetic message + metadata. Message synthetic có thể được filter/hide ở UI nhờ metadata.compaction_continue. User không thấy "continue" lạ xuất hiện trong chat — chỉ thấy agent tiếp tục làm việc.

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

Branch detection — xác định trạng thái trước compact

session/compaction.ts — branch detection

TS
{`
function detectPostCompactionState(
  messages: Message[]   // messages TRƯỚC khi compact
): 'media' | 'unanswered' | 'mid-task' {
  // Tìm last user message
  const lastUser = [...messages]
    .reverse()
    .find(m => m.role === 'user')

  if (!lastUser) return 'mid-task'

  // Branch 1: user message có media
  if (hasMedia(lastUser)) return 'media'

  // Branch 2: user message chưa được reply
  // (không có assistant message sau nó)
  const lastUserIdx = messages.lastIndexOf(lastUser)
  const hasReply    = messages
    .slice(lastUserIdx + 1)
    .some(m => m.role === 'assistant')

  if (!hasReply) return 'unanswered'

  // Default: agent đang mid-task
  return 'mid-task'
}
`}

Branch 1 — Media message: inject text description

session/compaction.ts — media branch

TS
{`
function buildMediaContinuation(lastUserMsg: Message): Message {
  // Không replay media thật — mô tả bằng text
  const description = lastUserMsg.content
    .filter(block => block.type !== 'image')
    .map(block => block.text ?? '')
    .join(' ')
    .trim()

  return {
    role:    'user',
    content: description
      ? \`[Continuing from compaction] \${description}\`
      : '[Continuing task — previous message contained media attachments]',
    metadata: { compaction_continue: true, had_media: true },
  }
}
`}

Branch 2 — Unanswered: replay user message

session/compaction.ts — unanswered branch

TS
{`
function buildUnansweredContinuation(lastUserMsg: Message): Message {
  // Replay nguyên văn — không cần synthetic message
  // metadata vẫn đánh dấu là continuation để UI biết
  return {
    ...lastUserMsg,
    metadata: {
      ...lastUserMsg.metadata,
      compaction_continue: true,
    },
  }
}
`}

Branch 3 — Mid-task: synthetic continue

session/compaction.ts — mid-task branch

TS
{`
function buildMidTaskContinuation(): Message {
  return {
    role:    'user',
    content: 'continue',
    metadata: {
      compaction_continue: true,
      // UI filter: ẩn message này khỏi chat display
      // Agent vẫn nhận và xử lý bình thường
    },
  }
}
`}

Main entry point — kết hợp 3 branches

session/compaction.ts — buildContinuation

TS
{`
export function buildContinuation(
  messagesBeforeCompact: Message[]
): Message {
  const state = detectPostCompactionState(messagesBeforeCompact)

  switch (state) {
    case 'media':
      return buildMediaContinuation(
        messagesBeforeCompact
          .filter(m => m.role === 'user')
          .at(-1)!
      )
    case 'unanswered':
      return buildUnansweredContinuation(
        messagesBeforeCompact
          .filter(m => m.role === 'user')
          .at(-1)!
      )
    case 'mid-task':
    default:
      return buildMidTaskContinuation()
  }
}
`}

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

T8 compact() hoàn tất │ ├── returns: [summary_sys, ...tail] (messages mới) │ ▼ T11 buildContinuation(messagesBeforeCompact) │ ├── detectPostCompactionState() │ │ │ ┌─────┼──────────────────────┐ │ │ │ │ │ media unanswered mid-task │ │ │ │ │ ▼ ▼ ▼ │ text replay "continue" │ desc user msg + metadata │ │ │ │ └───┴─────┴──────────────────────┘ │ ▼ T1 Agent loop nhận message mới → tiếp tục xử lý bình thường UI layer đọc metadata.compaction_continue → ẩn synthetic messages trong chat display

T11 là bước cuối cùng của compaction pipeline. Nó nhận messages trước compact (để detect state), nhưng inject message vào session sau compact (messages array đã được T8 thay thế).

Quan trọng: snapshot trước compact. T11 cần messagesBeforeCompact — không phải messages sau compact. T8 phải truyền snapshot này sang T11 trước khi overwrite messages array.

Failure modes Risk

FM-1: Không inject continuation

Anti-pattern — no continuation after compact

TS
{`
// ❌ Compact xong nhưng không inject gì
async function compactAndContinue(session) {
  const newMessages = await compact(session.messages, ...)
  session.messages  = newMessages
  // Không inject continuation → T1 loop không có new user message
  // → agent stop, chờ user input dù đang mid-task
}
`}

FM-2: Replay media attachment

Replay media thật thay vì text description:

Anti-pattern — replay media

TS
{`
// ❌ Replay nguyên văn kể cả media
function buildContinuation(lastUserMsg) {
  return lastUserMsg  // chứa image blocks có thể đã expire
  // Anthropic media blocks có URL expiry 1 giờ
  // → 400 error "image URL expired" nếu compact xảy ra sau >1h
}
`}

FM-3: Hiển thị synthetic message cho user

Nếu UI không filter metadata.compaction_continue, user thấy "continue" xuất hiện trong chat như thể họ đã gõ — confusing UX.

Ưu điểm
  • 3-branch — xử lý đúng 3 trạng thái khác nhau sau compact
  • Media safety — không replay expired media
  • Metadata filter — UI có thể ẩn synthetic messages
  • Unanswered replay — user không cần gõ lại lệnh
Nhược điểm
  • Cần snapshot messages trước compact — thêm complexity
  • Synthetic "continue" có thể confuse model trong edge cases
  • Branch logic dựa trên heuristic — không 100% accurate

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

HarnessContinuation sau compactMedia handling?Mid-task aware?
opencode3-branch: media/unanswered/mid-taskCó — text description thay thếCó — synthetic "continue"
Claude CodeTương tự (known from Anthropic blog)
LangChainReplay last message đơn giản, không phân biệt stateKhông có special handlingKhông
ForgeCodeDocumented continuation pattern (single branch)Không rõPartial

Implementation recipe Recipe

compaction.ts — 3-branch continuation recipe

TS
{`
// continuation.ts — post-compaction continuation logic

export function buildContinuation(
  messagesBeforeCompact: Message[]
): Message {
  const userMessages = messagesBeforeCompact.filter(m => m.role === 'user')
  const lastUser     = userMessages.at(-1)

  if (!lastUser) return syntheticContinue()

  // Branch 1: media
  if (contentHasMedia(lastUser.content)) {
    const textOnly = extractTextContent(lastUser.content)
    return {
      role:    'user',
      content: textOnly
        ? \`[Resuming] \${textOnly}\`
        : '[Resuming — previous message had media attachments]',
      metadata: { compaction_continue: true, had_media: true },
    }
  }

  // Branch 2: unanswered user message
  const lastUserIdx = messagesBeforeCompact.lastIndexOf(lastUser)
  const answered    = messagesBeforeCompact
    .slice(lastUserIdx + 1)
    .some(m => m.role === 'assistant')

  if (!answered) {
    return { ...lastUser, metadata: { ...lastUser.metadata, compaction_continue: true } }
  }

  // Branch 3: mid-task
  return syntheticContinue()
}

function syntheticContinue(): Message {
  return {
    role:     'user',
    content:  'continue',
    metadata: { compaction_continue: true },
  }
}
`}

Tham khảo Refs