Repository navigation
[17.0-rc][疑似平台] crm_case 自动编号计数器落后于既有 case_number:REST 建单连续 409,每次失败还烧掉一个号(实测重试 25 次才成功) #698
Description
Activity
- addedbugSomething isn't workingSomething isn't workingupstream:objectstackBlocked on / caused by the ObjectStack platform — tracked upstreamBlocked on / caused by the ObjectStack platform — tracked upstream
on Aug 5, 2026 - added a commit that references this issue
on Aug 5, 2026 Status update from the flow phase, same day: the 409 storm did not reproduce — all 4 case creates succeeded on attempt 1 (CASE-00041…44). That's consistent with the mechanism as filed: the earlier grind burned the counter forward past the seeded numbers (up to CASE-00039), so the collision zone is behind it now. The underlying defect — counter not syncing to
MAX(existing)per tenant on boot/seed — remains unfixed and will reproduce on any fresh database with seeded cases; the burn-through just makes it a one-time storm per DB rather than a permanent state. Repro guidance for whoever picks this up: test on a fresh DB, not one that has already been ground past the seeds.
Generated by Claude Code
- added a commit that references this issue
on Aug 5, 2026 Blocked-by: objectstack-ai/objectstack#5495
分诊(PM 循环第 1 轮,会话
session_01VHrPAGEgFDoHjphqYG4BMa):计数器不同步 + 失败烧号判定为平台缺陷(autonumber 供给),⛔ 不在 hotcrm 侧修,已立镜像单 objectstack#5495(查重:#3696/#1603/#3152 均已关闭,不覆盖「计数器不对齐既有行」)。含你补充的复现要点:需 fresh DB,烧穿后风暴一次性消失。CRM 半侧(seed 行
organization_id = NULL)暂不单独派发:seed 行的 org 戳是平台种子加载路径的行为,与 objectstack#5494(自动化插入不打 org)同类 —— hotcrm 侧没有可写的「生产者」。若平台裁定 seed 可/应带 org,再回本单开 CRM 补种子任务。本单打pm:blocked。
Generated by Claude Code
[17.0-rc2验收] A3(Flows)组补充新证据:fresh DB 上这个缺陷不是 409 风暴,而是零报错的静默重号 —— 且不止 crm_case,crm_quote / crm_contract 同样命中。
环境:
hotcrm@0899b4f + @objectstack 17.0.0-rc.2,全新 dev DB(--seed-admin,仅种子数据,无历史 API 行)。实测
REST 依次创建 case / quote / contract(均不带编号字段),一次 409 都没有,但铸出的号全部与种子行重复:
crm_case: CASE-00001 ×2 CASE-00002 ×2 CASE-00003 ×2 CASE-00004 ×2 (API 行 vs 种子行) crm_quote: QTE-0001 ×2 QTE-0002 ×2 QTE-0003 ×2 QTE-0004 ×2 crm_contract: CTR-0001 ×2 CTR-0002 ×2 CTR-0003 ×2机制与本单「Aggravator」一节完全吻合:种子行
organization_id = NULL,API 行带 org,唯一索引(organization_id, case_number)只在分区内咬合 —— fresh DB 没有任何同分区旧行,于是从 1 起步的计数器每个号都「可用」,重号直接落库。楼主环境里的 409 风暴,其实是「DB 里已有同 org API 行」这个额外条件下的显形;fresh DB 上同一根因表现为更糟的形态:业务标识符静默重复,无任何信号。下游已观察到的实害(flows 面)
contract_renewal的幂等闸门以subject: 'Renewal due: contract {contract_number}'+ account 做去重键 —— 同 account 下两份不同合同共号(实测 CTR-0002 就有种子/API 各一)时,第二份合同的续约提醒会被第一份的 open task 误吸收,静默漏提醒。- 各 sweep 的通知文案(
Contract expired: CTR-0001、SLA breached: case CASE-00004)在重号环境下指向不明。
给 objectstack#5495 修复者的复现要点:fresh seeded DB(不要用被 409 磨穿过的库)+ 任一带 autonumber 的对象直接 POST,看编号而不是看状态码 —— 状态码这条路在 fresh DB 上是全绿的。
Generated by Claude Code
huangyiirene commented
on Aug 11, 2026 CollaboratorMore actionsRe-measured on
@objectstack/*17.0.0-rc.6 during the rc.5 → rc.6 upgrade (#1059, PR #1066). The platform half — defect 1, "the counter does not sync to the existing rows" — is addressed upstream.The autonumber seed in
@objectstack/objectqlno longer reads an arbitrary window. Its own commentary describes the exact failure this card measured:The seed used to be one
findwithlimit: 5000, noorderByand no filter: the max of an ARBITRARY 5000-row window (on SQL, typically the oldest 5000 rows), which for any object past that size — or any scope whose rows sit outside the window because other scopes filled it — seeds BELOW the real MAX. The counter then issues numbers from an already-taken band, and on auniquerecord-number field that is a duplicate business identifier.That is why the counter started at 10 on a database already holding much higher case numbers. What replaces it (#6249):
- a complete keyset-paginated scan of the scope, not a windowed one (seek pagination, not
offset— "an offset walk cannot promise it visited every row"); - the numeric max computed by parsing every value, never delegated to
ORDER BYor an aggregatemax, because those rank stored values as text andCASE-99999sorts aboveCASE-100000; - prefix pushed down as
$startsWithso each scope reads its own rows.
Not established by this probe — two things worth re-running before closing:
- Defect 2, burn-on-failure. Whether a failed insert still advances the counter is a separate mechanism from seeding, and I did not measure it. If seeding is now correct the collisions should not arise in the first place, but the "every failure burns a number" behavior may still be there under genuine contention.
- The CRM-side aggravator — seed rows carrying
organization_id = NULLwhile API rows carry it, so(organization_id, case_number)doesn't bite across the partition andCASE-00003can exist twice. That is this repo's half and is untouched by the upgrade. Note crm_case 的 [organization_id, case_number] 手写复合唯一索引在无 organization 的装机上什么也不约束,且 Protocol 18 拒收该拼法(ADR-0120) #1023/fix(case): 案号唯一性改用字段级声明,在无 organization 的装机上也真正生效(#1023) #1029 has since landed case-number uniqueness enforcement for untenanted installs, which may already cover it — worth checking whether that closed this half.
Scope note: static read of the shipped seeding path, not a live warm-database
POST /api/v1/data/crm_caserun.
Generated by Claude Code
- a complete keyset-paginated scan of the scope, not a windowed one (seek pagination, not
GA reading (17.0.0) — one residual half is FIXED, the other REPRODUCES. Card stays open.
From #1153 (GA close-out B2), session
session_01XAK3brMLjd4ykF4QAhFnuo. Live probe, not a source read:objectstack dev -p 4002 --seed-admin --freshon@objectstack/*17.0.0 GA, hotcrmd4ddee0,SqlDriver(better-sqlite3), a fresh database (38 seeded cases,CASE-00001…CASE-00038) — the repro condition the 2026-08-05 follow-up asked for.Half (a) — "every failed create burns a number": NOT REPRODUCED
The mechanism is gone, and it is gone because a taken number no longer produces a failure at all. Planted a contiguous band of taken numbers above the counter, then issued one create:
counter at 11; planted a TAKEN band CASE-00012..CASE-00020 ONE create -> HTTP 201 case_number=CASE-00021 counter after that single create: 21 (was 11)One request, one 201, counter advanced straight past the whole band. The old shape would have been nine 409s with the counter climbing one per failure — both outcomes were printed by the same probe, so this is not a green that could not have gone red.
A create that genuinely fails also leaves the counter alone:
POST missing required fields -> HTTP 400 VALIDATION_FAILED counter before=22 after=22 => a rejected create did NOT consume a numberConsistent with objectstack#5495 → PR #6932 ("re-seed a stale autonumber counter instead of burning a number per failed create"), merged 2026-08-09.
Note on the third path: supplying
case_numberexplicitly in the POST body does not produce a unique violation either — the value is ignored and a fresh number is minted (POSTwithcase_number: "CASE-00001"returned 201 carryingCASE-00022). So there is no remaining route by which a create failure can burn a number.Half (b) — silent duplicate numbering on a fresh DB: REPRODUCES, unchanged
Four REST creates on the fresh seeded database, no
case_numbersupplied:seeded rows: 38, max=CASE-00038 seed organization_id values: [null] sequence last_value BEFORE any API create: 38 POST #1 -> 201 case_number=CASE-00001 POST #2 -> 201 case_number=CASE-00002 POST #3 -> 201 case_number=CASE-00003 POST #4 -> 201 case_number=CASE-00004 DUPLICATE MINTED against seed numbers: CASE-00001, CASE-00002, CASE-00003, CASE-00004 duplicate case_number values now in DB: [["CASE-00001",2],["CASE-00002",2],["CASE-00003",2],["CASE-00004",2]]Zero 409s, zero warnings, four duplicated business identifiers. This is the 2026-08-05 finding verbatim, on GA.
And the mechanism is now pinned exactly. The counter is a persisted table, and this install carries two rows for one object:
_objectstack_sequences where object='crm_case': tenant_id='__global__' last_value=38 <- what the SEED loader minted from tenant_id='org_mssymr19xzd645gv' last_value=6 <- what the REST API mints from crm_case rows by organization_id: organization_id=null 38 rows CASE-00001..CASE-00038 organization_id='org_mssymr19xzd645gv' 6 rows CASE-00001..CASE-00006and the unique index is partitioned by exactly that column:
CREATE UNIQUE INDEX `uniq_crm_case_organization_id_case_number` ON `crm_case` (COALESCE(`organization_id`, '__global__'), `case_number`)
So: the seed loader writes untenanted rows while the REST path stamps an organization, on a stack whose boot banner reads
Tenancy: single. One logical tenant is split across two partitions of the uniqueness index, each with its own counter. Each counter is correct within its own scope — which is why the rc.6 seeding fix (objectstack#6249's complete keyset scan) does not help here: the org-scoped counter scans its own partition, finds it empty on a fresh database, and correctly starts at 1. The defect is upstream of the counter, in who stampsorganization_id.Not specific to
crm_case— confirmed live on a second object in the same run:POST crm_knowledge_article -> 201 article_number=KA-0001 org=org_mssymr19xzd645gv article numbers by org: organization_id=null KA-0001,KA-0002,KA-0003,KA-0004 organization_id='org_mssymr19xzd645gv' KA-0001Disposition
Card stays open on half (b). Mirror and nomination posted (linked below).
Label note:
pm:blockeddropped. Its blocker objectstack#5495 is closed as completed via merged PR objectstack#6932 — a card should not wear a blocker that no longer exists. #1153 ties this drop to its non-reproducer branch and this card is a survivor, so I am extending it on that instruction's own stated rationale rather than on its letter; flagging it here so the PM can reverse it in one edit if that reading is wrong. No other label touched, and notarget:v17applied (nomination is a comment, per #8667).
Generated by Claude Code
Generated by Claude Code
Mirror and nomination for the surviving half (silent duplicate numbering), per #1153:
- Mirror: Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA) objectstack#8686 — "Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA)". Filed fresh rather than reopening objectstack#5495: that card is closed as completed via merged PR objectstack#6932, its burn-on-failure half is confirmed fixed in the same run, and the surviving mechanism (who stamps
organization_idon a seeded row) is upstream of the counter #5495 was about. Searched #5495 / #6249 / #5030 / #3696 / #6806 / #5979 first; all closed, none covering it. - Nomination: [Epic] v17 bug focus — Seat A: platform bugs (objectstack) objectstack#8667#issuecomment-5293907644 — a comment naming the card and the evidence. No
target:v17applied by this seat; the maintainer approves by labeling.
Generated by Claude Code
Generated by Claude Code
- Mirror: Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA) objectstack#8686 — "Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA)". Filed fresh rather than reopening objectstack#5495: that card is closed as completed via merged PR objectstack#6932, its burn-on-failure half is confirmed fixed in the same run, and the surviving mechanism (who stamps
Closed — moved upstream to objectstack-ai/objectstack#8686
Dispositioned under the transfer sweep #1156 (part 2). Maintainer ruling, 2026-08-14, quoted verbatim and untranslated:
hotcrm 席位的原则是用平台的能力做元数据应用的开发,平台的需求应该转给平台
and for the hotcrm side of a transferred card, the same ruling: 「关掉,只留指向镜像的指针」. Authority: #1156.
The mirror — verified before this close, not assumed
objectstack-ai/objectstack#8686 — "Seed loader writes untenanted rows while the REST path stamps an organization — one single-tenant install runs two autonumber scopes and mints duplicate business identifiers, silently (17.0.0 GA)". Confirmed open, labelled
bug, and carrying the surviving half of this card with its mechanism pinned: the two_objectstack_sequencesrows, theCOALESCE-partitioned unique index, and the second-object confirmation oncrm_knowledge_article.The reading this close rests on
The GA reading already on this card — comment of 2026-08-14, a live probe on a fresh seeded database, the repro condition the 2026-08-05 follow-up asked for.
- Half (a), "every failed create burns a number": NOT reproduced. A taken number no longer produces a failure at all — one create skipped a planted nine-number band in a single request, and a genuinely rejected create left the counter unmoved. Consistent with objectstack#5495 → merged PR objectstack#6932. This half needed no mirror and deliberately did not get one.
- Half (b), silent duplicate numbering: reproduces, unchanged. Four REST creates on a fresh 38-row seeded database minted
CASE-00001…CASE-00004on top of the seed rows — zero 409s, zero warnings, four duplicated business identifiers on auniquefield.
The mechanism is now upstream of the counter, which is why objectstack#6249's complete keyset scan does not help: the org-scoped counter scans its own partition, correctly finds it empty, and correctly starts at 1. The defect is who stamps
organization_idon a seeded row, on a stack whose boot banner readsTenancy: single.Why it closes here
Both write paths are the platform's — the seed loader and the REST ingress. This card's 2026-08-05 triage already recorded that the CRM half has no writable producer in this repo ("hotcrm 侧没有可写的「生产者」"), which is exactly the condition under which the ruling above sends a card upstream. If the platform rules that seed rows may or must carry an organization, a CRM seeding task can be opened fresh against that ruling.
Label note:
pm:blockedwas already dropped by the GA probe round and is not touched here; the remaining labels are left as they were.Nothing is lost by this close: #8686 is open, states which of the two decisions a fix has to make, and is readable without hotcrm context.
Generated by Claude Code
Generated by Claude Code
- addedpriority:p1High: required for production / M2High: required for production / M2and removed
on Sep 9, 2026
Found during the 17.0 GA acceptance sweep on
@objectstack/*17.0.0-rc.2 (currentmain), action-sweep phase.Symptom
POST /api/v1/data/crm_case {...}(nocase_numbersupplied — it is an autonumber) returns 409 UNIQUE_VIOLATION, repeatedly. The server log shows the attempted number climbing by one per failed insert:attempted
CASE-00010→ 409 →CASE-00011→ 409 → … → succeeded atCASE-00039after 25 attempts (devserver2.log from 07:23:51Z onward).Two defects compound here:
MAX(existing) + 1per tenant on boot/seed, or the insert should re-check on collision instead of failing the request.Aggravator (CRM-side seed inconsistency)
The DB contains a duplicate
CASE-00003pair: a seed row withorganization_id = NULLand an API-created row with the org set. Seed rows do not carryorganization_idwhile API rows do, so the unique index(organization_id, case_number)only bites within the org partition and the seed rows sit outside it. That's a HotCRM seed-data problem worth fixing regardless of the platform half: it makes "what numbers are taken" ambiguous per tenant.Impact
Any integration or UI path that creates cases on a warm database will hit bursts of 409s and, without client-side retry, hard failures. The acceptance run only got through by brute-force retrying.
Reproduction
POST /api/v1/data/crm_casewith a minimal valid body, nocase_number.Attribution
Counter-sync and burn-on-failure: platform (autonumber provisioning).
organization_id NULLon seed rows: CRM (seed data). Filed here with both halves stated; split upstream if the owner prefers.Refs #689