Repository navigation
[verify] bootStack({ databaseFile }) 的数据活不过进程内重启 —— 表在、行没了,挡住冷启动类 e2e #4518
Description
在做 #4470(让验证 harness 走真实持久化路径)时发现的。它挡住了 #4470「最小形态」的第三条,所以单独记录 —— packages/qa/dogfood/test/flow-durable-suspend.dogfood.test.ts 的文件头、以及 @objectstack/verify 的 changeset 都引用了这条。
问题
#4470 给 bootStack 加了 databaseFile,本意是让两次顺序启动共享同一个 SQLite 文件,从而构成一次真正的冷启动(ADR-0019 的挂起 run 必须活过的那种重启)。实际行为是:第二次 bootStack 打开同一个文件,表结构在,行没了。
关键点是这与挂起 run 无关 —— 普通业务记录同样不保留。所以归因在 harness/驱动这一层,不在挂起存储。
复现
const dir = mkdtempSync(join(tmpdir(), 'probe-'));
const dbFile = join(dir, 'v.sqlite');
const a = await bootStack(someStack, { automation: true, databaseFile: dbFile });
const ta = await a.signIn();
await a.apiAs(ta, 'POST', '/data/suspend_note', { name: 'persisted-me', status: 'new' });
await a.stop(); // kernel shutdown
const b = await bootStack(someStack, { automation: true, databaseFile: dbFile });
const tb = await b.signIn();
await b.apiAs(tb, 'GET', '/data/suspend_note?limit=10');实测输出:
PROBE hot row status 200 ← 第一个进程里读得到
PROBE file exists true 1073152 ← 文件确实在,1MB
PROBE cold automation_run {"records":[],"total":0}
PROBE cold suspend_note {"records":[],"total":0} ← 普通记录也没了
两次运行文件大小完全一致(1073152),像是在某个早期时点(schema sync 之后)写过一次就再没更新。表能查到 200(不是 no such table),说明第二次启动确实读到了那个文件 —— 只是里面没有后来写入的行。
线索
packages/plugins/driver-sqlite-wasm/src/sqlite-wasm-driver.ts——persist默认'on-disconnect':只在驱动 disconnect 时把 sql.js 的内存镜像刷回磁盘(外加一个process.once('beforeExit')的兜底 flush)。packages/runtime/src/default-datasource-plugin.ts——destroy()会connection.disconnect('default', { asDefault: true }),看起来应该触发那次 flush。packages/verify/src/harness.ts——stop()关掉 http server 后调用kernel.shutdown();日志里确实出现Graceful shutdown started/✅ Graceful shutdown complete。
所以从代码看这条链路本该 flush,但结果表明没有(或者 flush 发生得太早)。没有继续深挖,因为这已经在 #4470 的范围之外了。
注意 beforeExit 兜底对进程内重启是无效的:进程根本没退出。objectstack dev 这类「跑到被杀」的用法因此不受影响,这也可能是它一直没被发现的原因。
影响
- 挡住 [verify] 端到端验证从构造上绕开了挂起 run 的持久化路径(harness 写死 suspendedRunStore: 'memory') #4470 的第三条最小形态 —— 「冷启动一个新 kernel,
resume能继续走对分支」目前无法在 dogfood 层断言。已在测试文件头标注为 KNOWN GAP,没有用放松断言的方式假装通过。 - 任何想验证「跨进程状态存活」的 e2e 都会撞上同一堵墙 —— 而这正是持久化类缺陷([automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420 那一类)最需要被覆盖的维度。
期望
bootStack({ databaseFile }) 之后的 stop() 应当保证数据已落盘,使下一次 bootStack 是一次真实的冷启动。可能的方向(未验证,留给认领者判断):
- 查清
stop()→kernel.shutdown()→ 插件destroy()→ 驱动disconnect()这条链在 harness 里是否真的走全了; - 或让 harness 在
stop()里显式调用驱动已有的flush(); - 或在
databaseFile模式下改用更即时的persist策略(如debounced:*)。
修好之后,flow-durable-suspend.dogfood.test.ts 最自然的下一个用例就是它最初被写出来的那个:挂起 → 关停 → 冷启动 → resume 走对分支。
参考
- [verify] 端到端验证从构造上绕开了挂起 run 的持久化路径(harness 写死 suspendedRunStore: 'memory') #4470 / [automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420 / ADR-0019
packages/verify/src/harness.ts(databaseFile、stop())packages/plugins/driver-sqlite-wasm/src/{sqlite-wasm-driver,wasm-connection}.tspackages/runtime/src/default-datasource-plugin.tspackages/qa/dogfood/test/flow-durable-suspend.dogfood.test.ts(KNOWN GAP 注释)
Activity
docs: point the "filed separately" references at #4518
os-zhuang commentedon Aug 2, 2026
认领:PM 循环第 1 轮(/pm-dispatch 会话)
分支:
claude/issue-4518-bootstack-durable-flushWorktree:
objectstack-issue-4518开发 agent 即将开工;如另有会话已在做本 issue,请以更早的认领评论为准,本认领让行。
Generated by Claude Code
os-zhuang commentedon Aug 2, 2026
PM 验收:ACCEPT → PR #4564(等 CI 全绿后加入 merge queue)。
根因不在 issue 预判的三个方向,而在
driver-sqlite-wasm的 Knex 方言:_query按「是否返回行」选执行分支、只在无行分支标脏,而 ObjectQL 的写入形状正是INSERT … RETURNING *——几乎全部业务写入都走了不标脏的分支,on-write与on-disconnect两种策略共用同一个 dirty 标志,一起丢写。修复为结构性收敛:「语句是否改库」只有一个判定点(statementMutatesDatabase,同时看 method 与 SQL 文本),执行与持久化两段分离;markDirty()去参数化,消除双判定点。stop()返回 ⇒ 落盘,自此是结构保证。dogfood 的 KNOWN GAP 冷启动套件解封:挂起 → 关停 → 冷启动 → resume 走对分支 → 第三次启动仍可读。回滚验证 10 条测试变红。Generated by Claude Code
docs(automation): 屏幕流 resume 契约与审批越权标记的文档 + 两份 changeset(objectstack-…
fix(driver-sqlite-wasm): persist RETURNING writes — unblock cold-boot…