Skip to content

message.part.updated arrives after message.part.delta #26924

Closed as not planned
Closed as not planned

Description

@Nifury

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

Activity

  1. github-actions commented on Jul 11, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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