## 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` 的 ```ts } 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`。 ## 复现 ```bash rm -rf examples/app-showcase/.objectstack && pnpm dev ``` 想看时序:`objectstack dev --seed-admin --log-level info`,对比 `WARN Flow '...'` 与 `ApprovalsServicePlugin: service registered` 的时间戳。
Activity
os-zhuang commentedon Aug 3, 2026
认领:PM 循环第 3 轮
会话:
session_015Br2xsJsczFsTR9bvbh2Ny分支:
claude/issue-4771-flow-validation-kernel-readyWorktree:
objectstack-issue-4771分诊:落地 objectstack(后端);PM 裁定取修法 1(校验推迟到
kernel:ready,即所有插件start()完成之后)。理由按两条轴:
registerNodeExecutor(type)扩展节点词汇表。那么在扩展完成前就拍板的校验器,是在校验一个还没成型的世界;修法 2(降级为 debug + 事后补一次权威校验)保留了那个错误的早期判断,只是把它藏起来。把校验挪到词汇表真正封闭的那一刻,是唯一让这个检查「说的话为真」的做法。一并处理(issue 末尾提到,属于同一缺陷类,不要漏):
plugin-approvals/src/approvals-plugin.ts:283那个catch把「没有 automation 引擎 ⇒ approval 节点没注册」记成info,而 dev 默认日志级别是warn—— 真降级发生时反而看不见。按 #4632 已立的规则(静默降级必须响亮),这里至少是warn。维护者可否决本裁定;不阻塞派发。
Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
交叉链接:本 issue 与 #4769、#4772 是同一个形状在三个互不相干子系统里的三次独立发作(启动期把「提供方尚未注册」当成「没有提供方」,并把这个结论记账下来)。已把「是否要在个案之外收紧 kernel 注册表契约」立为决策单 #4776,附两轴分析,等维护者裁定。
本 issue 的修复照常进行,不受 #4776 阻塞 —— #4776 讨论的是这一类还要不要有结构性防线,不改变个案的修法。
Generated by Claude Code
os-zhuang commentedon Aug 3, 2026
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 做的事。裁定的意图(校验只在词汇表封闭那一刻才成立)被完整保住了。复核要点
registerFlow恢复即时告警,因为 Studio 发布 / dev reload 进正在运行的服务器时,词汇表确实完整,那句断言此时为真。有专门用例钉住。the two deployments are DISTINGUISHABLE直接断言「装了插件的 boot」与「没装的 boot」输出不同(0 !== 1)。修复前这两个 boot 打印的东西完全一样 —— 那才是这个 bug 的实质。把「可区分」本身写成断言,比任何计数断言都准确。Flow registered: …出现在Node executor registered: approval之前,即误报窗口确实发生了。没有这一步,测试可能在一个根本不触发 bug 的顺序下"通过"。nodeTypeVocabularySealed初值改回true(等价于修复前),4 个用例里 3 个失败,其中「没装插件」那条从 1 条告警变成 2 条(注册时 + 封闭时各报一次)。obsolete/invalid)不再上报,口径与既有getTriggerBindingAudit一致。这是我没要求、但完全正确的收敛:它消灭的正是本 issue 要消灭的那类假断言。getUnknownNodeTypeAudit()让 host(CLI 启动摘要 / 健康检查)直接读引擎,而不是去 grep 日志。沿用getTriggerBindingAudit()的既有套路。info改成warn([convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632 规则),还(a)把catch收窄到「服务查找」这一步 —— 这样registerApprovalNode内部真出错时会以自己的身份抛出,不再被贴上 "no automation engine" 的错误标签;(b)补上了「automation服务存在但不接受节点执行器」这条从前一条日志都不打的分支。第二条是 dev 自己发现的,原 issue 没提。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
docs(automation): flows.mdx 的节点 `type` 校验时机改为两阶段描述 (#4771)
fix(automation,approvals): 节点类型校验推迟到插件词汇表封闭之后 —— approval flow 不再被误报「…
refactor(hono,runtime): place the middleware seam up front so the lim…
feat(devx): gate the "startup registry verdict" class — read the regi…
feat(spec,runtime,hono)!: 入站 rateLimit 从零建 seam —— 授权预算真正产生 429 (obje…
fix(service-automation): warn once when a flow runs on a never-sealed…
docs(qa): record the objectstack-ai#7545 ruling in flow-node-type-mat…
fix(lint): flow, hook, action and expression rule findings state each…