anomalyco / anomalyco/opencode
[Bug]: Compaction double-fires (same parent/input), summarize rounds leave "terminated" stubs, question parts stuck running, tail-fallback budget under-compresses (1.16.53)
@kitlangton is already working on this.
Since Sep 9, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
Description
Description
Environment: opencode 1.16.53, Windows, one server instance with 5+ concurrently active sessions, 200k-context model via a custom OpenAI-compatible provider. Evidence below is from the local storage db (message/part rows) and server logs, all from 2026-09-08. I verified mechanism candidates against the dev-branch source (session/compaction.ts select(), session/prompt.ts runLoop, server/.../handlers/session.ts summarize).
Four related symptoms, one night of data:
1. Double compaction: two compaction messages with the SAME parentID and the SAME input token count.
One session shows two compaction user messages 8s apart, both with parentID = <same assistant msg>, both built from the same input snapshot (input tokens 58157/58157; tokens.total 61868 vs 63166 — outputs differ by ~1.3k). Another session shows the same pair pattern (input 68288/68288, 1s apart). A single summarize trigger produced two compaction LLM calls for one session. Suspected mechanism: two concurrent prompt loops both tasks.pop() the same compaction task and both run compaction.process (there is also a client-side amplifier: a plugin loaded once per client connection fired summarize twice 2s apart — 21:24:56/21:24:58 — but the same-parent/same-input pair occurs even with a single trigger).
2. Interrupted summarize rounds leave stub assistant messages (terminated) and split thinking.
DB rows: an assistant message with tokens.input = 0 and error = {name: "UnknownError", data: {message: "terminated"}} (9 min after a double-fire), another with MessageAbortedError, and zero-token stubs whose parts array already contains [step-start, reasoning, text, tool]. The interrupted round leaves a partial message; the re-run continuation emits a second one — the user sees two thinking blocks for one turn (split thinking).
3. ask() question parts stuck running forever.
Two separate part rows (2026-09-03 and 2026-09-08), both status = running, output null, observed 15+ min; the pending question wedged a step loop (20 consecutive "reply unknown" in the log). Related 2.0-era reports: #36585, #44471 (also #36604, #44747). The 1.16.x mechanism I traced: the question registry is a per-directory in-memory Map, and ask uses Effect.ensuring(Deferred.await, () => pending.delete(id)) — on fiber interruption the entry is deleted WITHOUT settling the deferred, so the awaiting part never resolves and stays running forever; there is no TTL and no reconciliation sweep.
4. Tail-fallback budget = 8000 structurally under-compresses large-context sessions (related #37871).
compaction.ts select(): MAX_PRESERVE_RECENT_TOKENS = 8000. With a 200k-context model, a last turn of ~32k output tokens (>8000) makes splitTurn fail → total = 0 → keep = undefined → head = ALL messages, no tail preserved — the compaction LLM gets the full context and the summary replaces everything. Measured on the 2026-09-08 data:
- Compression ratio only 56–60% (152502 → 85442; 153861 → 91745)
- Context regrows ~4k tokens / 90s (tool-output accounting), re-crossing the 75% threshold in ~10 min
- Overnight boundary counts per session: 56 / 23 / 21 / 12 compaction boundaries → order of 100+ extra 60k-input LLM calls in one night, one session compacted 56 times
#37871 reports the same constant causing tight total=0 loops on small-context models; on large-context models it is not a tight loop but a slow self-exciting cycle (compaction → 56% ratio → 10-min refill → compaction) with real token cost.
Fix directions
- Single-flight compaction per session: an in-flight guard around compaction task processing /
summarize, so concurrent loops cannot double-process one compaction task. - Serialize or otherwise protect in-flight summarize rounds instead of letting a new round terminate the previous one (source of the
terminatedstubs and split thinking). - Question parts: settle (or TTL) the deferred on fiber interruption, plus a reconciliation sweep for parts stuck
running. - Revisit the tail budget: scale
MAX_PRESERVE_RECENT_TOKENSwith the model context window / last-turn size, or degrade to a turns-based tail when the token budget cannot split the last turn.
Plugins
A custom auto-compact plugin (loaded once per client connection — contributes the 2s-apart double summarize in symptom 1; already fixed locally with a cross-instance lock). The same-parent/same-input double compaction and all other symptoms are server-side.
OpenCode version
1.16.53
Operating System
Windows
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.