Skip to content

SessionPrompt.run compares message IDs instead of transcript order/time, which breaks custom messageIDs accepted by the API #23490

@UniIsland

Description

@UniIsland

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:

  1. The API accepts custom message IDs, so OpenCode should not assume all message IDs preserve its internal ordering semantics.
  2. 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:

  1. Start a prompt with a caller-supplied messageID
  2. Assistant finishes normally with finish = "stop"
  3. OpenCode should stop
  4. Instead, lastUser.id < lastAssistant.id is false because the user ID is custom and the assistant ID is OpenCode-generated
  5. The loop runs one extra iteration
  6. 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

  1. Start a prompt with a caller-supplied messageID
  2. Assistant finishes normally with finish = "stop"
  3. OpenCode should stop
  4. Instead, lastUser.id < lastAssistant.id is false because the user ID is custom and the assistant ID is OpenCode-generated
  5. The loop runs one extra iteration
  6. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions