Repository navigation
Web prompts can stop after the first reply when client and server clocks differ #24476
Description
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
- Start opencode with
opencode serve. - Open the Web UI from a browser.
- Send the first message.
- The assistant responds normally.
- Send additional messages.
- The later user messages are persisted in the database.
- No new assistant message is created.
- The server log shows:
session.prompt ... exiting loop
Steps to reproduce
- Start
opencode serve. - Open the Web UI from a browser/device whose clock differs from the server clock.
- Send a first message.
- Wait for the assistant response.
- Send another message.
- 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 servefrom 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
github-actions commentedon Apr 26, 2026
This issue might be a duplicate of existing issues. Please check:
opencode webremotely (with workaround) #18807: Bug: prompt loop ID comparison still broken in v1.3.0 when runningopencode webremotely (with workaround) (closed)If those issues were resolved but the problem has resurfaced, please note that in this issue so maintainers are aware of the regression.
[-]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[/+]quark-zju commentedon Apr 30, 2026
I hit the same issue. #25024 - the server's clock is ahead of the client's clock. The key problem is the message id:
LLM generated details:
1) Observed mismatch:
message.idordering vstime.createdIn failed cases,
message.idlexical order does not matchmessage.time.createdorder across user and assistant turns.2) Two ID generation origins
packages/app/src/components/prompt-input/submit.ts(Identifier.ascending("message"))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.:packages/opencode/src/session/prompt.ts(lastUser.id < lastAssistant.id)packages/core/src/util/binary.tspackages/app/src/context/global-sync/event-reducer.tspackages/app/src/context/sync.tsxSo 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:
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.
FantasyLeo commentedon Jul 2, 2026
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:
finish: "stop", no tool callsfinish: "stop"/finish: "tool-calls"Why 8 iterations:
From
161a1y2J, the ID hex prefixes show the client clock was ahead:f208f0f1f001f208ed581002ed58<f0f1✗f208ede31002ede3<f0f1✗f208f0cbd002f0cb<f0f1✗f208f1d31002f1d3>f0f1✓The loop creates assistant messages with server
Date.now(). Only the 8thassistant (~16s later) finally sorts above the user ID, allowing the 9th
iteration to break. So the bug is:
lastUser.id < lastAssistant.idis true on subsequentmessages → loop exits prematurely (your symptom)
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: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.
wkgcass commentedon Jul 28, 2026
kgothatsontsane commentedon Aug 15, 2026
Third trigger, same broken assumption. The
parentIDfix 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.tsid.tspacksDate.now() * 0x1000 + counter(52 bits) intoBuffer.alloc(6)(48 bits). That wraps every2^48 / 4096 = 2^36 ms≈ 2.18 years after epoch. My machine wrapped at2026-08-15T00:39:55Z(= 26 × 2^36 ms). From then on, every new message — server-generated included — got a small ID likemsg_004c…sorting below old ones likemsg_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:
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:
lastAssistant.parentID === lastUser.id) is the real fix: it stops comparing ID strings for loop exit entirely.parentIDis 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 carriesparentID === lastUser.id, so the loop exits correctly.One gap:
parentIDalone doesn't fix the insertion side.binary.ts,event-reducer.ts, andsync.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.idandpart.message_idneeded 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:
parentIDexit condition (PR fix: stop clock-skew response loops #38213) — closes the loop bug for every trigger. Priority.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.opencode-agent commentedon Aug 17, 2026
This was fixed in version 1.18.15. Please upgrade to 1.18.15 or newer.