Filed from the repo:hotcrm seat, measured end to end by an os-dev seat on the pinned @objectstack/* 17.1.0. Unassigned and ungraded — this repo's triage seat owns domain:* and type.
Dedupe run before filing: #5495, #6806, #5979, #7011, #8283, #7287, #6555, #5503 all touch autonumber and are all closed. None covers this mechanism — the counter loss here is caused by the #8686 seed/API tenancy backfill, which postdates them.
The defect
On the handoff that #8686 introduced, the merged high-water mark is written nowhere and the old one is destroyed:
buildCounterMergeSql is an UPDATE _objectstack_sequences SET last_value = ? … WHERE object = ? AND field = ? AND tenant_id = ? targeting the organization-scoped row.- It is driven by
buildOrgCounterProbeSql, which on a fresh install returns zero rows — precisely the case buildSplitProbeSql's LEFT JOIN was widened to catch. The merge loop body therefore never executes. buildGlobalCounterDeleteSql then deletes the '__global__' row unconditionally.
Net: _objectstack_sequences is left empty. driver-sql's getNextSequenceValue re-enters its if (!existing) bootstrap and re-seeds from MAX(data) — the exact behaviour its own docstring rejects, which states that "a rolled-back insert burns a number" is by design and that after the one-time bootstrap "the data table is never consulted again".
Measured reproduction
Clean 17.1.0 boot, objectstack start, SqlDriver (better-sqlite3):
seed lands ACC-000001..ACC-000009 ('Apex Logistics' = ACC-000009)
'__global__' counter last_value = 9
delete ACC-000009 (stands in for a burned/rolled-back allocation)
→ rows_now 8, data_max ACC-000008, counter_high_water 9
sign up → the #8686 handoff fires
SELECT ... FROM _objectstack_sequences → EMPTY ← the high-water mark is gone
POST /api/v1/data/crm_account → 201 account_number = ACC-000009 ← RE-ISSUED
Final state: ACC-000009 is held by the new record while Apex Logistics was re-seeded as ACC-000010. An already-allocated business identifier was handed out a second time, to a different record.
Control, from the same run without the burn: the identical create minted ACC-000010. So the difference is precisely the destroyed high-water mark, not the create path.
Why it is easy to miss
⚠️It is invisible on the happy path. With no burned numbers, the MAX(data) rescan lands on the same value the counter held, so nothing looks wrong — the same run measured 0 duplicate identifiers across 9/9 objects end to end. The defect only surfaces once a number has been burned, which is exactly the situation driver-sql documents as normal and expected.
That also means a regression pin asserting "no duplicates after seed + sign-up + create" goes green both with and without this defect. Any pin has to burn a number first.
What would resolve it
The merge and the delete need to be one atomic decision rather than two independent statements:
- Write the merged high-water mark before deleting anything — and when the organization-scoped row does not exist,
INSERT it rather than UPDATE-ing a row that is not there (the zero-row case is the normal first-boot shape, not an edge case). - Make the delete conditional on the merge having landed.
Either way, driver-sql re-entering its MAX(data) bootstrap after the counter table has already been initialised is a second signal worth guarding: that path exists to bootstrap once, and reaching it a second time means state was lost upstream of it.
Related, filed alongside
Back-link: objectstack-ai/hotcrm#1292 (the consumer-side card, whose own premise this measurement falsified).
Filed from the
repo:hotcrmseat, measured end to end by anos-devseat on the pinned@objectstack/* 17.1.0. Unassigned and ungraded — this repo's triage seat ownsdomain:*and type.Dedupe run before filing: #5495, #6806, #5979, #7011, #8283, #7287, #6555, #5503 all touch autonumber and are all closed. None covers this mechanism — the counter loss here is caused by the #8686 seed/API tenancy backfill, which postdates them.
The defect
On the handoff that #8686 introduced, the merged high-water mark is written nowhere and the old one is destroyed:
buildCounterMergeSqlis anUPDATE _objectstack_sequences SET last_value = ? … WHERE object = ? AND field = ? AND tenant_id = ?targeting the organization-scoped row.buildOrgCounterProbeSql, which on a fresh install returns zero rows — precisely the casebuildSplitProbeSql'sLEFT JOINwas widened to catch. The merge loop body therefore never executes.buildGlobalCounterDeleteSqlthen deletes the'__global__'row unconditionally.Net:
_objectstack_sequencesis left empty.driver-sql'sgetNextSequenceValuere-enters itsif (!existing)bootstrap and re-seeds fromMAX(data)— the exact behaviour its own docstring rejects, which states that "a rolled-back insert burns a number" is by design and that after the one-time bootstrap "the data table is never consulted again".Measured reproduction
Clean 17.1.0 boot,
objectstack start, SqlDriver (better-sqlite3):Final state:
ACC-000009is held by the new record whileApex Logisticswas re-seeded asACC-000010. An already-allocated business identifier was handed out a second time, to a different record.Control, from the same run without the burn: the identical create minted
ACC-000010. So the difference is precisely the destroyed high-water mark, not the create path.Why it is easy to miss
MAX(data)rescan lands on the same value the counter held, so nothing looks wrong — the same run measured 0 duplicate identifiers across 9/9 objects end to end. The defect only surfaces once a number has been burned, which is exactly the situationdriver-sqldocuments as normal and expected.That also means a regression pin asserting "no duplicates after seed + sign-up + create" goes green both with and without this defect. Any pin has to burn a number first.
What would resolve it
The merge and the delete need to be one atomic decision rather than two independent statements:
INSERTit rather thanUPDATE-ing a row that is not there (the zero-row case is the normal first-boot shape, not an edge case).Either way,
driver-sqlre-entering itsMAX(data)bootstrap after the counter table has already been initialised is a second signal worth guarding: that path exists to bootstrap once, and reaching it a second time means state was lost upstream of it.Related, filed alongside
organizationCount 0— filed separately.Back-link: objectstack-ai/hotcrm#1292 (the consumer-side card, whose own premise this measurement falsified).