Skip to content

Web can persist duplicate final answers when client message IDs sort after assistant IDs #29478

Closed as not planned

Description

@carlsonb99

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@latest is also 1.15.10; @openchamber/web@latest is 1.11.7

Evidence

Exact duplicate groups from an exported session:

  • Parent msg_e66b0324a001tkL7psLzlPJUte
    • msg_e66b02f80001pMYlCedum3PPo3: finish=stop, input 71766, output 107, cache.read 37376
    • msg_e66b042b5001ibwkcEbh1S8RLg: finish=stop, input 223, output 109, cache.read 109056
  • Parent msg_e66b4c2bc001Bb9VRViN7mamWH
    • msg_e66b4c0c10016S1jhwII6eq3s3: input 72114, output 112, cache.read 37376
    • msg_e66b4d422001Tz56NfxFy9nNm2: input 586, output 110, cache.read 109056
  • Parent msg_e66b522b7001TQuIxHrGhB8nGl
    • msg_e66b51feb001Zxmxsc7RBEqZVs: input 72335, output 94, cache.read 37376
    • msg_e66b532b1001CcTDEFYL1UnxHF: input 267, output 94, cache.read 109568

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

  1. github-actions commented on May 27, 2026

    @github-actions
    Contributor

    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.

  2. akhamar commented on May 31, 2026

    @akhamar

    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.

    Image

    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.

  3. BobbyL2k commented on Jun 1, 2026

    @BobbyL2k

    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.

    Image 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.

    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.

  4. added a commit that references this issue on Jun 25, 2026
    225fb1c
  5. chouzz commented on Jun 27, 2026

    @chouzz

    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

    • OpenCode version: 1.17.11 (latest at time of report)
    • Install: opencode web launched with web on port 4096
    • OS hosting OpenCode: Linux
    • Browser: Microsoft Edge (desktop), connecting to the web UI on :4096
    • Access path: OpenCode Web backend (opencode web)

    Steps to reproduce

    1. Launch opencode web (listening on port 4096).
    2. Open the web UI in Edge.
    3. Ask a simple question, e.g. which opencode version do you use?.
    4. Observe the assistant reply.

    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:

    which opencode version do you use?

    Resulting duplicated assistant messages (all from that single prompt):

    Full duplicated transcript (click to expand)
    which opencode version do you use?
    
    The user is asking which opencode version I use. Let me check this by running the opencode command to get the version. The user has been echoing my responses, but this is a genuine question now (twice asked). Let me actually check the version.
    
    You're running opencode v1.17.11.
    
    Confirmed — opencode v1.17.11.
    
    Correct. What would you like to work on?
    
    That's my line! 😄 I'm the assistant. Send me a real task whenever you're ready.
    
    Ha, you got me! 😄 Mirror loop continues.
    
    I'll just wait. Drop me a real task whenever.
    
    😄 Standing by for a real task.
    
    Standing by.
    
    😄 Loop continues! I'll wait for a real task from you.
    
    😄 OK, I give up trying to break it. Standing by.
    
    😄 Truce! I'm genuinely standing by — drop me a real task whenever.
    

    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. 👍

  6. slowdream commented on Jul 2, 2026

    @slowdream

    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

    • OpenCode version: 1.17.13
    • Install: npm package opencode-ai
    • Access path: OpenCode Web / OpenChamber / desktop client connected to OpenCode server
    • OS hosting OpenCode: Linux

    Repro

    Send a short prompt such as:

    пинг?
    

    Actual behavior
    A single user message can produce multiple persisted assistant messages with the same parentID.

    Example session:

    ses_0dbe017eaffebxH0XDd1uUyC64
    

    Persisted messages:

    msg_f241fea5d001scY0ktk1u3COpT  user
    msg_f241fe880001J2KF71oFfznUfV  assistant  parent=msg_f241fea5d001scY0ktk1u3COpT  finish=stop
    msg_f241ffe4e001pEhuUDCD22Cw3Y  assistant  parent=msg_f241fea5d001scY0ktk1u3COpT  finish=stop
    

    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:

    lastUser.id < lastAssistant.id

    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.

  7. WhiteGiverMa commented on Aug 3, 2026

    @WhiteGiverMa

    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:

    • the user row's stored time.created preceded its first assistant by 31–44 ms;
    • nevertheless, the first assistant ID sorted lexicographically before its user parent ID;
    • the assistant finished with finish: "stop" and had no tool calls;
    • 4–7 ms after that terminal completion, SessionPrompt.runLoop() created a second terminal assistant with the same parentID.

    So the server-side durable timestamp and the lexical ID order disagreed. The next loop iteration misses its exit condition solely because of:

    lastUser.id < lastAssistant.id

    Minimal local validation

    I changed only the exit predicate in packages/opencode/src/session/prompt.ts:

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

    parentID is 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_a vs msg_z) and verifies that prompt.loop() exits without starting another model request.

    Validation on the current source checkout:

    bun test test/session/prompt.test.ts
    57 pass, 1 skip, 0 fail
    
    bun typecheck
    passed
    

    This is the same one-line fix proposed in the issue, with a concrete current-version reproduction and regression coverage.

  8. chouzz commented on Aug 3, 2026

    @chouzz

    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.

  9. github-actions commented on Oct 3, 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