Skip to content

Bug: Assistant messages invisible in web UI when client clock is ahead of server #23092

Description

@heimoshuiyu

Description

Assistant messages disappear in the web UI when the client browser clock is ahead of the server clock. The TUI renders the same session correctly.

Root cause: The browser generates user message IDs using local Date.now() for optimistic updates (packages/app/src/components/prompt-input/submit.ts:489). The server reuses this client-generated ID (packages/opencode/src/session/prompt.ts:944). When the client clock is ahead, the user message ID sorts after the first assistant message ID, and the web UI's forward-only positional scan in session-turn.tsx skips it.

The fundamental issue is that the server trusts client-generated time-based IDs, which are not monotonically ordered when clocks diverge.

Reproducible example

A session with 3124ms client clock skew produced this message order (sorted by ID):

[0] Assistant 1:  msg_d9b6fff38001...  ← sorts BEFORE user message
[1] User message:  msg_d9b700b5d001...
[2] Assistant 2:  msg_d9b701e9f001...
[3] Assistant 3:  msg_d9b704423001...

The assistantMessages memo in packages/ui/src/components/session-turn.tsx scans forward from the user message index only:

for (let i = index + 1; i < messages.length; i++) {  // never visits index 0
  if (item.role === "user") break
  if (item.role === "assistant" && item.parentID === msg.id) result.push(item)
}

Assistant 1 at index 0 is never visited → invisible in the UI.

Bug-introducing commit

c0f9b1363 ("fix(desktop): more fine-grained state updates") changed the scan from a full .filter() to a forward-only positional scan, assuming IDs always sort chronologically.

Fix

Replace the forward-only scan with a full-array scan using parentID matching:

-  for (let i = index + 1; i < messages.length; i++) {
+  for (let i = 0; i < messages.length; i++) {
     const item = messages[i]
     if (!item) continue
-    if (item.role === "user") break
     if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)

This is safe because:

  • parentID matching is the correct invariant, not positional ordering
  • Revert/fork/retry are unaffected (reverted messages are filtered out before rendering; fork generates fresh IDs)
  • Performance impact is negligible (createMemo with equals: same deduplicates, content-visibility: auto skips off-screen turns)

A more robust fix would also have the server generate all IDs server-side instead of trusting client-generated time-based IDs.

OpenCode version

1.4.8 (still present)

Steps to reproduce

  1. Run OpenCode server on a machine whose clock is behind the client browser by a few seconds
  2. Open the web UI and send a message that triggers a multi-step response
  3. The first assistant message (typically containing reasoning + text + tool call) will not render
  4. Switch to TUI — all messages display correctly

Related

Metadata

Metadata

Assignees

No one assigned

    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