Description
Bug Description
In opencode, message.part.updated can arrive after message.part.delta for the same
partID on the /event SSE stream. Subscribers that use message.part.updated to register
a part before processing its deltas will encounter a race where a delta arrives first.
Environment
- OpenCode version: tested on v1.14.48
- Affected path:
SyncEvent.run → Session.updatePart → bus publish
- Trigger: any streaming response that emits text or reasoning parts
Steps to Reproduce
Connect to the SSE event stream and filter for message.part.* events while a prompt is
being processed. Observe that message.part.delta events for a given partID can appear
before the first message.part.updated for that same partID.
Expected Behavior
For each partID, message.part.updated arrives before any message.part.delta with that
same ID:
data: {"type":"message.part.updated","properties":{"part":{"id":"prt_abc","type":"text",...}}}
data: {"type":"message.part.delta","properties":{"partID":"prt_abc","field":"text","delta":"..."}}
data: {"type":"message.part.delta","properties":{"partID":"prt_abc","field":"text","delta":"..."}}
Actual Behavior
message.part.delta can arrive before message.part.updated for the same partID:
data: {"type":"message.part.delta","properties":{"partID":"prt_abc","field":"text","delta":"..."}}
data: {"type":"message.part.updated","properties":{"part":{"id":"prt_abc","type":"text",...}}}
Root Cause
Session.updatePart and Session.updatePartDelta publish to the bus through different
mechanisms:
updatePart routes through SyncEvent.run, which defers the bus publish via
Database.effect as a fire-and-forget runPromise call — the publish runs on a
separate fiber and may not complete before updatePart returns.
updatePartDelta calls yield* bus.publish(...) directly on the caller's fiber.
Even though processor.ts correctly calls updatePart before updatePartDelta, the delta's
publish can reach the PubSub first because it runs on the current fiber while the update is
still pending on its orphaned fiber.
The relevant code in sync/index.ts:
Database.effect(() => {
const result = convertEvent(def.type, event.data)
const publish = (data) => ProjectBus.publish(def, data, { id: event.id })
if (result instanceof Promise) void result.then(publish)
else void publish(result) // ← fire-and-forget, no caller awaits this
})
Impact
Any subscriber that uses message.part.updated to learn a part's type before processing its
deltas will mishandle or drop the early deltas. In our integration, it causes:
AI_UIMessageStreamError: Received text-delta for missing text part with ID "prt_...".
Ensure a "text-start" chunk is sent before any "text-delta" chunks.
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Description
Bug Description
In opencode,
message.part.updatedcan arrive aftermessage.part.deltafor the samepartIDon the/eventSSE stream. Subscribers that usemessage.part.updatedto registera part before processing its deltas will encounter a race where a delta arrives first.
Environment
SyncEvent.run→Session.updatePart→ bus publishSteps to Reproduce
Connect to the SSE event stream and filter for
message.part.*events while a prompt isbeing processed. Observe that
message.part.deltaevents for a givenpartIDcan appearbefore the first
message.part.updatedfor that samepartID.Expected Behavior
For each
partID,message.part.updatedarrives before anymessage.part.deltawith thatsame ID:
Actual Behavior
message.part.deltacan arrive beforemessage.part.updatedfor the samepartID:Root Cause
Session.updatePartandSession.updatePartDeltapublish to the bus through differentmechanisms:
updatePartroutes throughSyncEvent.run, which defers the bus publish viaDatabase.effectas a fire-and-forgetrunPromisecall — the publish runs on aseparate fiber and may not complete before
updatePartreturns.updatePartDeltacallsyield* bus.publish(...)directly on the caller's fiber.Even though
processor.tscorrectly callsupdatePartbeforeupdatePartDelta, the delta'spublish can reach the PubSub first because it runs on the current fiber while the update is
still pending on its orphaned fiber.
The relevant code in
sync/index.ts:Impact
Any subscriber that uses
message.part.updatedto learn a part's type before processing itsdeltas will mishandle or drop the early deltas. In our integration, it causes:
Plugins
No response
OpenCode version
No response
Steps to reproduce
No response
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response