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)

Open
#48,065 0 comments 0 reactions 1 assignee View on GitHub

@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 = 0keep = undefinedhead = 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
  1. Single-flight compaction per session: an in-flight guard around compaction task processing / summarize, so concurrent loops cannot double-process one compaction task.
  2. Serialize or otherwise protect in-flight summarize rounds instead of letting a new round terminate the previous one (source of the terminated stubs and split thinking).
  3. Question parts: settle (or TTL) the deferred on fiber interruption, plus a reconciliation sweep for parts stuck running.
  4. Revisit the tail budget: scale MAX_PRESERVE_RECENT_TOKENS with 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.