ADR 004: Short time-ordered ids and fractional-index ordering
Date: 2026-09-10 (revised the same day after the API/MCP research). Status: accepted.
Decision#
- Pages, blocks, and property definitions get 14-character ids in lowercase Crockford base32:
45 bits of milliseconds since the epoch (time-ordered, sortable, good for a thousand years)
followed by 25 random bits, with a per-process monotonic bump inside the same millisecond.
Example:
1k7f3q9xz2hav4. - Block ids appear in markdown as an Obsidian-style
^idsuffix on the block's first line, both in the mirror files and in the API/MCP outline serialization. - Logseq UUIDs (
id::properties,((uuid))refs,{{embed ((uuid))}}) are mapped to new ids at import time and all references are rewritten. (As built, 2026-09-13: the mapping lives in memory for the one import; there is no import table. A second import into the same data dir is safe but not an update: every page whose name exists is skipped with apage-key-collisionwarning, so nothing is duplicated and nothing changed in Logseq since arrives — verified by importingdocs/wikitwice: 21 pages, 422 blocks after both runs.) - Device ids stay 8 hex characters (they are part of the HLC string); op ids are HLC strings.
- Sibling order is a fractional-index string (
fractional-indexing), stored per block. No linked lists (Logseq'sleftpointer caused corruption bugs; Logseq DB also moved to fractional indexes). - Page identity is the lowercased, NFC-normalized, whitespace-collapsed name. Namespaces are "/"-separated inside the name; ancestors are implied, never stored.
Why#
- A UUID costs about 20 tokens; a 14-character id costs about 5. A page read with ids on 200
blocks drops from roughly 4,000 tokens of ids to roughly 1,000. Clear, cheap ids for LLM
agents are a core requirement, and
^idis a syntax Obsidian already understands. - 25 random bits per millisecond make cross-device collisions negligible while keeping ids short; 18 bits (a 12-character id) was judged too tight for multi-device creation.
- Per-block independently mergeable attributes make last-writer-wins sync trivial; a linked list makes every reorder touch two neighbours and is unmergeable.
Consequences#
- The
corepackage'snewId()switches from UUIDv7 to this format;isUuidremains for recognizing Logseq ids during import. - Exported files are no longer byte-compatible with Logseq's
id::convention; they remain fully readable, and Obsidian block links work.
Confirmation#
User was indifferent between UUIDs and short ids on 2026-09-10; short ids kept for token efficiency and Obsidian compatibility.