Repository navigation
[Bug] Agent loop produces duplicate responses due to non-monotonic MessageID comparison #38787
Description
Description
Agent loop fails to exit after finish=stop when the assistant message ID is lexicographically smaller than the user message ID, causing a duplicate LLM call and a second assistant response.
In packages/opencode/src/session/prompt.ts, the runLoop function uses a string comparison to decide whether to break the loop:
if (
lastAssistant?.finish &&
!["tool-calls"].includes(lastAssistant.finish) &&
!hasToolCalls &&
lastUser.id < lastAssistant.id // string lexicographic comparison
) {
break
}MessageID.ascending() generates IDs like msg_f93b221d1.... These IDs are not guaranteed to be lexicographically monotonic. When the assistant message ID happens to sort before the user message ID, the condition lastUser.id < lastAssistant.id evaluates to false, the loop does not break, and a second LLM call is made — producing a duplicate assistant response.
This only affects agents that return finish=stop without tool calls (e.g. a plan-mode / discussion agent). Agents that use finish=tool-calls follow a different code path and are unaffected.
Concrete evidence
Session with 5 consecutive duplicate pairs. Each pair: same parentID, two finish=stop assistant messages, gap of 2–32ms between them.
| User message ID | First assistant ID | user.id < asst1.id |
Second assistant ID | user.id < asst2.id |
|---|---|---|---|---|
msg_f93b221d1... |
msg_f93b221b9... |
false ← loop continues |
msg_f93b2853f... |
true ← loop breaks |
msg_f93b7b78b... |
msg_f93b7b748... |
false ← loop continues |
msg_f93b7efa2... |
true ← loop breaks |
msg_f93bb731c... |
msg_f93bb72da... |
false ← loop continues |
msg_f93bbd637... |
true ← loop breaks |
msg_f93c03ac5... |
msg_f93c03a9e... |
false ← loop continues |
msg_f93c0b842... |
true ← loop breaks |
msg_f93c23126... |
msg_f93c230ef... |
false ← loop continues |
msg_f93c26a5d... |
true ← loop breaks |
In all 5 cases, the first assistant message's ID is lexicographically smaller than the user's, so the loop runs one extra iteration and produces a second response.
Plugins
oh-my-openagent@latest
OpenCode version
1.18.5
Steps to reproduce
- Use an agent that responds with text only (no tool calls), returning
finish=stopdirectly. - Send a message. In ~some cases (when
MessageID.ascending()produces an assistant ID that sorts before the user ID), the agent will respond twice with similar but slightly different content. - The issue is intermittent because it depends on ID generation, but happens frequently in sessions with many turns.
Suggested fix
Replace the string ID comparison with a more reliable ordering check. Options:
- Compare by
time.createdtimestamps instead of ID strings - Use a numeric counter that is guaranteed monotonic
- Or at minimum, don't use the ID comparison as the loop exit gate —
finish=stop(when nottool-callsand no pending tool calls) should be sufficient to break
Operating System
macOS 15.7.7 (x64)
Terminal
OpenChamber (Electron app, opencode v1.18.5)
Activity
woohahahaaa commentedon Jul 25, 2026
Runtime info
[-][Bug] Agent loop produces duplicate responses when MessageID.ascending() generates non-monotonic IDs[/-][+][Bug] Agent loop produces duplicate responses due to non-monotonic MessageID comparison[/+]github-actions commentedon Jul 25, 2026
This issue might be a duplicate of existing issues. Please check:
lastUser.id < lastAssistant.idlexicographic comparison fails when client-generated user IDs sort after server-generated assistant IDs, causing a duplicatefinish=stopassistant response. Includes an identical suggested fix (lastAssistant.parentID === lastUser.id).finish=stop.illusive-c commentedon Aug 4, 2026
This would actually explain why this issue is intermittent.
11inyuan commentedon Aug 14, 2026
Additional corroborating case (from a closed duplicate, #42570):
On 2026-08-14T11:19:55Z the 36-bit embedded timestamp in MessageID wrapped (26 x 2^36 ms). Sessions with messages on both sides of the wrap show the same non-monotonic ordering, but flipped the other way: pre-wrap assistant IDs (msg_ffff...) now sort AFTER the new post-wrap user ID (msg_0001...), so \lastUser.id < lastAssistant.id\ evaluates true and the loop exits at step=0 without any reply — opposite symptom of the duplicate-response case here.
Same root cause, same line of code. Worked around locally by rewriting affected session message IDs in SQLite to a monotonic scheme (backup first). Agree with the suggested fix: compare by time.created, or use a full-width timestamp in the ID.
opencode-agent commentedon Aug 17, 2026
This was fixed in version 1.18.15. Please upgrade to 1.18.15 or newer.
fluffyspace commentedon Oct 13, 2026
Cross-link: #55204 (v2.0.28, still reproducing). Not the same failure mode as this issue's loop-exit bug — there the assistant message gets generated twice; here the user's queued message itself is re-injected into the model prompt on every loop step after an interrupted run (4–7 copies seen, while the DB holds exactly one row per send). Sharing the trigger area (interrupt + resume + queued messages), so noting the relation for whoever triages either one.
alterdaemon commentedon Oct 13, 2026
Confirmed. Same issues, experienced through 2.0.19 till current.