Repository navigation
Description
Description
Summary. On v2.2.3, an automatic compaction produced a summary that follows the template but states that no task exists. It replaced a valid summary that described an active task:
## Objective
- (none — no task has been established yet)
...
## Important Context
- No user request, task, or assistant output has occurred to summarize yet.
OpenCode stored it as status: "completed". The next assistant reply greeted the user as if the session had just started. The reply was in Spanish, roughly: "Hi! I'm your OpenCode agent. I see there is no active task yet." The active objective and its constraints were silently lost. The local model server did not truncate anything (see below), so the context was lost on the client side.
Environment
| OpenCode | @opencode/cli 2.2.3, no plugins, default compaction settings |
| Provider/model | Ollama 0.40.13 (native discovery, OpenAI-compatible route), qwen3.8:27b Q4_K_M, fully on GPU (RTX 4090 24 GB) |
| Context | Ollama serves 65,536 (n_ctx_slot = 65536); the model limit is overridden to context: 65536, output: 16384 because discovery reports 262,144 |
| Ollama | flash attention on, KV cache q8_0, 1 parallel slot |
| OS | Windows 11 Pro (build 26300) |
Expected behavior
- A compaction keeps the active objective and its constraints.
- A summary that loses them, or claims there is no task while earlier history clearly has one, does not silently replace a valid context. The previous summary/context is kept and the failure is surfaced.
Actual behavior. These are rows from the session's session_message table in opencode.db, matched to the Ollama server log. tokens.input excludes tokens.cache.read in these rows.
| DB seq | Row | tokens.input | cache.read | output | Notes |
|---|---|---|---|---|---|
| 161 | compaction #1 (auto, completed) | 1,453 | 50,032 | 5,830 | Good summary with full Objective, about 9 KB; no metadata.keepID |
| 164 | assistant (4 tool calls) | 14,615 | 0 | 1,984 | First request after compaction #1 |
| 187 | assistant (4 tool calls) | 38,963 | 16,598 | 3,240 | About 55.5K prompt, past the auto-compaction ceiling |
| 210 | compaction #2 (auto, completed) | 8,397 | 0 | 859 | Task-empty summary shown above; no metadata.keepID |
| 213 | assistant | 11,699 | 0 | 226 | Welcome reply, then idle |
| 224 | user | New user message (about 3.5K chars) | |||
| 414 | compaction #3 (auto, completed) | 12,634 | 0 | 182 | keepID → seq 224; summary again says no task ("user only greeted") |
| 484+ | later compactions | about 707–1,215 | 17K–23K | Normal again: reads the prompt cache, summaries keep the objective |
There were no user messages between compaction #1 and compaction #2, only two assistant steps with tool calls.
Ollama log for the compaction #2 request:
selected slot by LRU, n_tokens = 58800
prompt with length 58800, lcp = 3, f_keep = 0.000, f_sim = 0.000
new prompt, n_ctx_slot = 65536, n_keep = 4, task.n_tokens = 8397
stop processing: n_tokens = 9255, truncated = 0
The slot held the previous 58,800-token conversation. The compaction request shared only 3 prompt tokens with it, was about 7× smaller, and nothing was truncated by the server.
Compaction #3 kept the new user message verbatim through keepID (seq 224), and the agent kept working from it. Its summary was nevertheless again task-empty. This looks like propagation from compaction #2: the history before seq 224 was the empty summary plus the welcome reply. We count it as one failure plus its downstream effect, not as two independent reproductions.
Source analysis (tag v2.2.3, packages/core/src/session/)
Confirmed from the code
compaction.ts~L468-469 checks the summary only structurally. The check isfilled = text.split("\n").some(line => headings.includes(line.trim())): any reply containing one template heading such as## Objectivepasses. When a previous summary exists (the update path), nothing checks that the new summary still contains its objective before it replaces it.recentStart(~L1005-1027): when there is no user message since the previous compaction, and the previous compaction has no storedrecent, the split keeps nothing verbatim (keepIDundefined).SessionRecentText.kept()returns""whenevermetadata.recentis absent (recent-text.tsL12-13). Compactions feat: compact and other improvements #1 and Roadmap & Existing Issues #2 carry neitherkeepIDnorrecent. From that point the objective survived only inside the previous summary text, with no verbatim fallback if the summarizer dropped it.flattenAndDropOldest(~L691-759) is the text-transcript fallback thatdeliveruses when the request does not fit. It sends[previous summary][one user message containing the transcript]withtools: []. That prompt prefix differs from the normal request, which matchescache.read = 0/lcp = 3for compactions Roadmap & Existing Issues #2 and feat: model config persistence #3. The normal compactions (484+) read 17K–23K from cache.
Working hypothesis (not proven). The prompt that produced compaction #2 did not effectively present the previous summary's objective to the model. Two candidate mechanisms:
- (a) The flattened request lost or diluted the previous summary.
- (b) The model received it but ignored it under the "update the existing checkpoint" prompt.
Either way, mechanism (1) then accepted the result. One detail does not add up from the outside: 8,397 tokens is much smaller than both the 58.8K prior prompt and the room summaryRoom should leave at a 65,536/16,384 limit. Either flattenAndDropOldest received a much lower target than expected, or content was removed elsewhere. We have not located the exact line that drops it and do not claim one.
Suggested fix (proposals, untested)
- Validate before replacing. Treat a summary as failed when the previous summary had a non-empty
## Objectiveand the new one is empty or "none". Then keep the previous compaction and surface an error or retry instead of storingcompleted. - Carry the objective forward deterministically. Copy the previous Objective/Requirements when the new summary omits them, or always keep the last user request verbatim, even when no user message exists since the last compaction.
- Keep the old context on failure. A failed or suspicious compaction never replaces a valid one.
- Diagnostics. Log which
deliverpath was used (unchanged, history, or flattened), thetarget, the request estimate, and whether the previous summary was included. Log no content. - Measurements. Check that the token measurement used right after a compaction (the trigger at seq 187) describes the current, post-compaction context, so a stale count cannot trigger a premature second compaction.
Steps to reproduce
Observed (real session). This was one long agentic session with a local Qwen model at a 64K window and large tool outputs:
- Compaction feat: compact and other improvements #1 succeeds.
- With no new user message, two tool-heavy assistant steps push the prompt to about 55K, past the auto-compaction ceiling.
- Compaction Roadmap & Existing Issues #2 goes through a reduced, uncached request and produces the task-empty summary.
Proposed synthetic regression test (not yet built). It needs no private data:
- Session with model
limit{context: 65536, output: 16384}, served by Ollama at 64K or by a mock provider. - User message stating an explicit objective and constraints; force or trigger compaction feat: compact and other improvements #1.
- Without any new user message, run assistant steps whose tool results total about 40K tokens, so the next auto-compaction must use the flattened transcript path.
- Assert that compaction Roadmap & Existing Issues #2's request contains the previous summary's Objective text and that the stored summary's
## Objectivestill matches it. - Add a unit test where the summarizer stub returns a template-valid but task-empty summary; expect it to be rejected while a previous objective exists.
Related issues (similar symptoms, not claimed to be the same cause)
- compact silently lands reasoning-only (empty-body) summaries, causing irrecoverable context loss #44080: empty (reasoning-only) summary accepted as success. Here the summary has text and headings but no content.
- compaction: "Compaction produced no summary" kills every prompt in a long session #56857: "Compaction produced no summary". Here a summary was produced and accepted.
- [BUG] After auto-compaction the agent continues thinking/acting without confirmation and loses track of the original task goal #41358: task goal lost after auto-compaction on v1. The bot asked for a v2 report; this is one, with a different mechanism. Related PR: feat(core): enhanced compaction with ratios and combined context-restoration advancement #45125, closed.
- compaction: summary reports work as Active/Pending that was completed ~19h earlier — stale Work State propagates to handoffs #50816: semantically stale summary (wrong Work State) accepted.
- v2.0.32: every compaction runs twice, the same transcript is summarized again #57805: duplicate compaction on v2.0.32. Not confirmed here: compactions feat: compact and other improvements #1 and Roadmap & Existing Issues #2 were separate triggers about 4 minutes apart.
- V2: summary compaction still reads almost nothing from the prompt cache, even right after a warm request #52761 / fix(core): summarize the whole conversation during compaction #57091 / fix(core): shorten compaction transcripts oldest first before dropping history #58015 / refactor(schema): remove recent from compaction messages and events #57203: recent changes to the same request-building, transcript-shortening and
recentcode.
Reproduction status
The failure was observed once in a real session. A second, task-empty compaction followed in the same session, and we treat it as propagation, not an independent reproduction. It is corroborated by local DB token counts and the Ollama server log. No independent minimal reproduction exists yet. I can run a synthetic test or share redacted rows (types, token counts, reason/status, timestamps) on request. Private transcripts will not be shared.
Plugins
None
OpenCode version
2.2.3
Operating System
Windows 11 Pro (build 26300)
Activity
opencodejr commentedon Oct 30, 2026
Thanks for the detailed write-up and the token and Ollama data, they made this much easier to trace.
I rebuilt your scenario on the latest
dev(cb1e58f) with a simulated model at your limits (65,536 context, 16,384 output):## Objective.flattenAndDropOldest).What I found:
<conversation-checkpoint>. Then come the shortened transcript and the "Update the existing checkpoint…" prompt. So on our side the objective isn't dropped. In your session, the model received it and ignored it (your mechanism b).completedbecause OpenCode only checks that the reply follows the template (any required heading). It doesn't check that the content is meaningful.The last point is how it was built rather than a malfunction, so it may be better filed as a feature request. For example: reject or retry a summary that drops a previous non-empty Objective, or carry the Objective forward. If you can share the first lines of the actual compaction #2 request, or show that its previous summary was missing, please reply and we'll look again.
Related: #57240 (empty checkpoint replies marked succeeded), #50816 (stale summary accepted), #44080 (empty summary accepted on v1).
ivanfc commentedon Oct 30, 2026
I disagree that this is a feature request. From the user's side the result is silent data loss: the active objective disappears, the compaction is stored as "completed", and the agent restarts as if the session were new, with no warning and no way to notice except that it suddenly greets you. "Working as built" doesn't make that acceptable behavior; a summary that throws away a previously recorded objective should never silently replace a valid context.
For the record: on this machine all 17 automatic compactions so far are from local Qwen at a 64K window, and 2 of them came out task-empty. That's not a rare edge case for local models, which are exactly the ones most likely to ignore a checkpoint inside a long "update" prompt.
The minimum fix I'd expect as a bug fix, not an enhancement: if the previous summary has a non-empty
## Objectiveand the new one doesn't, don't mark it completed: retry once, and if it still fails, keep the previous compaction and surface an error. Please keep this open as a bug.John-Rood commentedon Oct 31, 2026
Three things in this thread are still open at the code level, and all three resolve in the v2.2.3 tree (082ac62, unchanged on current dev). Two of them come out your way.
Where the 8,397 tokens went
The target was not small. For an automatic compaction the budget is
calculateCeiling(compaction.ts:1134). With your limits, context 65,536 and output 16,384, that is 65,536 - 16,000 = 49,536, andsummaryRoom(:1148) returns the same number because 15% of the window is 9,830, underRESERVE_MIN. The summary request aimed at about 48.5k once prompt overhead is subtracted.What collapsed is the transcript, and the exact line is the level loop.
TRANSCRIPT_CLIP = 2_000(:150) withclip()(:1074) keeps 1,000 characters from the start and 1,000 from the end of a cut value, so it is 2,000 total, not 2,000 at each end.Shortening walks whole levels over each exchange (:742-751): whole, tool outputs clipped, tool outputs removed, user text only, omitted. There is no state between those levels.
With no user message after compaction #1,
recentStart(:1005) keeps nothing,splitConversation(:985) returnskeepID: undefined, andflattenAndDropOldestsees one group, because grouping starts a new group when the list is empty or the message is a user message (:715). Your entire post-checkpoint history is that one group.Level 0 is the full 55k, over room. The next reachable state is
OUTPUTS_CLIPPED, which cuts every tool result to 2,000 characters in a single pass. Eight tool results across the two steps is 16,000 characters, about 4,000 tokens, sitting in front of the roughly 9 KB checkpoint thatleadrebuilds (:702-704) at about 2,250 tokens, plus the summary prompt and the system message. 8,397 is what that adds up to. Nothing is removed elsewhere.That also answers your point 5. The count behind compaction #2 was 38,963 input + 16,598 cache read + 3,240 output = 58,801, against Ollama's
n_tokens = 58800.estimatePrompt(:1103) takes the last measured assistant row and adds an estimate of what came after it, so the figure was current to within a token, anddue(:422) fires at>= ceiling. There is no stale trigger to fix.The checkpoint was in the request
Not only in your rebuild.
leadis built frompreviousCompaction(:967) and rendered by to-llm-message.ts:293 as<conversation-checkpoint><summary>. No branch on the flattened path drops it. So your mechanism (b) is the one that happened: the model was given the objective and did not repeat it, and the code then accepted the result.The acceptance check is the defect
filled(:469) istext.split("\n").some((line) => headings.includes(line.trim())). One line that equals a template heading is enough, so a summary whose Objective reads "(none, no task has been established yet)" passes on the first attempt andpublishstores it ascompleted(:921).Keeping the previous context on failure is already the behaviour, one line further down. A compaction that is not
completedrenders as no messages at all (to-llm-message.ts:294), and history loads from the latest completed one, so a rejected summary leaves the old checkpoint as the lead. The auto path then fails the step (runner/llm.ts:243), which surfaces as a compaction error instead of a greeting.Your point 1 and point 3 are one change, and it is a predicate.
The predicate
One condition keeps it from failing legitimate compactions: only enforce it when no user turn has arrived since the checkpoint. After a user turn the update prompt allows the objective to change ("Newer history always takes precedence over the existing checkpoint",
buildPrompt:233).The existing
sendalready provides the retry you asked for. A reply that failsfilledgetsNUDGEonce, and a second failure becomescompaction.failed. With the predicate in place your task-empty summary gets one more attempt, and if it comes back empty again the turn fails visibly. Worth giving that case its own failure string so "did not match the required template" does not describe it.What I ran
I pulled the real
SUMMARY_TEMPLATEand the realfilledout of the tag instead of retyping them, then ran six cases: your summary passes today and is rejected with the predicate; a healthy summary still passes; a first compaction with no previous checkpoint is unaffected; a checkpoint whose objective is already "(none)" is unaffected; a compaction after a user turn is unaffected; a reply carrying only a legacy heading is still rejected. Six of six.A second bug worth its own issue
The coarse step. When the newest exchange is the only exchange, the loop cannot shorten anything older first, so it wipes detail inside the exchange the summary most needs while about 40k tokens of room go unused. Your incident has exactly that shape, which is why the compaction request looks nothing like the conversation it summarized. Shortening within an exchange, clipping the largest tool results before the rest, would recover the room. It is not required to stop the silent loss, but it is a real second defect and it is the one that made the request numbers look impossible.
Keep this open as a bug. The predicate cannot tell a usable summary from one that says there is no task, and the failure path you asked for already exists, so changing one boolean is not a design decision.
The cross-session version of the same loss is the problem I built MemoryRouter for: the conversation kept outside the context window and across compaction, so a checkpoint that comes back thin is not the only copy of the work. https://docs.memoryrouter.ai/coding-tools?utm_source=github&utm_medium=issue&utm_campaign=anomalyco-opencode
opencodejr commentedon Nov 2, 2026
Closing this as not planned, since we haven't heard back. On the latest
dev(cb1e58f), the shortened compaction request still put the previous summary's## Objectivein front of the model. The task-empty summary was the model ignoring it. OpenCode stored it ascompletedbecause it only checks the template structure, which is how it was built.If you'd like OpenCode to reject or carry forward a summary that drops a previous Objective, please open a feature request. If you can show the compaction #2 request from your session was missing the previous summary, comment here or open a new issue with that and we'll look again. Related: #57240, #50816.