Skip to content

automation: ActionDescriptor.supportsPause / isAsync 是零读者的声明字段 —— 引擎判定「暂停」的唯一事实是 execute() 返回的 suspend: true #5703

Description

@os-zhuang

发现于 #5561 第一步(注册时对未声明 resumeAuthority 的 pausing executor 打告警)的前提重验。observation-class:今天没有用户会撞到,单独立单只为把事实记下来。

事实(origin/main @ 6ddb2ec)

  • ActionDescriptor.supportsPause 与 ActionDescriptor.isAsync(均在 packages/spec/src/automation/node-executor.zod.ts,均 .default(false))在 framework / objectui / cloud 三仓没有任何非测试读者。仓库范围内对 .supportsPause / .isAsync 的读取只有 3 处断言:
    • packages/spec/src/automation/node-executor.test.ts:174(读默认值)、:218(读 legacy WAIT_EXECUTOR_DESCRIPTOR)
    • packages/plugins/plugin-approvals/src/approval-node.test.ts:100
  • 引擎判定「这个节点暂停了」的唯一事实是执行结果,不是描述符声明:packages/services/service-automation/src/engine.ts:4256 的 if (result.suspend)。
  • automation: the generic run-resume route needs an authorization gate keyed on the suspended node #3801 的 resume 闸门也不看它:resolveResumeAuthority(engine.ts:2708)只沿 alias 走到 canonical 后读 resumeAuthority,supportsPause 全程不参与。

为何是 observation-class

今天全部 6 个 pausing 执行器都在本仓内,且 supportsPause: true 声明都正确 —— screen(builtin/screen-nodes.ts:50)、wait(builtin/wait-node.ts:166)、subflow(builtin/subflow-node.ts:54)、map(builtin/map-node.ts:53)、approval(plugin-approvals/src/approval-node.ts:114)、approval_revise(approval-revise-node.ts:79)。仓内没有第三方 pausing 插件,示例应用也不注册任何节点执行器,所以这个字段今天错不了。

为何仍值得记

#5561 第一步的裁决把告警的触发条件定在 supportsPause: true。一旦有代码开始按这个字段下判词,它就从「无人读的声明」变成安全判定的输入 —— 而它当前不被任何执行路径校验:一个 execute() 返回 suspend: true、却把 supportsPause 留在默认 false 的执行器,既不会被那条告警点名,又照样拿到 resumeAuthority 的 fail-open 默认。这就是 declared 不等于 enforced(Prime Directive #10)在同一个节点上的第二层,且正好落在 #5561 想堵的那条缝上。

两条路(只记录,不在本单裁)

按 ADR-0049 enforce-or-remove 择一:

  1. enforce:在 engine.ts:4256 的 result.suspend 处对照该节点类型描述符的 supportsPause,不一致就拒绝/告警(把描述符声明变成运行时可反驳的事实)。
  2. 承认它是纯设计器面字段:它确实已经进了 content/docs/references/automation/node-executor.mdx,那就在 TSDoc 里写清「声明供设计器/文档使用,运行时不校验」,别让下一个作者以为它是闸门输入。

Refs #5561、#3801、#3823。

Activity

  1. claude commented on Aug 6, 2026

    @claude
    Contributor

    发现分诊轮判级(#4949 纪律):持有(finding 留)。

    为什么还留着:事实成立(ActionDescriptor.supportsPause / isAsync 三仓零非测试读者,引擎判"暂停"的唯一事实是 engine.ts:4256 的 if (result.suspend),#3801 的 resume 闸门也不看它),但处置是退役一个已发布的 spec 声明字段 —— 属 breaking,须走 ADR-0049 enforce-or-remove 的完整路线(ADR-0087 转换 + 生成基线 + 文档 + pin 测试),而 v17 的退役窗已随 #4938 批次收口。今天无用户可达伤害:全部 6 个 pausing 执行器都在本仓内且声明都正确,声明与事实暂时同向。

    重启条件:v18 列车的退役批开列时一并测量与判定 —— 与 #5295(ServerEvent* / ServerCapabilities / ServerStatus 零消费者,08-05 已按同一理由持有、同一重启条件)同批处理,两单是同一形态(spec 侧幸存声明 + 零读者 + 退役属 breaking),分开判会得出不一致的口径。

    ⚠️ 另一条可先行的路(若 services 车道认为值得):把「声明与事实不同向」变成门可见 —— 注册时校验 supportsPause 与执行器实际是否返回 suspend 的一致性(即 #5561 那条告警的同族),这不需要退役任何字段,可独立立单。本轮不代拆。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    ContributorAuthor

    存量裁决轮(维护者 2026-08-06 委托,session_01LeEfA7CFwbJb7JJmXm2KM3):维持持有,但记录一处前提更正——「零读者」已过时:#5561(PR #5725,06:03Z 前合入)已在 engine.ts:1388 落地按 supportsPause 下判词的告警,其 TSDoc 明写「#5703 tracks the runtime half」。本单现追踪的是 runtime 半边(execute 返回 suspend 而声明 false 时告警静默 fail-open);今天仓内 6 个 pausing 执行器声明皆正确、无第三方执行器,无人可撞。重启条件:services 车道投资一致性校验,或第三方 pausing 执行器面出现。


    Generated by Claude Code

  3. claude commented on Aug 7, 2026

    @claude
    Contributor

    Findings triage (#4949 discipline): hold — finding retained, domain:spec unchanged.

    Stale-premise check. The 08-06 08:06Z re-grading already corrected the body's "zero readers" premise: #5561 / PR #5725 landed a registration-time warning that does judge on supportsPause (engine.ts:1388), so what this issue now tracks is strictly the runtime half — an executor whose execute() returns suspend: true while its descriptor leaves supportsPause at the .default(false), which the warning cannot see and which still reaches resumeAuthority's fail-open default. That scoping still holds this round; no new commit changes it.

    One state change worth recording, because it moves the restart condition: #5561 is now pm:on-hold (it was open and active when the previous restart condition was written). Both of that condition's clauses therefore need re-reading:

    Restart condition, re-anchored to the surviving carrier: the v18 retirement batch, processed together with #5295 (ServerEvent* / ServerCapabilities / ServerStatus, held 08-05 on the same grounds). Both are the same shape — a surviving packages/spec declaration with no production reader, whose removal is breaking and so owes the full ADR-0049 enforce-or-remove route (ADR-0087 conversion, generated baselines, docs, pin tests). Judging them apart would produce inconsistent verdicts on one question, which is why they stay batched. The v17 retirement window closed with the #4938 batch, so neither can move before that train.

    Unchanged and still available as an independent path if the services lane wants it: making declaration-vs-fact gate-visible (validate at registration that a descriptor claiming no pause never returns suspend) retires nothing and needs no window. ⛔ Not split out by this seat — that is the lane's call, and the transfer protocol runs through the target queue, not through this thread.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Findings sweep (maintainer-authorized one-off, 2026-08-07 — registered on #6015): promoted to the queue as a retirement card. ActionDescriptor.supportsPause / isAsync have zero non-test readers across three repos while the engine's only pause fact is result.suspend — declared-but-inert fields under the maintainer's standing tight default (today's family rulings: #6247 retire, #5488 flip-to-false). ADR-0049 remove side unless a reader is found at claim time. finding → pm:queue.


    Generated by Claude Code

  5. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Bundled into sweep card #6486 (ADR-0049 zero-consumer retirements, 5 members). Keeps pm:queue but is excluded from batch selection while #6486 is open — claim the sweep card, not this one; it closes via Fixes when that PR merges.


    Generated by Claude Code

  6. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling (2026-08-08): Option A — close this card; successor filed as #6667.

    The card's premise ("zero readers in all three repos") expired while it sat: #5561 / PR #5725 landed warnIfResumeAuthorityUndeclared (engine-side wording keyed on supportsPause), the check-resume-authority-declared.mjs CI gate, dedicated tests, and TSDoc. supportsPause is now an enforced input, not an inert declaration — retiring it would delete an enforcement surface the maintainer just shipped. isAsync, taken alone, still has zero readers; its disposition moves to the successor so it gets decided on fresh measurements at pickup time, not today's reading.

    Successor #6667 carries the two remaining pieces: the runtime consistency check (executor suspend result vs declared supportsPause — the blind spot the authoring-time gate does not cover) and the isAsync decision.

    Closing as not planned: the retirement this card proposed will not happen.

    Maintainer directive (verbatim, covering all 14 inbox cards): 「你的建议全部接受」. Recorded by PM session session_01JaVVMrSxt7Tgi1uwEuDtH7.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions