Skip to content

Compaction persists a summary boundary when the request never returned: no parts, error null, tokens never populated #48543

Description

@sergioorestes

Summary

On v1.18.30 I see compaction summary messages persisted with no parts at all, error: null, and a token object that was never populated by a response. The session continues afterwards as if compaction had succeeded. Nothing surfaces the failure: there is no error to read and no retry.

This looks like a third trigger for the same family of failures already tracked in #42282, #44080 and #41571, but it matches none of them, so I am filing it separately rather than commenting. See the comparison below, and please close as duplicate if the guard in #42063 already covers this path.

How it differs from the existing reports

trigger error parts tokens
#42282 provider error on the summary turn set
#41571 / #44080 reasoning-only response, no text part null reasoning present populated
this report no response at all null [] — none never populated

The token object is the discriminator. A completed compaction carries a total key plus a real cache.write; these carry neither:

completed:  {"total": 19457, "input": 3, "output": 2985, "reasoning": 0, "cache": {"write": 16469, "read": 0}}
this bug:   {"input": 0, "output": 0, "reasoning": 0, "cache": {"read": 0, "write": 0}}

cache.write of 0 is the part I would rely on: had the prompt reached the API it would have been billed. None of these were.

Frequency and durations

7-day window, read from opencode.db. 85 compaction messages for anthropic/claude-haiku-4-5, of which 11 carry this exact signature — no parts, error: null, unpopulated tokens.

Recorded durations, in seconds, from the message time field:

2  ·  14  ·  20  ·  30  ·  50  ·  74  ·  89  ·  648  ·  1062  ·  3841  ·  36461

Two observations from that spread.

The long tail suggests some of these requests hang rather than fail fast. The largest is 36,461 s, slightly over ten hours, and there are three above 3,000 s. Whatever ends them, it is not a short timeout.

The short ones cluster in wall-clock time: four within 18 minutes on 09-07 starting at 23:03:43, and three within 11 minutes on 09-08 starting at 13:24:38. Consecutive pairs 51 s and 70 s apart look like retries that also came back empty, though I cannot confirm from the database alone that they are retries of one another.

Two further cases in the same window had a single patch part and still no text and no populated tokens. I left them out of the count of 11 because I am not sure they are the same path.

Impact

The compaction boundary is persisted, so the session proceeds against a summary that has no content. Of the 11, three were followed by further turns in the same session with no later compaction that produced text, which is unrecoverable context loss. The rest were the last message in their session, where nothing was lost.

The silence is the reportable part. With error null there is nothing for a monitoring script to key on, which is how these went unnoticed here: a local audit tool was attributing them to the compaction model's output quality until the token object showed no request had been billed.

Reproduction

I do not have a deterministic reproduction. These arose in normal use. The query that finds them in an existing database:

import sqlite3, json, time
from pathlib import Path

db = Path.home() / ".local/share/opencode/opencode.db"
c = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
since = int((time.time() - 7 * 86400) * 1000)

for mid, _, data in c.execute(
    "select id,time_created,data from message where time_created>?", (since,)
):
    r = json.loads(data)
    if not (r.get("summary") is True and r.get("mode") == "compaction"):
        continue
    parts = [json.loads(p) for (p,) in c.execute(
        "select data from part where message_id=?", (mid,))]
    text = "".join(p.get("text") or "" for p in parts if p.get("type") == "text")
    tok = r.get("tokens") or {}
    if not text.strip() and not r.get("error") and "total" not in tok:
        print(mid, r.get("providerID"), r.get("modelID"), [p.get("type") for p in parts], tok)

Expected

A compaction turn that never received a response should not persist a summary boundary, and should surface something an operator or a script can detect. Marking the summary errored, as #42063 proposes for empty summaries, would likely be sufficient if that guard is reached when there is no response at all rather than only when there is a response with no text.

Environment

  • opencode-ai 1.18.30, Linux x64
  • Compaction model: anthropic/claude-haiku-4-5, set via agent.compaction.model
  • All 11 cases are on that model. Two other compaction models in the same window, a local llama.cpp endpoint and a local Ollama endpoint, did not produce this signature, but they served far fewer compactions (12 and 11), so I would not read that as evidence the path is Anthropic-specific.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions