Development record
abcd-mints-collision-proof-record-ids-across-parallel-agents
Summary
spc-33 delivers itd-114's native mint: record ids become timestamp-numeric
(<family>-<yymmddHHMMSS><random digits>), minted through one family-generic
seam with injectable clock and entropy, wired for the captures family first.
The policy layer is settled and not this spec's to reopen — the format, the
capture-stability guarantee, the captures-first rollout, the offline-under-forge
posture, and the armed uniqueness detectors are
adr-45's
rulings, seated as brief invariant 11. The forge-backed allocator is a later
adapter behind the same seam and is out of this spec's delivery.
This spec settles the two mechanics adr-45 left to it — the random-digit width and the same-instant tiebreak — and records the design the fidelity review audits against.
Ruling 1 — random-digit width: four digits
A native mint is <family>-<yymmddHHMMSS><rrrr>: a 12-digit UTC second stamp
followed by a 4-digit uniform random suffix, zero-padded to fixed width — 16
digits, e.g. iss-2608201142077341.
Reasoning:
- Entropy is the only cross-branch defence. Two minters on different branches share no filesystem, so no lock or O_EXCL create can arbitrate between them; only the suffix separates two mints landing in the same UTC second. Width is therefore sized for the cross-branch coincidence case alone — the same-ledger case is the tiebreak's (ruling 2), answered deterministically for free.
- Four digits put chance collision beyond the repo's horizon. The observed fleet (two scheduled hunts plus interactive and autonomous sessions) mints in bursts spread over minutes; two different minters starting a mint in the same wall-clock second is a rare event — order one pair-coincidence a day at the current scale. At width 4 a coincident second collides with probability 10⁻⁴: an expected chance collision roughly once in decades. Width 3 (10⁻³) brings that into single-digit years — material over the record's life; width 5 buys another 10× at the cost of a 17-digit id. itd-114 priced the scheme at 14–16 digits; the top of that range is taken because entropy carries the whole cross-branch case.
- Uniform and fixed-width. The suffix is drawn from
crypto/randby rejection sampling (uniform over 0000–9999, no modulo bias) and zero-padded, so every native id in a family has the same length: the timestamp prefix stays column-aligned to the eye, and lexicographic and numeric order agree for all same-era ids.
Ruling 2 — same-instant tiebreak: O_EXCL create, redraw on clash
Within one ledger, the mint keeps the existing reservation discipline
(internal/core/capture/alloc.go): candidate id → ledger-presence check across
all status directories → O_EXCL|O_NOFOLLOW placeholder create under the
ledger flock, with the existing retry budget of 8. What changes is the retry
step: on EEXIST the mint redraws a fresh id (same clock read policy, new
random suffix) rather than bumping the previous candidate by one.
Reasoning:
- The filesystem answers the same-ledger case deterministically. Two processes minting into one ledger already serialise on the allocator flock, and the O_EXCL create is the second belt beneath it. Entropy width does not need to be sized for a case the O_EXCL create resolves with certainty — that is precisely the division of labour between rulings 1 and 2.
- A bump is a miniature max+1. Incrementing a taken candidate derives the next id from the ledger's current occupancy — allocate-by-looking at one-second scale, the mechanism adr-45 forbids, and it biases suffixes toward adjacent runs. A redraw keeps every candidate independent and uniformly distributed; the probability of exhausting the budget of 8 is negligible at any plausible same-second burst size.
- The presence check covers what O_EXCL cannot. The O_EXCL create guards
open/alone; the existing all-directories presence check (issPresent) runs first under the same lock, so a redraw also fires on the (practically impossible, but cheap to close) clash with a resolved or wontfixed id.
The seam
internal/core/recordid gains the mint (it already owns the id grammar and
the read-side resolver):
Minter{Now func() time.Time, Entropy io.Reader}— both seams injectable; zero value usestime.Nowandcrypto/rand.Reader. Injection is what makes the race and same-instant acceptance tests deterministic.(Minter) Mint(family string) (string, error)— family-generic (iss/itd/spcall cost the same call); the family tag is validated against the id grammar before it reaches a filename.- The stamp is UTC,
yymmddHHMMSS: global time-order across minters in different timezones, which local stamps would break. Sub-second order within one second is not promised — ids tie-sort by their numeric value.
Capture wiring (reservePath): the max+1 scan (maxIssN), its across-refs
floor (recordid.MaxAcrossRefs at the capture callsite), the loud-degrade
MintWarning on CaptureResult, and the integer-ceiling guard all retire —
the mint no longer looks at any maximum, so there is nothing to scan, no
degraded scan to warn about, and no counter to overflow. ForceID, the
placeholder transaction, the orphan sweep, and the cancel path are unchanged.
Rollout (adr-45 ruling 3): captures adopt now. The intent (itd) and spec
(spc) allocators keep their legacy max+1-with-refs-union minting and their
MintWarning plumbing untouched until each family adopts the seam as
configuration — a later change that swaps the allocation call, not a second
implementation.
Consumers hold unchanged
The scheme is format-preserving ([0-9]+), so the release-cut derivation and
bijection, the canonical resolver, the record-lint handle and uniqueness rules,
capture list ordering, and the abcd <id> dispatch hold without
modification. This is not asserted but executed: the ripple gate test runs
each of those consumers against freshly minted ids and a mixed
(legacy + timestamp) ledger. Mixed-ledger ordering is correct time order:
every timestamp id exceeds every legacy sequential id numerically, so
newest-first listing puts the new era above the old.
Bounds and residuals (recorded, accepted)
- Ids are int64-scale. A 16-digit id exceeds int32; every consumer parses
into Go's platform
int, which is 64-bit on all supported targets (the CI matrix and release targets are exclusively 64-bit). - Year-2100 horizon.
yyrestarts at 00 in 2100, which would sort that era below this one. A four-digit year costs two digits on every id minted in the next 74 years; the trade is taken and recorded here. - Host clock quality is the host's. A grossly wrong clock mints a mis-ordered (never colliding — the suffix still separates) id; the mint adds no clock guard.
- Same-second cross-branch residue (~10⁻⁴ per coincident second) is exactly what adr-45 ruling 5 keeps the uniqueness detectors armed for.
Acceptance (itd-114 criteria → tests)
Every itd-114 acceptance criterion lands as a test with the injectable seams:
same-instant two-minter divergence and merged-union uniqueness; offline mint
(the native path has no network use by construction — the mint's only
dependencies are the injected clock and entropy); the executable ripple gate
over the repo's own consumers; legacy-id stability in a mixed ledger; and
family-generality of the seam (itd/spc mint via the same call —
adoption is configuration). The forge-allocator criterion belongs to the
later adapter, not this delivery.
Frontmatter
| id | spc-33 |
|---|---|
| type | spec |
| lifecycle | closed |
.abcd/development/specs/closed/spc-33-abcd-mints-collision-proof-record-ids.md
open on GitHub ↗ · commit history ↗
References1
- itd-114Two Agents Can Mint At The Same Instant And Never Collideimplements
mentions18
- adr-12`.work/issues.md` stays the live operational ledger; the structured per-issue store is deferred until the native spec layer schedules the migration (historical)
- adr-45Record ids are timestamp-numeric, capture-stable, and never allocated by looking at the maximum
- dcn-26082000000000072026-08-20 — the spc-33 mint mechanics are ruled and the native mint ships
- iss-239Dangling pre-rebuild spec ids in the draft corpus: itd-44 (spc-56), itd-51 (spc-33/37), itd-57 (spc-48/60/62), itd-61 (spc-75) cite specs from the numbering adr-21/adr-26 reset (live store is spc-1..spc-22). Worse, intents/README.md and itd-34 assert the itd-44 lineage 'landed as itd-44 (spc-56 thin adoption)' — no such spec or decision-verdict code exists, so the record claims a delivery the tree cannot back.
- iss-2608210737264758The optional forge-backed allocator (itd-114 ac-5) is deferred by recorded ruling — spc-33 names it a later adapter behind the same mint seam, adr-45 ruling 4 fixes its posture (allocates and never stores per itd-129's ledger-canonical line; offline falls back to native loudly) — but no open work item holds the seat, so nothing tracks building it. The fidelity verdict on itd-114 carries the criterion as NOT_MET/deferred-by-ruling; this capture is the deferred work's ledger seat, builds on itd-129's adapter seam when that lands
- iss-2608210737265933Two accepted mint residuals recorded so future hunts do not rediscover them as findings (spc-33 bounds-and-residuals section; surfaced by the pre-PR security review of the itd-114 delivery): (1) recordid's fileID rebuilds an id from the parsed integer, so a timestamp stamp with a leading zero — reachable only from a host clock in 2000-2009 or from 2100 onward — resolves to a different id than its filename claims (the year-2100 horizon the spec accepts); (2) the mint's rejection-sampling loop is uncapped, safe today because entropy is crypto/rand or an injected test reader that EOFs, but if Minter.Entropy ever becomes configurable from env/config/flags the loop needs an iteration cap. Neither has an attack path under the current wiring
- iss-2608211143185943README.md described abcd capture as minting records 'with the next free id' — the proper name of the max+1 allocation protocol that adr-45/spc-33/itd-114 abolished after live collisions (one costing a release re-cut). 'Next free id' is the codebase's own name for the retired mechanism (recordid.MaxAcrossRefs, lint.go). The README is the only public document still describing it, setting the wrong mental model (small sequential successor-comparable ids) and legitimising the hand-add-at-max+1 path iss-74 identified as a collision source. Written 2026-08-17, pre-rollout.
- iss-2608211144555085The ADR index (.abcd/development/decisions/adrs/README.md) ends at the adr-44 row while adr-45 is accepted (2026-08-20) and cited as settled law by six live records (the invariants brief, itd-114, spc-33, .abcd/work/issues/README.md, two open issues, DECISIONS.md). 37 ADR files vs 36 index rows — exactly one missing. The index is the declared hand-maintained roll-call (adrs/README.md) and is not among index_drift's four registered indexes, so it is structurally ungated. Third recurrence of this class in the same table (after resolved iss-296 and iss-374), which iss-38's decision (hand-kept indexes must be deleted or gated) predicts.
- iss-2608271804176759the specs store has no charter and its id namespace is doubly allocated: specs/ is the only durable-tier record family with no README at any level (directory_coverage flags specs/, open/ and closed/ independently), the live store is spc-2..spc-42 while the retired predecessor store minted its own spc-1..spc-83, and above-ceiling references now collide — spc-33 and spc-37 resolve to live specs on different subjects than the retired records the citations meant. Write the three READMEs first (grammar, open-to-closed lifecycle, intent back-link, the no-status-field rule per adr-3, and the mandatory predecessor-store qualifier), then re-scope iss-239 with the corrected live range and the two collisions.
- iss-2609260926323349itd-48 shipped the consistency pass (Role 2) and nothing tracks the five spc-33 deferrals it was the routed home for: spc-33:A1 (Role-2 mechanical half, cross-doc lint codes), spc-33:A2 (Role-2 pre-commit hook, blocking-vs-advisory policy), spc-33:A3 (Role-3 pre-commit scheduling, whose shape role now belongs to the kinds lint in itd-34), spc-33:A4 (chunked corpus review, dormant until a report shows bundle_overflow: true) and spc-33:G1 (a stable reconciliation key for consistency findings across re-runs). This record is their home until one is planned; A3 goes with itd-34, the rest are follow-ups of the shipped pass.
- iss-2609261536147903Planned intents and disciplines still cite specs of the retired predecessor store unqualified, and such an id at or below spc-70 resolves to a live spec on another subject (spc-12, for one, is live as the disembark grounding spec): itd-48, itd-50 and itd-53 name spc-12, spc-28, spc-29, spc-31, spc-33, spc-43 and spc-52 for the fidelity reviewer, lifecycle hook and cleanup work (which of these are predecessor ids is itself the reading this record owes); itd-24 names spc-66 as the phase-audit receipt; itd-1 and itd-37 name spc-12 as the manual reviewer. The specs charter (Two spc-N Namespaces) rules that every predecessor citation carries the '(predecessor store)' qualifier; iss-239 applied it to the draft corpus only. Each site needs a reading to tell a predecessor citation from a live one, which is why the sweep was not folded into iss-239.
- iss-2609290448510918The predecessor-store qualifier sweep reaches past the six intents iss-2609261536147903 named: outside drafts/ and those six, 192 citation sites across about forty intents name a spc-N at or below spc-70 that is not the citing intent's own spec, with no '(predecessor store)' on the line. Some are live cross-references (the cold-reading family's intents cite each other's live specs), and some are the predecessor store's (itd-4, itd-6, itd-29, itd-47 and itd-49 describe pre-rebuild work in its terms), so each site needs the same reading against the live spec it collides with; the specs charter's Two spc-N Namespaces section is the rule. Found while sweeping the pattern of iss-2609261536147903 in lane drainDrift3; a census script over intents/{planned,shipped,disciplines} reproduces the count.
- iss-2610110359273529After the drain lane for iss-2610092011131598 lands, the surprise, admission, disposition and reframe records redraw a minted id only when it clashes with a record this same process wrote, and still refuse a clash with any other existing record. The issue and reading-item allocators redraw on any clash (spc-33 ruling 2). Both of that lane's reviewers noted the split. The retained refusal guards against overwriting, which a redraw never does, so it protects nothing a redraw would endanger, and it can still fail a same-second mint against a peer's committed record.
- itd-33Multiple Agents Coordinate On Intent And Spec Work Without Duplicating Effort Or Producing Competing Artefacts
- itd-48`intent-fidelity-reviewer` Gains Its Cross-Doc And Kind-Classification Roles
- itd-51abcd Knows What "Safe Enough to Adopt" Means Before a New Harness Arrives
- itd-2609291923559186The consistency pass runs where records are written and names each finding the same way every time
- spc-2610021456200324The consistency pass at commit time: a stable finding key, cross-document lint codes, chunked review, and a pre-commit gate