Skip to content

Web prompts can stop after the first reply when client and server clocks differ #24476

Closed

Description

@Jovines

Description

I ran into a behavior when using opencode serve with the Web UI: the first message received a normal assistant response, but subsequent messages were persisted as user messages without triggering another assistant response.

The same model and configuration worked normally in the terminal/TUI.

After some debugging, this seems related to Web client-generated messageIDs and clock skew between the browser/client device and the server.

What I observed

  1. Start opencode with opencode serve.
  2. Open the Web UI from a browser.
  3. Send the first message.
  4. The assistant responds normally.
  5. Send additional messages.
  6. The later user messages are persisted in the database.
  7. No new assistant message is created.
  8. The server log shows:
session.prompt ... exiting loop

Steps to reproduce

  1. Start opencode serve.
  2. Open the Web UI from a browser/device whose clock differs from the server clock.
  3. Send a first message.
  4. Wait for the assistant response.
  5. Send another message.
  6. Observe that the later user message is persisted, but no new assistant response is generated.

Debugging notes

In the database, the later user messages had later time_created values, but their messageIDs sorted before the first assistant message.

In my environment, the server clock was about 170 seconds ahead of the client/browser device that was accessing the Web UI.

After decoding the timestamp portion of the message IDs, the sequence looked roughly like this:

user1       09:38:26
assistant1 09:41:16
user2       09:38:37
user3       09:38:38
user4       09:38:39

So user2, user3, and user4 were sent after assistant1, but their IDs still sorted before assistant1.

I noticed this condition in packages/opencode/src/session/prompt.ts:

if (
  lastAssistant?.finish &&
  !["tool-calls"].includes(lastAssistant.finish) &&
  !hasToolCalls &&
  lastUser.id < lastAssistant.id
) {
  yield* slog.info("exiting loop")
  break
}

My understanding is that, in the Web UI case, the user message ID can be generated using the browser/client device clock, while the assistant message ID is generated using the server clock. If those clocks differ enough, the ID ordering may not match the actual conversation order, and the prompt loop may conclude that the latest user message has already been answered.

Additional check

I also tested sending a prompt directly to the server API without providing a client-generated messageID, allowing the server to generate the user message ID.

In that case, the subsequent assistant response was triggered correctly.

Impact

This may be more likely to happen when using the Web UI from another device, for example:

  • accessing opencode serve from a phone or another computer
  • the browser/client device clock differs from the server clock
  • the server clock itself is not synchronized

The user-facing behavior is a little confusing because the message appears to be sent successfully, but no assistant response appears.

Question

Would it make sense to avoid relying on ordering between client-generated user message IDs and server-generated assistant message IDs in this path?

For example, perhaps Web prompts could have their final user message IDs generated or normalized on the server, or this loop condition could use a server-trusted ordering mechanism instead.

Plugins

None

OpenCode version

1.14.25

Operating System

Ubuntu 24.04

Terminal

zsh

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Apr 26, 2026
  2. github-actions commented on Apr 26, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    If those issues were resolved but the problem has resurfaced, please note that in this issue so maintainers are aware of the regression.

  3. changed the title [-]Web prompts may stop replying when client and server clocks differ[/-] [+]Web prompts can stop after the first reply when client and server clocks differ[/+] on Apr 26, 2026
  4. quark-zju commented on Apr 30, 2026

    @quark-zju

    I hit the same issue. #25024 - the server's clock is ahead of the client's clock. The key problem is the message id:

    • Both client and server can generate the id (server trusts the msg id from client blindly)
    • The msg id is used in many places to sort the conversation (instead of using parent ids, or use "created_at" timestamp)

    LLM generated details:

    1) Observed mismatch: message.id ordering vs time.created

    In failed cases, message.id lexical order does not match message.time.created order across user and assistant turns.

    2) Two ID generation origins

    • Client-generated user IDs:
      • packages/app/src/components/prompt-input/submit.ts (Identifier.ascending("message"))
    • Server-generated assistant IDs:
      • packages/opencode/src/session/prompt.ts (MessageID.ascending())

    3) Ordering assumptions in core logic

    OpenCode relies on ID-based ordering/comparison (not only time.created) in multiple places, e.g.:

    • prompt loop condition comparing user/assistant IDs:
      • packages/opencode/src/session/prompt.ts (lastUser.id < lastAssistant.id)
    • binary-search insertion/lookups over ID-sorted arrays:
      • packages/core/src/util/binary.ts
      • packages/app/src/context/global-sync/event-reducer.ts
      • packages/app/src/context/sync.tsx

    So correctness depends on IDs sharing a single monotonic timeline.

    4) Why this fails with clock skew

    If user IDs come from client time and assistant IDs come from server time, skew can make one side’s IDs “jump ahead”.
    Then ID comparisons no longer represent actual turn chronology, and run/loop behavior becomes inconsistent.


    Proposed Fix (suggestion)

    Maintain a monotonic server-side ID floor:

    1. Server tracks the highest seen message ID per session (including client-provided user IDs).
    2. When generating any new server message ID, ensure it is strictly greater than that floor.
    3. If an incoming client message ID is greater than current floor, advance floor to that value.
    4. Optionally, reject or normalize client IDs that violate format/order guarantees.

    This keeps all IDs in one monotonic sequence even under clock skew.

    The frontend (client) needs to do a similar thing in case the client's clock is ahead.

  5. removed
    webRelates to opencode on web / desktop
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on May 3, 2026
  6. FantasyLeo commented on Jul 2, 2026

    @FantasyLeo

    I hit the mirror case of this issue with the same root cause. Sharing analysis
    so both symptoms are documented.

    Symptom (mirror case): When the client clock is ahead of the server clock,
    a single user message in plan mode generates 8+ parallel assistant replies,
    all sharing the same parentID, instead of 1. Each reply references the previous
    one, forming a self-conversation chain.

    Share data:

    Why 8 iterations:
    From 161a1y2J, the ID hex prefixes show the client clock was ahead:

    Message ID prefix vs user
    User f208f0f1f001 —
    Reply 1 f208ed581002 ed58 < f0f1 ✗
    Reply 2 f208ede31002 ede3 < f0f1 ✗
    ... ...
    Reply 7 f208f0cbd002 f0cb < f0f1 ✗
    Reply 8 f208f1d31002 f1d3 > f0f1 ✓

    The loop creates assistant messages with server Date.now(). Only the 8th
    assistant (~16s later) finally sorts above the user ID, allowing the 9th
    iteration to break. So the bug is:

    • server clock ahead → lastUser.id < lastAssistant.id is true on subsequent
      messages → loop exits prematurely (your symptom)
    • client clock ahead → condition is false even on 1st message → loop never
      breaks → N duplicate replies (my symptom)

    Verified: Advancing the client clock by 10s increases reply count;
    NTP-syncing clocks eliminates the issue entirely.

    Suggested fix in packages/opencode/src/session/prompt.ts:1115:

    - lastUser.id < lastAssistant.id
    + lastAssistant.parentID === lastUser.id

    Every assistant is created with parentID = lastUser.id (prompt.ts:1188),
    directly encoding the relationship without relying on cross-machine ID ordering.
    This fixes both the premature-exit and duplicate-reply directions.

  7. wkgcass commented on Jul 28, 2026

    @wkgcass
    Image
  8. kgothatsontsane commented on Aug 15, 2026

    @kgothatsontsane

    Third trigger, same broken assumption. The parentID fix in PR #38213 covers it.

    This thread keeps surfacing one root cause: the prompt loop orders the conversation by comparing message ID strings, which don't reliably reflect real time. @Jovines documented server-ahead skew → premature exit; @FantasyLeo documented client-ahead → duplicate replies. I hit a third trigger with no client/server split at all — the ID generator wrapped.

    The trigger: 48-bit overflow in id.ts

    id.ts packs Date.now() * 0x1000 + counter (52 bits) into Buffer.alloc(6) (48 bits). That wraps every 2^48 / 4096 = 2^36 ms ≈ 2.18 years after epoch. My machine wrapped at 2026-08-15T00:39:55Z (= 26 × 2^36 ms). From then on, every new message — server-generated included — got a small ID like msg_004c… sorting below old ones like msg_fd4a…. New IDs stopped being larger than old IDs, purely by arithmetic.

    Symptom: a session with pre-wrap history stopped responding. Progress bar flashed, then silence. New user messages persisted, no assistant message created. Log: loop → step=0 → exiting loop. Fresh sessions worked fine, which made it confusing.

    Same exit condition from the original post:

    lastUser.id < lastAssistant.id   // exits prematurely when this is true

    latest() picks the newest message by max ID. Post-wrap, a fresh user message has a smaller ID than the last assistant reply, so the loop treats it as already answered. Same logic as the 170s skew, just no skew present.

    What this means for the fixes here:

    • The monotonic floor idea is good defense-in-depth, but only holds if every ID in the conversation honors it — client-provided, plugin-generated, and server IDs alike. That's its own coordination problem.
    • PR fix: stop clock-skew response loops #38213 (lastAssistant.parentID === lastUser.id) is the real fix: it stops comparing ID strings for loop exit entirely. parentID is already set by the creating code, so it's immune to clocks and the wrap. I verified the wrap case — post-wrap, the assistant still carries parentID === lastUser.id, so the loop exits correctly.

    One gap: parentID alone doesn't fix the insertion side. binary.ts, event-reducer.ts, and sync.tsx (per @quark-zju) binary-search ID-sorted arrays and will misplace messages after a wrap even if the loop runs. So parentID loop fix plus the floor (or 8-byte buffer), not either alone.

    Manual repair that worked: renumber post-wrap message IDs above the last assistant ID. Only message.id and part.message_id needed touching (audited every table). Single transaction, 0 orphan parts. I also renumbered ~18,500 pre-wrap messages across all other sessions so they don't freeze on their next message. One-time cost; doesn't prevent the next wrap.

    Proposed fix:

    1. Ship the parentID exit condition (PR fix: stop clock-skew response loops #38213) — closes the loop bug for every trigger. Priority.
    2. Monotonic per-session ID floor — keeps ID order trustworthy for the binary-search insertion paths.
    3. Widen the timestamp buffer from 6 to 8 bytes in id.ts — moves the wrap from ~2.18 years to ~143,000 years.

    1 is the fix; 2 and 3 are cheap insurance. I confirmed v1.18.18 still ships Buffer.alloc(6), so all three triggers remain reproducible on current releases.

  9. opencode-agent commented on Aug 17, 2026

    @opencode-agent
    Contributor

    This was fixed in version 1.18.15. Please upgrade to 1.18.15 or newer.

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

Metadata

Metadata

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