Repository navigation
bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 3, 2026 认领:PM 循环第 3 轮
会话:session_015Br2xsJsczFsTR9bvbh2Ny
分支:claude/issue-4771-flow-validation-kernel-ready
Worktree:objectstack-issue-4771分诊:落地 objectstack(后端);PM 裁定取修法 1(校验推迟到
kernel:ready,即所有插件start()完成之后)。理由按两条轴:
- 长远合理性 —— ADR-0018 明确允许插件在运行时通过
registerNodeExecutor(type)扩展节点词汇表。那么在扩展完成前就拍板的校验器,是在校验一个还没成型的世界;修法 2(降级为 debug + 事后补一次权威校验)保留了那个错误的早期判断,只是把它藏起来。把校验挪到词汇表真正封闭的那一刻,是唯一让这个检查「说的话为真」的做法。 - 防 AI 写错 —— 这条告警当前信噪比为 0:真的没装 approvals 插件的部署,和装了的部署,拿到一模一样的 8 条。信号无法区分两种情况时,它就不再是护栏,只是训练人忽略 warn。修好之后它才第一次具备拦截价值。
一并处理(issue 末尾提到,属于同一缺陷类,不要漏):
plugin-approvals/src/approvals-plugin.ts:283那个catch把「没有 automation 引擎 ⇒ approval 节点没注册」记成info,而 dev 默认日志级别是warn—— 真降级发生时反而看不见。按 #4632 已立的规则(静默降级必须响亮),这里至少是warn。维护者可否决本裁定;不阻塞派发。
Generated by Claude Code
- 长远合理性 —— ADR-0018 明确允许插件在运行时通过
PM 复核:ACCEPT(待 CI 全绿) —— PR #4791。
dev 改进了我的裁定,并且给出了理由
我派发时写的是「推迟到
kernel:ready」。dev 改用了kernel:bootstrapped,并给出三条理由:(1) 本插件自己的kernel:readyhandler 还会再注册一批 flow(syncFlowsFromProtocol);(2) 排在本插件之后启动的插件仍可能在它自己的kernel:ready里贡献执行器;(3)packages/core/src/kernel.ts对这个 hook 的定义原文就是「消费 later-starting plugin 的kernel:ready产物的 reconcile 工作属于这里,不属于kernel:ready(那里 handler 顺序会 race)」。这三条都对,我的裁定是错的 —— 我说的
kernel:ready会重新引入一个更窄的同类竞态。dev 没有照着执行,而是查清语义后选了严格更晚的锚点并解释清楚,这正是我希望 dev 做的事。裁定的意图(校验只在词汇表封闭那一刻才成立)被完整保住了。复核要点
- 这是时序修复,不是把告警静音 —— 这是我最担心的失败模式,dev 正面处理了:封闭之后
registerFlow恢复即时告警,因为 Studio 发布 / dev reload 进正在运行的服务器时,词汇表确实完整,那句断言此时为真。有专门用例钉住。 - 验收标准 2 被直译成了测试:
the two deployments are DISTINGUISHABLE直接断言「装了插件的 boot」与「没装的 boot」输出不同(0 !== 1)。修复前这两个 boot 打印的东西完全一样 —— 那才是这个 bug 的实质。把「可区分」本身写成断言,比任何计数断言都准确。 - 测试证明了复现条件,而不是假设它:每个用例都从 boot 日志断言
Flow registered: …出现在Node executor registered: approval之前,即误报窗口确实发生了。没有这一步,测试可能在一个根本不触发 bug 的顺序下"通过"。 - 反向验证:把
nodeTypeVocabularySealed初值改回true(等价于修复前),4 个用例里 3 个失败,其中「没装插件」那条从 1 条告警变成 2 条(注册时 + 封闭时各报一次)。 - 「跑不起来的 flow 谈不上运行时失败」 —— 禁用的 flow(
obsolete/invalid)不再上报,口径与既有getTriggerBindingAudit一致。这是我没要求、但完全正确的收敛:它消灭的正是本 issue 要消灭的那类假断言。 - 发现不只是日志,还是状态:
getUnknownNodeTypeAudit()让 host(CLI 启动摘要 / 健康检查)直接读引擎,而不是去 grep 日志。沿用getTriggerBindingAudit()的既有套路。 - approvals 那一半超出了我的要求:除了把
info改成warn([convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632 规则),还(a)把catch收窄到「服务查找」这一步 —— 这样registerApprovalNode内部真出错时会以自己的身份抛出,不再被贴上 "no automation engine" 的错误标签;(b)补上了「automation服务存在但不接受节点执行器」这条从前一条日志都不打的分支。第二条是 dev 自己发现的,原 issue 没提。 - 范围守住:9 个文件全在
service-automation+plugin-approvals,未碰本轮并行批的objectql/service-storage/plugin-auth。changeset 的minor(新增公共 API)定级正确。
一个我不拦、但已另立单的连带影响
sealNodeTypeVocabulary()需要 host 显式调用。走AutomationServicePlugin的部署(绝大多数)由插件在kernel:bootstrapped自动调用,没问题;但直接new AutomationEngine()的嵌入式 host,如果不调用它,就完全拿不到节点类型校验了 —— 而修复前它们是有的(且对「先注册执行器、后注册 flow」的 host 而言,那个校验当时是准确的)。也就是说这部分 host 用「不可靠的告警」换成了「没有告警」。dev 已经把这一点写进 changeset 和 PR,处理是诚实的;但「文档写了」不等于「强制了」,这正是本仓一贯不接受的形态。已另立 #4792 记录,给了一个廉价缓解方向(首次
execute()时若词汇表仍未封闭则告警一次),不阻塞本 PR —— 本 PR 让主路径从「永远说谎」变成「永远说真话」,这个净收益远大于该边缘 population 的回退。CI 仍在跑,全绿后标 ready 并送合并队列。
Generated by Claude Code
- 这是时序修复,不是把告警静音 —— 这是我最担心的失败模式,dev 正面处理了:封闭之后
- added a commit that references this issue
on Aug 3, 2026 - added a commit that references this issue
on Aug 3, 2026 - added a commit that references this issue
on Aug 3, 2026 - added 3 commits that reference this issue
on Aug 4, 2026 - added a commit that references this issue
on Oct 7, 2026
TL;DR
pnpm dev(showcase)每次启动报 8 条「这些 flow 运行时会失败」,全是误报。校验在ApprovalsServicePlugin注册approval节点执行器之前 0.8 秒就跑完了。告警措辞是断言式的,会直接把人送去查一个不存在的 bug(本 issue 的作者就被送去查了一轮)。
现场
×8:
showcase_expense_signoff、showcase_committee_quorum、showcase_budget_approval、showcase_invoice_signoff、showcase_one_task_signoff、showcase_closure_signoff、showcase_dynamic_approval、showcase_approver_bindings。证据:是时序,不是缺失
用
--log-level info跑一次:showcase_expense_signoff04:28:05.302ApprovalsServicePlugin: service registered04:28:06.105晚 0.8 秒。
而且
ApprovalsServicePlugin的兜底日志 ——approvals-plugin.ts:283的—— 在整份 info 级日志里 一次都没出现(
grep -c= 0)。说明ctx.getService('automation')拿到了引擎,registerApprovalNode(...)确实跑了,approval最终是注册上的,flow 运行时是好的。相关代码:
packages/plugins/plugin-approvals/src/approvals-plugin.ts:272-285、packages/plugins/plugin-approvals/src/approval-node.ts:92。为什么值得修,而不是当噪音
registerNodeExecutor(type)扩展词汇表 —— 那么一个在扩展完成前拍板的校验器,就是在校验一个还没成型的世界。approval的部署(没装 approvals 插件)会得到一模一样的 8 条告警。现在这条信号的信噪比是 0,没人能用它区分两种情况。修法(择一)
start()完成之后(kernel:ready)—— 那时词汇表才是完整的。这是最贴合 ADR-0018 的做法。kernel:ready再跑一次权威校验,只有那一次才 warn。倾向 1。另外顺带看一下
approvals-plugin.ts:283那个catch—— 它把"没有 automation 引擎导致 approval 节点没注册"记成info,而 dev 默认日志级别是warn,也就是真的降级发生时你反而看不见。这个 catch 至少该是warn。复现
rm -rf examples/app-showcase/.objectstack && pnpm dev想看时序:
objectstack dev --seed-admin --log-level info,对比WARN Flow '...'与ApprovalsServicePlugin: service registered的时间戳。