You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
每个 os migrate 子命令关停时,悬空引用巡检都会把 sys_metadata / sys_view_definition 报成 unreadableObjects(连接已关闭) #4747
cd examples/app-crm
OS_DATABASE_URL="file:/tmp/t.db" node packages/cli/bin/run.js migrate recorded-by --json
命令正常返回 JSON、退出码正确。但在返回之后、进程关停时,稳定打出:
ERROR Find operation failed {"object":"sys_metadata","error":{"message":"Unable to acquire a connection", …}}
WARN [integrity] dangling-reference audit could not list an object {"object":"sys_metadata","error":"Unable to acquire a connection"}
ERROR Find operation failed {"object":"sys_view_definition","error":{"message":"Unable to acquire a connection", …}}
WARN [integrity] dangling-reference audit could not list an object {"object":"sys_view_definition","error":"Unable to acquire a connection"}
WARN [integrity] stored references that resolve to nothing (#4551)
{"scanned":0,"dangling":0,"undetermined":0,
"unreadableObjects":["sys_metadata","sys_view_definition"],
"truncatedObjects":[],"references":[]}
The single most important property here. An existence probe that cannot run … must never be recorded as "the target does not exist" … Together they are what stops "0 dangling" from ever being read as "everything is fine".
3. Absence must be loud. … a verifier that silently degrades (reusing a stale build, skipping a check it could not run) is worse than no verifier, because it reports success.
ObjectQLPlugin 把关停逻辑写在 stop() 里,而内核的插件契约是 init/start/destroy,stop() 从来没有被任何人调用过 —— 这件事仓里甚至写明过(DefaultDatasourcePlugin.destroy 的注释原话:「stop() exists nowhere in the Plugin contract and is never called」)。
bootSchemaStack().shutdown() 调的是 (runtime as any).stop?.(),而 Runtime 没有 stop —— 一个强制转型加一个可选调用,把「没有关停」伪装成了「关停过了」。
第二条尤其值得记住:as any + ?. 的组合让一个从来不可能工作的调用安静地通过了所有检查。这与今晚反复出现的形状同族 —— 声明了做某件事的代码,实际什么都没做,而没有任何信号。
Activity
os-zhuang commentedon Aug 3, 2026
认领:PM 循环第 4 轮
会话:
session_015Br2xsJsczFsTR9bvbh2Ny分支:
claude/issue-4747-integrity-audit-shutdownWorktree:
objectstack-issue-4747分诊:落地 objectstack(
packages/objectql/src/integrity/+src/lifecycle/);采纳 issue 的 A + B,无需维护者裁定。这属于恢复不变量类 —— 巡检自己的文档已经把规则写死了:
而现在
unreadableObjects在每一次正常 CLI 调用里都非空。一个恒为真的告警不携带任何信息,它训练操作者忽略这个桶 —— 等真有对象因数据源故障读不出来时,那条信息会淹没在同样的两行里。规则已经在仓里,现状违反了它,修它不需要谁批准。取 A + B 的理由:A(巡检跑在 shutdown 之前 / 关停时跳过)修时序;B(关停中的连接失败不进
unreadableObjects)保证即使时序再次被打破,这个桶也不会被非发现污染。单独的 C(一次性命令不跑巡检)不取 —— 那会让巡检在 CLI 场景永久失明,与它的存在意义相悖,issue 作者的判断是对的。一并处理:那两行
ERROR Find operation failed出现在一条成功命令的输出里,对抓 ERROR 的 CI 流水线是直接误报源,修完应当消失。packages/plugins/plugin-auth与 9 处scripts/i18n-extract.config.ts。另外 PR #4794 正在合并队列中,同样改packages/objectql(但落点是src/validation/与engine.ts,与本条的src/integrity/+src/lifecycle/不同文件)。请从最新 main 建分支;若 #4794 在你工作期间落地,rebase 一次即可。若你发现必须改engine.ts或src/validation/**,停下来在报告里说明。顺带一提:本条与 #4776(启动期判断跑在提供方注册之前)是同一族的镜像面 —— 那边是"判断下得太早",这边是"判断下得太晚(依赖已经拆了)"。共同点是检查与它依赖的资源之间没有明确的生命周期契约。
Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
PM 复核:ACCEPT(待 changeset 修正 + CI 全绿) —— PR #4815。
根因比 issue 的猜测深一层,而且证据确凿
issue 猜的是「巡检在 shutdown 之后才跑到这两个对象」。dev 挖出来的事实是:shutdown 根本没发生过 —— 两处静默空转叠在一起:
ObjectQLPlugin把关停逻辑写在stop()里,而内核的插件契约是init/start/destroy,stop()从来没有被任何人调用过 —— 这件事仓里甚至写明过(DefaultDatasourcePlugin.destroy的注释原话:「stop()exists nowhere in the Plugin contract and is never called」)。bootSchemaStack().shutdown()调的是(runtime as any).stop?.(),而Runtime没有stop—— 一个强制转型加一个可选调用,把「没有关停」伪装成了「关停过了」。第二条尤其值得记住:
as any+?.的组合让一个从来不可能工作的调用安静地通过了所有检查。这与今晚反复出现的形状同族 —— 声明了做某件事的代码,实际什么都没做,而没有任何信号。A + B 都按裁定落地,B 的实现方式是对的
stop→destroy;shutdown()改走kernel.shutdown()—— 与os serve收 SIGTERM 同一条路径,一次性命令和服务器只有一条出口。signal),dev 明确拒绝了对Unable to acquire a connection做消息嗅探 —— 那会把一个错误文案变成事实上的第二份契约,换个 driver 就失灵(PD Add comprehensive test suite for Zod schema validation #12)。aborted是独立字段而不是unreadableObjects里的一条:「没人问」不是「数据源拒绝了」。不完整依然是响的(报告永远不会被读成 clean bill of health),但不占用只该装发现的桶。且aborted: false是显式的,消费者不用猜undefined是「跑完了」还是「旧报告形状」。测试把「修之前无法区分的两种情况」钉成了一对
开头那对是全部要义:同一段抛错代码 —— 没有 abort ⇒ 进桶、warn 照发;被 teardown race ⇒ 丢弃、
aborted: true、不发 warn。删掉 abort 处理第二条红,删掉进桶逻辑第一条红。另有:abort 在读之前 ⇒ 一次查询都不发(这正是ERROR Find operation failed消失的机制);中途 abort ⇒ 已证成的 dangling 保留;探针在关停下抛错不算undetermined(同一个范畴错误,隔壁桶)。集成测试在真实
bootSchemaStack上把两头都断言了,最后一条故意证明沉默不是空转:shutdown 后直接对引擎find会 reject,说明池确实关了,上面的安静是修复的结果而不是「本来就没东西可读」。验收第三条(巡检在 CLI 场景仍然运行)守住了 —— 没有「一次性命令不跑巡检」的开关,只有「引擎活着才读」的生命周期边界。被拒绝的选项 C 没有从后门溜进来。
一处要改:changeset 定级
@objectstack/objectql标了patch,但本 PR 给它新增了公共 API(AuditAbortSignal从包根导出、DanglingReferenceAuditOptions.signal、DanglingReferenceReport.aborted)。按 #4791 的先例(service-automation 因新增公共 API 标 minor),应为minor;@objectstack/cli保持patch。已通知 dev 修正,一行改动。超范围产出
pm:queue)—— 内核的 init/start 超时守卫定时器从不清除也不 unref,每个 CLI 进程干完活还要空转 ~120 秒。它正是本 issue 的使能条件:一个 3 秒就该退出的进程,被 8 根带 ref 的定时器钉到 120 秒,才活到 60 秒巡检定时器触发。带 probe 证据、修法定位到隔壁 30 行已有的正确写法(shutdown()自己的守卫连注释都写好了),质量很高。PluginContext暴露内核级 teardown 信号)已记入,归 [决策] 启动期「提供方尚未注册」与「根本没有提供方」在注册表里是同一个值 —— 一次 showcase 冷启暴露 3 个同形缺陷,是否收紧 kernel 注册表契约 #4776 裁定时一并考虑,本 PR 正确地没有自行推广。changeset 修正推上、CI 全绿后送合并队列。
Generated by Claude Code
fix(objectql,cli): 巡检随引擎一起停 —— `unreadableObjects` 不再被关停污染 (objectsta…
feat(objectql): audit readonly references into their own `provenance`…
fix(objectql): bound every lifecycle reap, not just the guarded ones (o…
fix(objectql): name the objects a finite row budget never reached (ob…
fix(objectql): archive 冷侧 keep prune 尊重 objectstack-ai#4747 的 teardow…
fix(objectql): a failed `find` reports at `warn`, not `error` — the r…
test(cli): the #4747 teardown pin holds on both compositions
fix(cli,runtime): one-shot CLI boots run no seed loader and arm no li…
feat(metadata-core,metadata-protocol,metadata,platform-objects,spec)!…