做 #4045 的 wait 契约毕业时发现的,属于「已声明但从不被读」那条臂 —— 而且是 PD #10 的形状:宣传了一个运行时不提供的能力。
两处,合起来是一件事
1. waitEventConfig.onTimeout 有零个读取者。
// packages/spec/src/automation/flow.zod.ts:210
onTimeout: z.enum(['fail', 'continue']).default('fail').describe('Behavior when the wait times out'),
在 framework 仓库里搜 onTimeout(排除 dist/、.d.ts、测试),除定义处外的命中全是无关的:acquireConnectionTimeout 注释、actionTimeoutMs、connectionTimeoutMs。没有任何路径读它。它带 .default('fail'),所以每个 wait 节点都被盖上一个不起作用的值。
2. waitEventConfig.timeoutMs 唯一的读取者把它当别的东西用。
声明的语义是超时上限:
// flow.zod.ts:209
timeoutMs: z.number().int().min(0).optional().describe('Maximum wait time before timeout (ms)'),
而唯一读它的地方把它当定时时长的兜底:
// packages/services/service-automation/src/builtin/wait-node.ts
const durationMs =
parseIsoDuration(wec.timerDuration) ??
(typeof wec.timeoutMs === 'number' ? wec.timeoutMs : undefined);
即:没写 timerDuration 时,timeoutMs 变成了「等多久」,而不是「最多等多久」。(engine.ts:3135 读的 node.timeoutMs 是节点上的另一个字段,不是这个。)
合起来:wait 根本没有超时实现。 Zod 和文档描述的「等待超时后 fail 或 continue」这套契约在运行时不存在。
为什么这条值得单独立项
我们自己的示例在依赖它。
// examples/app-showcase/src/automation/flows/index.ts:554
waitEventConfig: { eventType: 'timer', timerDuration: 'PT1M', onTimeout: 'continue' },
onTimeout: 'continue' 什么都不做。而且 timeoutMs 进了生成参考文档(content/docs/references/automation/flow.mdx:107 的 waitEventConfig 类型里列着 timeoutMs?: integer),所以这是已发布的表面,不是内部细节。
两条路,需要人来定
不是可以顺手猜的 —— 两条都会改动已发布契约:
A. 实现它。 在 resume 触发器之外再排一个一次性超时 job:到点后 resume(runId) 并按 onTimeout 决定是让该节点失败还是继续往下走。要注意冷启动重排那一遍(rearmSuspendedWaitTimers)也得认识这个第二个 deadline,否则重启后超时就丢了 —— 这正是 ADR-0019 后续为定时器补 waitUntil 持久化时踩过的那类问题。同时得把 timeoutMs 从「时长兜底」改回「超时上限」,那是一个行为变更:今天只写 timeoutMs 的流程会从「等 N 毫秒」变成「立刻不带 deadline 挂起」。
B. 撤回它。 给两个键上墓碑、停止发布,并把 showcase 那处 onTimeout 删掉。如果 timeoutMs 的实际用法就是「时长」,那更诚实的做法是把它并进 timerDuration(parseIsoDuration 已经接受裸数字当毫秒,所以 timerDuration: 60000 已经能表达同一件事)。
我倾向 B 的后半段:timerDuration 已经能表达 timeoutMs 今天实际做的事,而真正的超时语义是个新特性,应该在有人真的要它的时候按需求做,而不是为了对齐一段没人实现过的描述文字去补。但这条要你拍。
相关
未覆盖
只搜了 framework 仓库。objectui 侧是否有读 onTimeout 的设计器代码没查 —— 但本仓库里没找到 waitEventConfig 的任何设计器表单,所以大概率也没有。
做 #4045 的
wait契约毕业时发现的,属于「已声明但从不被读」那条臂 —— 而且是 PD #10 的形状:宣传了一个运行时不提供的能力。两处,合起来是一件事
1.
waitEventConfig.onTimeout有零个读取者。在 framework 仓库里搜
onTimeout(排除dist/、.d.ts、测试),除定义处外的命中全是无关的:acquireConnectionTimeout注释、actionTimeoutMs、connectionTimeoutMs。没有任何路径读它。它带.default('fail'),所以每个 wait 节点都被盖上一个不起作用的值。2.
waitEventConfig.timeoutMs唯一的读取者把它当别的东西用。声明的语义是超时上限:
而唯一读它的地方把它当定时时长的兜底:
即:没写
timerDuration时,timeoutMs变成了「等多久」,而不是「最多等多久」。(engine.ts:3135读的node.timeoutMs是节点上的另一个字段,不是这个。)合起来:
wait根本没有超时实现。 Zod 和文档描述的「等待超时后 fail 或 continue」这套契约在运行时不存在。为什么这条值得单独立项
我们自己的示例在依赖它。
onTimeout: 'continue'什么都不做。而且timeoutMs进了生成参考文档(content/docs/references/automation/flow.mdx:107的waitEventConfig类型里列着timeoutMs?: integer),所以这是已发布的表面,不是内部细节。两条路,需要人来定
不是可以顺手猜的 —— 两条都会改动已发布契约:
A. 实现它。 在 resume 触发器之外再排一个一次性超时 job:到点后
resume(runId)并按onTimeout决定是让该节点失败还是继续往下走。要注意冷启动重排那一遍(rearmSuspendedWaitTimers)也得认识这个第二个 deadline,否则重启后超时就丢了 —— 这正是 ADR-0019 后续为定时器补waitUntil持久化时踩过的那类问题。同时得把timeoutMs从「时长兜底」改回「超时上限」,那是一个行为变更:今天只写timeoutMs的流程会从「等 N 毫秒」变成「立刻不带 deadline 挂起」。B. 撤回它。 给两个键上墓碑、停止发布,并把 showcase 那处
onTimeout删掉。如果timeoutMs的实际用法就是「时长」,那更诚实的做法是把它并进timerDuration(parseIsoDuration已经接受裸数字当毫秒,所以timerDuration: 60000已经能表达同一件事)。我倾向 B 的后半段:
timerDuration已经能表达timeoutMs今天实际做的事,而真正的超时语义是个新特性,应该在有人真的要它的时候按需求做,而不是为了对齐一段没人实现过的描述文字去补。但这条要你拍。相关
configSchemaand the keys its executor actually reads are still unreconciled —notifyhonourscfg.source, which no schema declares #4045 —— 「声明 vs 实际读取」未对账这条主线;本条是它 caveat 清单里wait那格查出来的第二个结果config后门毕业进 ADR-0087 D2 转换层),并刻意保留了timeoutMs当时长的现有行为 —— 在一次契约清理里顺手改行为是不对的未覆盖
只搜了 framework 仓库。objectui 侧是否有读
onTimeout的设计器代码没查 —— 但本仓库里没找到waitEventConfig的任何设计器表单,所以大概率也没有。