Repository navigation
Web can persist duplicate final answers when client message IDs sort after assistant IDs #29478
Description
Description
OpenCode Web can persist duplicate assistant final answers for one Web-originated user prompt on 1.15.10. This appears to be backend/session duplication, not browser-only rendering.
In an exported session JSON, duplicate assistant messages share the same parentID, both have finish: "stop", and both text parts are final-answer text. The final answer text is byte-identical. The second request typically has tiny tokens.input and huge tokens.cache.read, showing it is an immediate follow-up model call over cached context.
Local logs show session.prompt starts step=1 loop immediately after the first finish=stop final answer and creates a second assistant message for the same parent. This appears to match a client-generated user ID ordering problem: a Web-created user message ID can sort after the server-created assistant ID. The current loop exit condition appears to require lastUser.id < lastAssistant.id; it should likely use the assistant/user parent relationship instead.
Environment
- OpenCode version:
1.15.10 - Install: npm package
opencode-ai - Access path: OpenChamber Web (
@openchamber/web@1.11.7) -> OpenCode Web backend (opencode web) - OS: Linux
- npm latest checked:
opencode-ai@latestis also1.15.10;@openchamber/web@latestis1.11.7
Evidence
Exact duplicate groups from an exported session:
- Parent
msg_e66b0324a001tkL7psLzlPJUtemsg_e66b02f80001pMYlCedum3PPo3:finish=stop, input71766, output107, cache.read37376msg_e66b042b5001ibwkcEbh1S8RLg:finish=stop, input223, output109, cache.read109056
- Parent
msg_e66b4c2bc001Bb9VRViN7mamWHmsg_e66b4c0c10016S1jhwII6eq3s3: input72114, output112, cache.read37376msg_e66b4d422001Tz56NfxFy9nNm2: input586, output110, cache.read109056
- Parent
msg_e66b522b7001TQuIxHrGhB8nGlmsg_e66b51feb001Zxmxsc7RBEqZVs: input72335, output94, cache.read37376msg_e66b532b1001CcTDEFYL1UnxHF: input267, output94, cache.read109568
The duplicate user parent IDs sort after their first assistant IDs, even though the user messages' stored time.created values precede the assistant messages by about 20 ms.
Service log pattern:
session.prompt ... step=0 loop
session.processor ... messageID=msg_e66b02f80001pMYlCedum3PPo3 process
... message finishes stop ...
session.prompt ... step=1 loop
session.processor ... messageID=msg_e66b042b5001ibwkcEbh1S8RLg process
... duplicate message finishes stop ...
session.prompt ... step=2 loop
Local validation
Prompt format used for local sanity checks: Reply with exactly: duplicate-test-<timestamp>
| Path | Result |
|---|---|
Direct CLI/TUI-style opencode run --pure --agent coordinator |
no duplicate persisted; one assistant final answer |
Direct CLI/TUI-style opencode run --pure --agent build |
no duplicate persisted; one assistant final answer |
CLI opencode run --attach http://127.0.0.1:4096 --agent coordinator |
no duplicate persisted; one assistant final answer |
| Web/OpenChamber session export | duplicates persisted in backend/session JSON |
Suspected root cause
The upstream source appears to still contain this loop exit condition in packages/opencode/src/session/prompt.ts:
if (
lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) &&
!hasToolCalls &&
lastUser.id < lastAssistant.id
) {
break
}That condition is unsafe when Web/client-generated user IDs and server-generated assistant IDs are compared lexicographically.
OpenChamber Web client appears to generate and submit the user messageID to /session/:id/prompt_async, so clock/order differences between client-side user IDs and server-side assistant IDs can trip this condition.
Suggested fix
Use the assistant/user relationship instead of lexicographic ID order:
if (
lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) &&
!hasToolCalls &&
- lastUser.id < lastAssistant.id
+ lastAssistant.parentID === lastUser.id
) {
break
}Rationale: the assistant message already records the user parent it answers. That relationship is the invariant the loop needs; message ID ordering is not reliable across client/server generation paths.
Impact
- Extra model call after a completed final answer
- Token/cache usage increase
- Persisted duplicate assistant messages pollute session context
- Display-only filtering would not fix the backend/session duplication
Plugins
None
OpenCode version
1.15.10
Steps to reproduce
No specific steps other than some messages return twice (new or existing session)
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Activity
github-actions commentedon May 27, 2026
This issue might be a duplicate of existing issues. Please check:
Please review these issues and confirm whether this is a new occurrence or a continuation of one of them.
akhamar commentedon May 31, 2026
Same issue on my side. On the tty client I get single response, on the web client I get everything duplicated and stored in session as is.
Which mean when loading previous session everything is duplicated.
Even when loading on tty cli, everything that as been done on web version is duplicated.
This is recent and extremely anyoing.
Edit:
I've solved the issue, it was created because of a time difference of a few millisecond between my server (running opencode web) and my browser on my current computer.
To reproduce just manually change the local time.
Like @carlsonb99 explained you need to change the code from something that is variant on clocks to something that is invariant.
BobbyL2k commentedon Jun 1, 2026
I've experienced the duplicate response too. But I've found it to be from my local LLM serving backend (llama.cpp server). Do you also happen to be using llama.cpp or its variant?
I'm not sure which commit in llama.cpp introduced the issue since I haven't done a bisect but found that the latest b9444 to be good, anything before b9283 to be good and b9320 exactly to be problematic.
The duplicate request is still an issue but at least the LLM serving engine on my side is not mixing up the two requests.
fix(session): use parent relationship instead of ID ordering for loop…
chouzz commentedon Jun 27, 2026
Confirming this still reproduces on the latest release, v1.17.11 (published 2026-06-25), and the severity appears worse than originally reported — every assistant reply is duplicated many times (not just final answers).
A fix is tracked in #31945, which is still open/unmerged, so updating does not resolve this. Leaving this newer repro here in case it helps get the fix merged.
Environment
1.17.11(latest at time of report)opencode weblaunched with web on port 4096:4096opencode web)Steps to reproduce
opencode web(listening on port 4096).which opencode version do you use?.Actual behavior
The single assistant turn is rendered multiple times in the conversation — the same response (including tool-call reasoning and final answer) appears repeatedly. In the example below a single question produced ~11 duplicate assistant messages. This makes the web UI effectively unusable for back-and-forth.
Expected behavior
One assistant reply per user prompt.
Evidence — full conversation from one question
Question asked once:
Resulting duplicated assistant messages (all from that single prompt):
Full duplicated transcript (click to expand)
Note: the duplicate blocks share the same "parent" turn (one user message) and each repeats the full assistant output — consistent with the loop-exit-by-ID-ordering problem described here and the fix in #31945.
Happy to provide exported session JSON or server logs if useful. 👍
slowdream commentedon Jul 2, 2026
Confirming this still reproduces on opencode-ai v1.17.13.
This appears to be the same root cause tracked in #29478 and fixed by #31945, but the fix is still open/unmerged.
Environment
1.17.13opencode-aiRepro
Send a short prompt such as:
Actual behavior
A single user message can produce multiple persisted assistant messages with the same parentID.
Example session:
Persisted messages:
The second assistant message may be visually hidden/empty in the UI, but it is still persisted and can enter the context/history.
Root cause confirmation
This matches the ID-ordering issue described here:
In the failing session, the client-generated user messageID sorts after the first server-generated assistant id, so the loop does not exit even though the assistant already finished with finish=stop.
When messageID is omitted and OpenCode generates the user id server-side, the same prompt produces only one assistant message.
So the issue is still present in 1.17.13, and #31945 still looks like the correct fix.
WhiteGiverMa commentedon Aug 3, 2026
Confirmed this still reproduces on v1.18.9. I am adding this as a non-duplicate data point because the failure can occur with only a very small ordering mismatch.
Observed invariant violation
Across three persisted occurrences:
time.createdpreceded its first assistant by 31–44 ms;finish: "stop"and had no tool calls;SessionPrompt.runLoop()created a second terminal assistant with the sameparentID.So the server-side durable timestamp and the lexical ID order disagreed. The next loop iteration misses its exit condition solely because of:
Minimal local validation
I changed only the exit predicate in
packages/opencode/src/session/prompt.ts:parentIDis the actual turn-ownership invariant, so it also handles equal timestamps and client/server clock differences without relying on an ordering heuristic.I added a regression test that seeds a completed assistant whose ID sorts before its user parent (
msg_avsmsg_z) and verifies thatprompt.loop()exits without starting another model request.Validation on the current source checkout:
This is the same one-line fix proposed in the issue, with a concrete current-version reproduction and regression coverage.
chouzz commentedon Aug 3, 2026
Just to add another data point: in my case the issue was definitively caused by clock skew between the browser (client) and the server running
opencode web.After I synchronized the system clock on the machine hosting the browser to match the server, the duplicates stopped completely. No code changes needed.
This aligns with what @akhamar already observed (manually changing local time triggers it) and confirms that the root cause is the fragile exit condition relying on lexicographic ID ordering (
lastUser.id < lastAssistant.id), which breaks whenever the client-side generated user message ID sorts after the server-generated assistant ID due to clock differences — even by just a few milliseconds.The fix in #31945 (switching to
parentID-based comparison) would make this invariant to clock skew entirely.github-actions commentedon Oct 3, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.