Skip to content

fix(session): mint message ids and timestamps from one clock read - #46103

Open
iceteaSA wants to merge 1 commit into
anomalyco:devfrom
iceteaSA:message-clock-read
Open

fix(session): mint message ids and timestamps from one clock read#46103
iceteaSA wants to merge 1 commit into
anomalyco:devfrom
iceteaSA:message-clock-read

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Aug 29, 2026

Copy link
Copy Markdown

Issue for this PR

Closes #46102

Type of change

  • Bug fix

What does this PR do?

A message's id and its time.created come from two different clocks. Identifier.ascending() reads Date.now() inside Identifier.create; each producer then reads Date.now() again for time.created. If the assembly path yields between the two reads, a message minted later can capture an earlier time.created than one minted before it.

Messages sort (time_created DESC, id DESC) (message-v2.ts:439). When time.created disagrees with mint order, history sorts wrong — and since time.created is the primary sort key, it sorts by the corrupted value.

Read the clock once, mint the id from that same timestamp:

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

Identifier.create already accepts an explicit timestamp, and its per-millisecond counter still applies on that path, so same-millisecond ids stay unique and strictly ascending. Applied at the message producers in prompt.ts, compaction.ts, plan.ts, and the debug/github CLI callers. The session-fork path is left alone — it clones an existing message's time on purpose — as are the ephemeral, never-persisted messages that never reach the ordering query.

Two neighbouring issues this is not:

Deriving time.created from the id (via Identifier.timestamp) is the obvious alternative and it's wrong: the id's embedded timestamp can predate a legacy row's separately-read Date.now(), sorting a new message ahead of existing data.

How did you verify your code works?

Red-first, through the real producer — the test asserts a minted message's embedded id timestamp equals its stored time.created, observed through SessionPrompt.prompt, not a hand-built fixture. Reverse-applying only the src/ hunks (test kept present) shows it fail against unmodified dev:

production reverted, test present:
  (fail) persists message IDs with their creation timestamp — Expected 1016, Received 1015
  58 pass, 1 fail
restored: 59 pass, 1 skip, 0 fail

500 ids minted at one explicit timestamp were all unique and strictly ascending. Full suite packages/opencode: 3396 pass / 0 fail. bun typecheck clean in opencode and core.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Freshly persisted messages now read Date.now() once, mint the ascending message ID from that value via Identifier.create("msg", "ascending", created), and persist the same value as time.created. The captured inversion was msg_04b012001003... with time_created 1787964760065 appearing after msg_04b012001001... with time_created 1787964760066 despite the later ID. Deriving time.created from the ID was tried and rejected because the ID's embedded timestamp can predate a legacy row's separately-read Date.now(), mis-ordering new messages against existing data.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant