anomalyco / anomalyco/opencode
Message time.created can disagree with the message id's ordering because they come from two separate clock reads
@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.createdinstead of the id string. That makes ordering robust to id/time disagreement, but here thetime.createdvalue 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.