Repository navigation
Description
Summary
Auto-compaction + the synthetic post-compaction Continue… turn can produce an eternal loop on tool-heavy sessions: each "compaction" leaves net context ≥ 0, so isOverflow() stays true and compaction re-fires every single step, while each continue-turn re-primes the model to re-emit its most recent completed tool call. opencode's DOOM_LOOP_THRESHOLD guard only counts repeated tool parts within the current assistant message (processor.ts, parts.slice(-3)), so a cross-message loop is invisible to it.
Reproduction (observed live, not synthetic)
opencode 1.18.34, model local/qwen3.8-27b-15g (declared limit: {context: 65536, output: 16384}). PR-review task with several ~3.5K-char GitHub tool outputs. From the event store (27 summary messages, 26 compaction parts):
- Turn inputs: 49,417 → 50,835 tokens, monotonically increasing across 26 compactions — compaction never shrank anything.
- Per cycle: tool call →
step-finish→ compaction part (auto: true, overflow: false) → summary assistant message (~2K input/~200 output via a cloud compaction agent) → synthetic user part ("Continue if you have next steps…") → the same tool call again. - 13+ identical
worktree-verifycompletions before manual abort; ~80 s and ~50K tokens burned per cycle;cached_tokens: 0every turn.
Root cause (source read, this version)
usable()= context −maxOutputTokens= 65536 − 16384 = 49,152. A ~50K-token session is genuinely over it — the loop starts once history crosses this line and never leaves it.- Compaction cannot reduce context here:
preserveRecentBudget()=min(15000, max(2000, usable × 0.25))= 12,288 tokens of recent turns always kept in the tail;- tool outputs within
PRUNE_PROTECT(40K) of the end are exempt from pruning; - in the observed session
select()returnedtail_start_id: undefinedfor all 26 compactions (head/tail overlap → everything summarized is also kept); - each cycle additionally adds the summary assistant message (~200 output tokens) + synthetic continue part.
Net effect:context_after ≥ context_before— the overflow state is a fixed point. Compaction fires forever.
- The synthetic
Continue…turn lands right after a summary whose tail contains the just-completed tool call(s); small local models pattern-match the last-seen safe action instead of executing their stated next step. Since repeat detection is per-message only, nothing trips.
Expected
- Compaction should be net-negative on context for the session that triggered it, or auto-compaction should refuse to re-arm while the previous compaction failed to reduce
isOverflow()(the second compaction is a no-op → surface an explicit error/idle instead of looping). - The doom-loop detector should consider the last N assistant messages, not just parts of the current one — a compaction boundary is exactly where the naive in-message window fails.
- The synthetic continue turn should not be appended when the immediately preceding stretch shows the same completed tool call ≥ threshold times across messages.
Observed impact
Eternal ~80 s/turn loop at ~50K tokens/turn with no prefix cache; manual abort was the only exit; no user-visible progress.
Local mitigation (documented, not a fix)
A plugin hooking experimental.compaction.autocontinue and denying the synthetic turn when the recent history shows threshold identical completed tool calls across messages breaks the loop mechanically; ships in our config repo. But the upstream fixed point (compaction that cannot shrink below usable() still re-firing forever) is the real bug.
Happy to provide the full event-store dump (session ses_f06ae22e5ffeFv7pyZePkenY5i, seq 190–676) if useful.
Activity
github-actions commentedon Oct 2, 2026
This issue doesn't fully meet our contributing guidelines.
What needs to be fixed:
Please edit this issue to address the above within 72 hours, or it will be automatically closed.
This issue might be a duplicate of existing issues. Please check:
If you believe this was flagged incorrectly, please let a maintainer know.
github-actions commentedon Oct 5, 2026
This issue has been automatically closed because it was not updated to meet our contributing guidelines within the 72-hour window.
Feel free to open a new issue that follows our issue templates.