Skip to content

Bug: opencode run exits after compaction when compaction model's token usage exceeds overflow threshold #13946

@andrea-tomassi

Description

@andrea-tomassi

Description

When using opencode run (non-interactive/headless mode), the process exits cleanly (exit code 0) after auto-compaction if the compaction model's own response has total token usage exceeding the model's overflow threshold.

This happens because the compaction processor's finish-step handler runs the same isOverflow() check on the compaction model's token usage. When the compaction input is large (>~94k tokens for a model with 128k context/32k output), isOverflow() returns true, causing processor.process() to return "compact" instead of "continue". This prevents the synthetic "Continue..." user message from being created, which in turn causes the prompt loop's exit condition to fire.

Steps to Reproduce

  1. Start a session with opencode run
  2. Accumulate context through tool calls (file reads, etc.) until tokens reach ~80-95k
  3. Read several large files in rapid succession, pushing context to ~100k+ tokens in a single turn
  4. Auto-compaction triggers
  5. The compaction model summarizes successfully (finish: "stop", no error)
  6. Process exits with code 0 instead of continuing

Root Cause

In packages/opencode/src/session/compaction.ts, the process() function:

const result = await processor.process({ ... })

if (result === "continue" && input.auto) {
  // Create synthetic "Continue..." user message  ← THIS IS SKIPPED
}
if (processor.message.error) return "stop"
return "continue"

When the compaction model's token usage exceeds the overflow threshold, processor.process() returns "compact" (not "continue"). The condition result === "continue" && input.auto is false, so the synthetic "Continue if you have next steps..." user message is never created.

Back in prompt.ts, the message history ends with:

[user: compaction trigger] → [assistant: compaction summary, finish="stop"]

The exit check evaluates:

if (
  lastAssistant?.finish &&                              // "stop" → truthy ✓
  !["tool-calls", "unknown"].includes(lastAssistant.finish) && // true ✓
  lastUser.id < lastAssistant.id                        // compaction trigger < summary ✓
) {
  break  // ← Process exits here
}

Evidence

Analyzed 82 compaction events across 20+ sessions:

Compaction Total Tokens Outcome Count
< 96k ✅ SURVIVED 71
≥ 96k ❌ DIED 11

100% correlation. The threshold matches the isOverflow() calculation: 128k context - 32k maxOutput ≈ 96k usable.

Edge cases:

  • 94,361 input + 655 output = ~95k < 96kSURVIVED
  • 93,987 input + 2,983 output = ~97k > 96kDIED

All SURVIVED compactions had the synthetic "Continue..." user message. All DIED compactions did not.

Suggested Fix

Option A: Handle "compact" result in compaction process (minimal)

- if (result === "continue" && input.auto) {
+ if ((result === "continue" || result === "compact") && input.auto) {

Option B: Skip isOverflow check during compaction (recommended)

In processor.ts finish-step handler:

  if (
+   !input.assistantMessage.summary &&
    await SessionCompaction.isOverflow({ tokens: usage.tokens, model: input.model })
  ) {
    needsCompaction = true
  }

This prevents the compaction model's own response from triggering another compaction cycle.

Environment

  • OpenCode version: latest (as of Feb 2026)
  • Mode: opencode run (headless/non-interactive)
  • Models affected: claude-opus-4.6, gemini-3-pro-preview via github-copilot provider
  • OS: Ubuntu Linux 24.04

Related Code

  • packages/opencode/src/session/compaction.tsprocess(), isOverflow()
  • packages/opencode/src/session/processor.tsfinish-step handler
  • packages/opencode/src/session/prompt.ts — main loop exit conditions

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions