Skip to content

bug(service-automation): flow 节点类型校验跑在插件贡献的执行器注册之前 —— 每个 ADR-0019 approval flow 都被误报「will fail at execution time」 #4771

Description

@os-zhuang

TL;DR

pnpm dev(showcase)每次启动报 8 条「这些 flow 运行时会失败」,全是误报。校验在 ApprovalsServicePlugin 注册 approval 节点执行器之前 0.8 秒就跑完了。

告警措辞是断言式的,会直接把人送去查一个不存在的 bug(本 issue 的作者就被送去查了一轮)。

现场

WARN Flow 'showcase_expense_signoff' references node type(s) with no registered executor
     or descriptor: approval. They will fail at execution time unless a plugin registers
     them. Registered types: start, end, decision, assignment, loop, parallel, try_catch,
     get_record, create_record, update_record, delete_record, screen, script, http,
     connector_action, notify, wait, subflow, map

×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 跑一次:

事件 时刻
flow 校验告警 showcase_expense_signoff 04:28:05.302
ApprovalsServicePlugin: service registered 04:28:06.105

晚 0.8 秒。

而且 ApprovalsServicePlugin 的兜底日志 —— approvals-plugin.ts:283 的

} catch {
  ctx.logger.info('ApprovalsServicePlugin: no automation engine — approval node not registered');
}

—— 在整份 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。

为什么值得修,而不是当噪音

  1. 措辞是断言不是猜测。"They will fail at execution time" 对一个恰好会在 0.8 秒后被注册的类型来说是假的。ADR-0018 明确允许插件在运行时通过 registerNodeExecutor(type) 扩展词汇表 —— 那么一个在扩展完成前拍板的校验器,就是在校验一个还没成型的世界。
  2. 它掩盖真问题。真正没人提供 approval 的部署(没装 approvals 插件)会得到一模一样的 8 条告警。现在这条信号的信噪比是 0,没人能用它区分两种情况。
  3. 它是 showcase 冷启 24 条告警里的 8 条,占三分之一。

修法(择一)

  1. 把校验推迟到所有插件 start() 完成之后(kernel:ready)—— 那时词汇表才是完整的。这是最贴合 ADR-0018 的做法。
  2. 保留早期校验但降级为 debug/info,在 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 的时间戳。

Activity

  1. self-assigned this
    on Aug 3, 2026
  2. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    认领: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

  3. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    交叉链接:本 issue 与 #4769、#4772 是同一个形状在三个互不相干子系统里的三次独立发作(启动期把「提供方尚未注册」当成「没有提供方」,并把这个结论记账下来)。已把「是否要在个案之外收紧 kernel 注册表契约」立为决策单 #4776,附两轴分析,等维护者裁定。

    本 issue 的修复照常进行,不受 #4776 阻塞 —— #4776 讨论的是这一类还要不要有结构性防线,不改变个案的修法。


    Generated by Claude Code

  4. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    PM 复核:ACCEPT(待 CI 全绿) —— PR #4791。

    dev 改进了我的裁定,并且给出了理由

    我派发时写的是「推迟到 kernel:ready」。dev 改用了 kernel:bootstrapped,并给出三条理由:(1) 本插件自己的 kernel:ready handler 还会再注册一批 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

  5. added a commit that references this issue on Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions