Skip to content

Identifier time field wraps on 2026-08-14T11:19:55Z — all sessions with history enter infinite response loops #39624

Description

@svettypoo

Repo: sst/opencode
Version: 1.18.9 (algorithm unchanged on dev branch packages/opencode/src/id/id.ts as of 2026-07-30)

Summary

Identifier encodes Date.now() * 0x1000 + counter into a fixed 6-byte (12 hex char) field, reading bits 40..0. Since ms * 4096 currently needs ~53 bits, the top ~5 bits of the clock are silently truncated. The encoded time field is therefore only 36 bits of milliseconds, which wraps every 2^36 ms ≈ 2.178 years.

The current era began 2024-06-10T02:35:18.400Z. The next wrap is 2026-08-14T11:19:55.136Z UTC. At that instant, every running server's next generated ID drops from msg_fff… to msg_000000000001… — no restart required. There is no monotonic floor: the lastTimestamp/counter state only disambiguates IDs within the same millisecond and does not prevent new IDs from sorting below all previously issued ones.

Verification

Empirical, from a production sqlite DB — the first 9 hex chars of message IDs equal Date.now() mod 2^36 at creation:

message ID time.created (ms) low 36 bits (hex)
msg_dd7a9f577001… 1777439732992 dd7a9f577
msg_fb0a52512001… 1785375040786 fb0a52512

Wrap math: floor(1785375040786 / 2^36) = 25 → era start 25 · 2^36 = 1,717,986,918,400 (2024-06-10T02:35:18.400Z) → wrap at 26 · 2^36 = 1,786,706,395,136 = 2026-08-14T11:19:55.136Z.

User-visible impact at the wrap

  1. Every session with pre-wrap history enters an infinite duplicate-response loop. The turn-loop exit in SessionPrompt.run requires latestUser.id < latestAssistant.id (lexical). After the wrap, the pre-wrap user message is permanently the max-ID "latest user", the condition is permanently false, and the loop re-invokes the provider after every finish: "stop" — the model restates its final answer every ~15–25s until manually aborted. (We hit this exact failure mode early, in production, via a plugin that supplied random high-sorting messageIDs to promptAsync — chats repeated their answers 3–9 times per turn until aborted. The wrap generalizes it to every session with history, with token cost implications.)
  2. Transcripts scramble at the seam. Messages/parts are loaded ORDER BY id; post-wrap messages of an ongoing conversation sort before pre-wrap ones, so the context assembled for the model (and the UI transcript) interleaves incorrectly.
  3. New sessions list as oldest. Session IDs use the descending variant (~value), which jumps upward at the wrap.
  4. Identifier.timestamp(id) returns encoded / 4096 = ms mod 2^36, so age computations invert across the seam.

Suggested fix directions

  • Add a persisted monotonic floor: never issue an ID ≤ the last issued (initialize from max existing ID at boot), or
  • Widen the time field / re-base on a fixed epoch with explicit wrap handling, and
  • Make ordering-sensitive comparisons (latestUser.id < latestAssistant.id, ORDER BY id) robust to ID-scheme evolution (e.g., compare by stored creation time with ID as tiebreaker).

A migration story for existing DBs (which contain ~2 years of era-25 IDs) will be needed for any scheme change — the loop-exit and ORDER BY comparisons mix old and new IDs within the same session.

Happy to provide the DB evidence or test cases. Found while root-causing the duplicate-response loop described above; timeline is unfortunately fixed by the clock — ~15 days from this report.

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