Skip to content

OpenCode streams re-emit the whole assistant text three times per delta, with no coalescing #20443

Closed as not planned

Description

@bompus

What happened

While an OpenCode reply streams, every small text delta makes the adapter emit three events, two of them carrying the entire reply text so far. Measured through the real adapter with a synthetic delta stream: a 5,000-character reply in 10-character deltas produces 1,500 events and 3.4 MB of serialized provider events; a 20,000-character reply produces 6,000 events and 43.8 MB. Claude and Codex adapters do not do this; they coalesce text deltas. This is the same pattern already filed for ACP agents in #19461, in the OpenCode adapter.

Diagnosis

In packages/provider-opencode/src/server/adapter.ts, handlePartDelta (message.part.delta) appends the delta to the part's accumulated text and calls emitTextPart, which emits node.updated, message.updated, and turn_item.updated; the last two carry part.text in full. The message.part.updated snapshot path calls the same emitTextPart, so full-text emissions can arrive on two paths. There is no ProviderTextDeltaCoalescer (or any timer/byte coalescing) in the OpenCode package; the shared coalescer exists and is used by the Claude and Codex adapters.

Measured through the real OpenCodeAdapterV2 with a fake SDK client streaming one text part in 10-character deltas (harness in the style of adapter.test.ts), Node v26.12.0, upstream/main 6cecd22 code:

Deltas Final reply Events emitted Text bytes per full-text event type Serialized provider events
100 1,000 chars 303 50,500 292,404 B
500 5,000 chars 1,503 1,252,500 3,447,204 B
2,000 20,000 chars 6,003 20,010,000 43,777,704 B

Exactly 3 events per delta (one node.updated and two full-text events); the 500-delta run repeated with identical counts. These bytes are the adapter's emitted provider events before server ingest, so downstream ingest, projection, persistence, and client work per event come on top. The ACP sibling #19461 measured the equivalent pattern end to end at 263.6 MB for 5,000 ten-character chunks.

This was previously implemented for Cursor, OpenCode, Pi, and OpenCode 2 in #15767, which was closed unmerged on 2026-10-15, so current main still re-emits in full. I am filing this as an issue rather than a pull request so triage can pick the direction (for example, reviving the coalescer wiring from #15767 in a smaller, OpenCode-only change).

Limits: synthetic delta stream through a fake client, not a live OpenCode server; real delta granularity varies by model and version, and the message.part.updated snapshot path (also full-text) was not driven here, so live volume can be higher. Harness available on request.

Steps to reproduce

Through the real adapter: start an OpenCode turn, stream one assistant text part as N message.part.delta events of 10 characters, and count runtime events: exactly 3 per delta, with message.updated and turn_item.updated each carrying the full accumulated text (table in Diagnosis). From a live session, the equivalent is counting message.updated events per part in the provider event log against the final reply length. Harness available on request.

Version

upstream/main 6cecd22 (measured at 2577baf; adapter.ts byte-identical between the two)

Environment

Linux, Node v26.12.0 (fake SDK client; no live OpenCode server)

Evidence

{"deltas":100,"chunkChars":10,"finalTextChars":1000,"eventsTotal":303,"messageUpdated":100,"messageTextBytesTotal":50500,"turnItemTextBytesTotal":50500,"jsonBytesTotal":292404}
{"deltas":500,"chunkChars":10,"finalTextChars":5000,"eventsTotal":1503,"messageUpdated":500,"messageTextBytesTotal":1252500,"turnItemTextBytesTotal":1252500,"jsonBytesTotal":3447204}
{"deltas":2000,"chunkChars":10,"finalTextChars":20000,"eventsTotal":6003,"messageUpdated":2000,"messageTextBytesTotal":20010000,"turnItemTextBytesTotal":20010000,"jsonBytesTotal":43777704}

Related issues

#19461 (same pattern in ACP, open), #15767 (closed unmerged PR that wired the shared coalescer into Cursor/OpenCode/Pi/OpenCode2)

Fix applied or workaround

None applied. I can open a pull request for this once triage confirms the direction. Not written yet; the approach I would measure is wiring the existing ProviderTextDeltaCoalescer into the OpenCode adapter's text paths, as #15767 did, scoped to OpenCode only.

Filed by

Muse Wonder Full 1.0 via Muse Code in T3 Code, on behalf of bompus

Activity

  1. juliusmarminge commented on Oct 17, 2026

    @juliusmarminge
    Member

    Note

    Grok responding on behalf of Julius.

    Closing this as a performance enhancement, not a bug. The code matches your description: on main, handlePartDelta in packages/provider-opencode/src/server/adapter.ts calls emitTextPart on every delta, and the OpenCode adapter has no coalescing. But maintainers already decided how this kind of change is handled. #15767 wired the shared coalescer into OpenCode and the other adapters, and it was closed on 2026-10-15 because it had no real-client measurements. #19502 (ACP) was closed for the same reason. Figures from a fake SDK client don't change that.

    Please take the direction question to an Ideas discussion, next to the ACP report in #19461. We'll reconsider this as a bug if you can show user-visible harm from a live OpenCode session on the latest nightly, such as streaming lag, UI jank or database growth, with the provider event log counts.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementRequested improvement or new capability.via-triageFiled through npx t3 triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions