-
Notifications
You must be signed in to change notification settings - Fork 11.3k
Description
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
- Start a session with
opencode run - Accumulate context through tool calls (file reads, etc.) until tokens reach ~80-95k
- Read several large files in rapid succession, pushing context to ~100k+ tokens in a single turn
- Auto-compaction triggers
- The compaction model summarizes successfully (finish: "stop", no error)
- 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 < 96k→ SURVIVED ✅93,987 input + 2,983 output = ~97k > 96k→ DIED ❌
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-previewviagithub-copilotprovider - OS: Ubuntu Linux 24.04
Related Code
packages/opencode/src/session/compaction.ts—process(),isOverflow()packages/opencode/src/session/processor.ts—finish-stephandlerpackages/opencode/src/session/prompt.ts— main loop exit conditions