anomalyco / anomalyco/opencode

Message time.created can disagree with the message id's ordering because they come from two separate clock reads

Open
#46,102 0 comments 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Aug 29, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Description

A message's id and its time.created are read from two independent clocks. Identifier.ascending() reads Date.now() inside Identifier.create (packages/opencode/src/id/id.ts:52), and each call site separately reads Date.now() again for time.created. Between those two reads a message-creation path can yield to the event loop, so a message minted later (higher id counter) can capture an earlier time.created than a message minted before it.

Message rows are ordered (time_created DESC, id DESC) (packages/opencode/src/session/message-v2.ts:439, and the pagination cursor predicate at :96). So when time.created disagrees with mint order, history sorts wrong — and because the ordering key is time.created first, ordering faithfully sorts by the corrupted value.

This is distinct from the id-ordering issues already handled:

  • #38791 (loop can't exit on non-time-sortable ids) was resolved by ordering on time.created instead of the id string. That makes ordering robust to id/time disagreement, but here the time.created value itself is wrong, so preferring it does not help.
  • #39624 / #43303 (the 36-bit timestamp field wrapping every ~2.18 years) is a different mechanism — a full id rollover, not a sub-millisecond two-clock race.
Root cause

createUserMessage and the other message producers do, in effect:

const id = MessageID.ascending()   // Date.now() read #1, inside Identifier.create
// ... an await can interleave here ...
const created = Date.now()          // read #2
info: { id, time: { created } }

Date.now() is non-decreasing, so read #2 is always ≥ read #1 for a single message — but across two concurrently-assembling messages the two reads can interleave, producing:

message A: id counter 1, time.created = T+1
message B: id counter 2, time.created = T

id(A) < id(B) but time.created(A) > time.created(B) — the ordering key and the id now disagree, and both orderings are individually plausible.

Evidence

Captured from a naturally-occurring full-suite failure of MessageV2.filterCompacted > retains history for an incomplete compaction:

msg_04b012001003…  time_created 1787964760065
msg_04b012001001…  time_created 1787964760066

The id ending …001 (minted first, lower counter) carries the later time_created 066; the id ending …003 (minted later) carries the earlier 065. The two disagree by 1 ms.

The failure is intermittent — roughly 1 in 3 full-suite runs on an otherwise idle machine, and never in isolation, because it needs enough concurrent message creation to hit the interleave.

Impact

Mis-ordered message history at the seam between two rapidly-created messages: transcript order, the assembled model context, and pagination can all place a later message before an earlier one. Low-rate in normal single-session use; more likely under bursts of programmatic message creation (compaction, injected prompts, tool-driven message writes).

Suggested direction

Read the clock once per message and mint the id from that same timestamp, so id order and time.created cannot disagree:

const created = Date.now()
const id = MessageID.ascending(Identifier.create("msg", created))
info: { id, time: { created } }

Identifier.create already accepts an explicit timestamp (id.ts:51) and its per-millisecond counter still applies on that path, so same-millisecond ids stay unique and strictly ascending. An earlier approach — deriving time.created from the id via Identifier.timestamp(id) — is unsafe against existing rows: the id's embedded timestamp can predate a legacy row's separately-read Date.now(), sorting a new message before an older one.

Happy to open a PR for the one-clock approach; it touches the message-producing sites (prompt.ts, compaction.ts, plan.ts, and the debug/github CLI callers) plus a regression test that asserts, through the real producer, that a minted message's embedded timestamp matches its stored time.created.

OpenCode version

dev (packages/opencode/src/id/id.ts, packages/opencode/src/session/message-v2.ts as of this writing)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.