发现于 #4728(修 `database-loader.ts` 的 `ensureSchema()` 静默吞 DDL 失败)。**仅记录,未在该 PR 中修改** —— 与本单无关,按 Prime Directive #10 单开。 ## 现象 `packages/metadata/src/loaders/database-loader.ts` 的 `nextEventSeq()`: ```ts private async nextEventSeq(): Promise< number > { const where: Record< string, unknown > = this.organizationId ? { organization_id: this.organizationId } : {}; try { const rows = await this._find(this.historyTableName, { where }); let max = 0; for (const row of rows as Array< { event_seq?: number | null } >) { const v = typeof row.event_seq === 'number' ? row.event_seq : 0; if (v > max) max = v; } return max + 1; } catch { // Table not provisioned yet or driver error — start at 1. return 1; } } ``` ## 为什么值得单开 这是 #4728 刚修掉的**同一种形状**,只是在下一层,而且 #4632 的机械检查**看不到它**(`_find` 不在 `DURABILITY_CRITICAL_CALLEES` 词表里): - 注释同时点名了两种原因 —— 「表还没建」(良性:此时确实该从 1 开始)和「驱动错误」(**不良性**),然后用同一个 `return 1` 对待两者; - 历史表里已经有 N 行时,一次瞬时读失败(连接抖动、超时、权限)会让下一条历史记录拿到 `event_seq = 1`,与既有行**直接撞号**; - `event_seq` 是历史排序/回滚定位的依据,撞号之后版本顺序就是错的,而写入本身**成功**、日志**一行没有** —— 系统对外完全正常。 `nextEventSeq` 的 TSDoc 已经承认它是 legacy、非事务、并发会撞号(canonical 是 `SysMetadataRepository`),所以这里不是「必须马上修」,但「并发撞号」和「读失败静默重置到 1」是两回事:前者被记录过,后者没有。 ## 建议 按 #4632 的判定问句处理,任选其一: 1. 只在**确实是表不存在**时返回 1(按错误类型判别 —— #4728 落地的 `packages/metadata/src/utils/schema-sync-errors.ts` 是同类判别的现成范式),其余读失败以 `error` 上报后果(历史序号将从 1 重新发号,与既有行撞号,版本顺序不可信)并**抛出**,让调用方决定; 2. 或者确认该 legacy 路径已无生产调用方,直接删除,把历史写入统一收敛到 `SysMetadataRepository`。 顺带:如果要动这个词表,`_find` 这类「读出来决定后续写什么」的调用是否该进 `DURABILITY_CRITICAL_CALLEES`,值得一并想清楚 —— 它不是「字节没落盘」,而是「落盘的字节是错的」,可能需要词表之外的第二条规则。 ## 参考 - #4632(降级日志级别规则 + gate)、#4728(`ensureSchema()` 的同形缺陷,已修)、#4420(事故)、#4471(根因模式)
Activity
os-zhuang commentedon Aug 3, 2026
PM 分诊:
pm:queue+domain:engine(落点packages/metadata)。本会话不派发 —— 维护者已指示本波在飞任务收完即暂停,本单留给下一个 engine 域会话。分诊结论:这条比它的来源单 #4728 更值得注意,理由是失败方向不同。
#4728 是「声称持久化、实际没落盘」—— 坏在缺失。本条是「落盘了,但落的字节是错的」:历史表已有 N 行时一次瞬时读失败让下一条拿到
event_seq = 1,与既有行撞号,而event_seq正是历史排序与回滚定位的依据。写入成功、日志无痕、系统对外完全正常 —— 版本顺序从此不可信,却没有任何东西会说出来。正文里那句提醒尤其准,我原样转给接手者:
这是今晚 #4632 那条规则第一次被它自己的应用者指出边界:词表建在「持久化调用」上,而这一类的危害发生在读上,读的结果决定了写什么。#4777(把词表扩到启动期注册表判断)是同一个方向的另一个延伸,两条可以一起考虑 —— 但 #4777 在
domain:devx车道,本条在engine,跨域协调请走 #4604。修法方向按正文的两条:优先方案 1(按错误类型判别,
packages/metadata/src/utils/schema-sync-errors.ts是 #4728 刚落地的现成范式),或方案 2(确认无生产调用方后直接删除,收敛到SysMetadataRepository)。先确认这条 legacy 路径还有没有生产调用方 —— 若没有,方案 2 更彻底,别花力气修一段该删的代码。Generated by Claude Code
claude commentedon Aug 3, 2026
认领:PM 循环第 7 轮
会话:
session_015Br2xsJsczFsTR9bvbh2Ny分支:
claude/issue-4825-next-event-seq-loudWorktree:
objectstack-issue-4825域:
engine文件面:
packages/metadata/src/loaders/database-loader.ts并行批隔离:本轮同批的另三条落在互不相交的包 —— #4825⚠️ #4821(
packages/metadata、#4813packages/core、#4808packages/plugins/plugin-auth、#4820packages/services/service-analytics。mergeByDimensions分隔符)与 #4820 同文件,未同批派发,串到下一轮以避开撞车。Generated by Claude Code
fix(metadata): never invent an event_seq from a read that failed (obj…
fix(metadata-protocol): never invent event_seq/version from a failed …
fix(metadata): DatabaseLoader 的读故障不再被吞成「什么都没声明」 (objectstack-ai#5108) (…
docs(types): re-anchor the dead tracker citations in packages/types/s…