Skip to content

ID generator wraps after ~795 days, breaking chronological sorting of message IDs #42589

Description

@Dawnfz-Lenfeng

Summary

packages/opencode/src/id/id.ts (and the client-side replicas in OpenChamber) encodes the timestamp as Date.now() * 0x1000 + counter into a 6-byte (48-bit) hex prefix, which wraps around every 2^36 ms ≈ 795 days. The latest wrap occurred on 2026-08-14 11:19:55 UTC.

After the wrap, newly created IDs sort before all older IDs lexicographically, silently breaking the "ascending ID = chronological order" contract that the format implies.

Reproduction (real data)

Same session, before and after the boundary:

msg_fffc192a8001…   2026-08-14 10:11 UTC   (prefix near FFFF…)
msg_0005afbc2002…   2026-08-14 12:59 UTC   (prefix wrapped to 0000…)

String comparison orders the 12:59 messages before the 10:11 ones. In OpenChamber this rendered the newest messages at the top of any conversation that spans the boundary.

Impact

  • Any code that derives chronological order from ID string comparison (the documented intent of the ascending IDs, and what third-party clients such as OpenChamber relied on) breaks every ~795 days.
  • Identifier.timestamp(id) in id.ts returns a wrong (mod-2^36-ms) value for IDs created after a wrap.
  • opencode's own DB queries (message-v2.ts uses ORDER BY time_created, id) are unaffected, but the sortable-ID contract exposed to clients is broken.
  • Note this also affects the same-millisecond counter: lastTimestamp/counter are process-global, and client-generated IDs (OpenChamber's optimistic inserts replicate this format) collide in ordering with server IDs generated in the same millisecond.

Client-side workaround (already shipped)

OpenChamber fixed the display-side impact by ordering messages on time.created instead of raw IDs: commit 53830952 on openchamber/openchamber@main ("fix(chat): keep messages chronological across ID rollover"). That only masks the root cause — the ID generator itself still wraps.

Suggested fixes

  1. Encode 7 bytes (56 bits) instead of 6 — pushes the wrap to ~3.4M years while keeping a compact ID. 8 bytes (64 bits) is also fine.
  2. If the format cannot change, document the ~795-day wrap and recommend clients order by time.created (already present on every message) rather than by ID.
  3. Keep in mind old IDs with the 12-hex prefix will remain in existing stores — a format change alone doesn't reorder existing data, so clients should still prefer time-based ordering.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions