Description
OpenCode accepts caller-supplied messageIDs on prompt input, but SessionPrompt.run later assumes message IDs are OpenCode-generated ascending IDs and uses lexical ID comparison to decide transcript ordering.
That is a bug in two ways:
- The API accepts custom message IDs, so OpenCode should not assume all message IDs preserve its internal ordering semantics.
- Transcript/order logic should not depend on ID ordering in the first place. It should use actual message timestamps or transcript position.
Relevant code
OpenCode accepts custom user message IDs here:
- packages/opencode/src/session/prompt.ts
const info: MessageV2.User = {
id: input.messageID ?? MessageID.ascending(),
...
}
But later SessionPrompt.run uses ID ordering to decide whether to exit the loop:
- 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
}
There is a second similar assumption here:
- packages/opencode/src/session/prompt.ts
if (step > 1 && lastFinished) {
for (const m of msgs) {
if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue
...
}
}
OpenCode’s own generated IDs are monotonic because MessageID.ascending() uses a timestamp-plus-counter scheme:
- packages/opencode/src/id/id.ts
So this works only as long as all message IDs come from OpenCode’s own generator.
Why this is a real bug
If a caller passes a valid custom messageID like msg_ or any other globally unique ID that does not sort lexically the same way as OpenCode IDs, the prompt loop can mis-order the transcript logically.
I hit this on a question tool resume flow:
- Start a prompt with a caller-supplied messageID
- Assistant finishes normally with finish = "stop"
- OpenCode should stop
- Instead, lastUser.id < lastAssistant.id is false because the user ID is custom and the assistant ID is OpenCode-generated
- The loop runs one extra iteration
- OpenCode sends another completion request with a transcript whose last message is assistant.
Providers that validate chat turn structure reject that request. In my case, Anthropic via OpenRouter failed with an error equivalent to: last message must be a user message.
So this is not just theoretical: a valid custom ID can cause an extra model call after a completed assistant turn.
Expected behavior
- If OpenCode accepts a caller-supplied messageID, all runtime logic should continue to work correctly regardless of that ID’s lexical ordering.
- Transcript sequencing should be based on actual order, not on comparing opaque IDs.
- A resumed question flow should not re-enter the model loop after the assistant has already finished.
Actual behavior
- SessionPrompt.run assumes lastUser.id < lastAssistant.id
- That assumption fails with custom IDs
- The prompt loop may run an extra iteration
- OpenCode may send an invalid provider transcript ending in assistant
Suggested fix
Do not use message ID ordering as a proxy for transcript order.
Better options:
- Use transcript position directly from msgs
- Or compare an actual created-time field
- Or persist/use a real DB/event-store timestamp / sequence column if available
At minimum, the loop exit and reminder logic should stop comparing opaque IDs:
- lastUser.id < lastAssistant.id
- m.info.id <= lastFinished.id
Those should be replaced by ordering based on actual message time or array position in the current transcript.
Why this matters for API design
Right now the API surface implies that passing messageID is supported, but the implementation only works reliably if callers happen to generate IDs compatible with OpenCode’s internal monotonic format.
That is a leaky internal invariant. Either:
- custom IDs must be fully supported, or
- the API should reject them / document strict ordering requirements
But the better fix is to stop using ID comparison for ordering at all.
Plugins
No response
OpenCode version
1.4.6.
Steps to reproduce
- Start a prompt with a caller-supplied messageID
- Assistant finishes normally with finish = "stop"
- OpenCode should stop
- Instead, lastUser.id < lastAssistant.id is false because the user ID is custom and the assistant ID is OpenCode-generated
- The loop runs one extra iteration
- OpenCode sends another completion request with a transcript whose last message is assistant.
Providers that validate chat turn structure reject that request. In my case, Anthropic via OpenRouter failed with an error equivalent to: last message must be a user message.
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response
Description
OpenCode accepts caller-supplied messageIDs on prompt input, but SessionPrompt.run later assumes message IDs are OpenCode-generated ascending IDs and uses lexical ID comparison to decide transcript ordering.
That is a bug in two ways:
Relevant code
OpenCode accepts custom user message IDs here:
But later SessionPrompt.run uses ID ordering to decide whether to exit the loop:
There is a second similar assumption here:
OpenCode’s own generated IDs are monotonic because MessageID.ascending() uses a timestamp-plus-counter scheme:
So this works only as long as all message IDs come from OpenCode’s own generator.
Why this is a real bug
If a caller passes a valid custom messageID like msg_ or any other globally unique ID that does not sort lexically the same way as OpenCode IDs, the prompt loop can mis-order the transcript logically.
I hit this on a question tool resume flow:
Providers that validate chat turn structure reject that request. In my case, Anthropic via OpenRouter failed with an error equivalent to: last message must be a user message.
So this is not just theoretical: a valid custom ID can cause an extra model call after a completed assistant turn.
Expected behavior
Actual behavior
Suggested fix
Do not use message ID ordering as a proxy for transcript order.
Better options:
At minimum, the loop exit and reminder logic should stop comparing opaque IDs:
Those should be replaced by ordering based on actual message time or array position in the current transcript.
Why this matters for API design
Right now the API surface implies that passing messageID is supported, but the implementation only works reliably if callers happen to generate IDs compatible with OpenCode’s internal monotonic format.
That is a leaky internal invariant. Either:
But the better fix is to stop using ID comparison for ordering at all.
Plugins
No response
OpenCode version
1.4.6.
Steps to reproduce
Providers that validate chat turn structure reject that request. In my case, Anthropic via OpenRouter failed with an error equivalent to: last message must be a user message.
Screenshot and/or share link
No response
Operating System
No response
Terminal
No response