Skip to content

fix(approvals): 无自动化引擎时审批决策不再静默搁浅流程 run (#4420) - #4969

Merged
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4420-suspended-run-durability
Aug 3, 2026
Merged

fix(approvals): 无自动化引擎时审批决策不再静默搁浅流程 run (#4420)#4969
xuyushun441-sys merged 1 commit into
mainfrom
claude/issue-4420-suspended-run-durability

Conversation

@xuyushun441-sys

Copy link
Copy Markdown
Contributor

Fixes #4420

⚠️ 先说结论:issue 的前提已过期,#4420 的两条「期望」在当前 origin/main都已交付

派发词要求先诊断再动手。诊断结果是:issue 描述的 rc.1 现场已经被修好了,修复随 17.0.0-rc.2 出货(changeset 2826d1e),并非「#1518 未落地」。实测证据(全部跑在 origin/main = 6bc93dc,未含本 PR 改动):

派发词的诊断问题 实际答案
谁给引擎接 durable store?默认宿主路径接了吗? AutomationServicePlugin.start()(plugin.ts:596)。默认 suspendedRunStore: 'auto' 即启用,默认路径已接线
谁注册 sys_automation_run 让 schema sync 建表? 同插件 init()manifest 注册(registerRunObject),start() 补注册一次;并声明了 optionalDependencies: ['com.objectstack.engine.objectql'] 保证顺序
复现「新引擎实例 B 收不到决策」 已有回归测试且通过:approval-restart-resume.test.ts

resume(runId) 找不到 run 也早已响亮:引擎返回 RUN_NOT_FOUND / STORE_UNAVAILABLE,serviceResumesuccess:false 抛成异常,assertRunResumable任何写入之前预检,REST 映射 409 / 503 / 500。

✓ approves a run that paused in a previous process, and the flow advances
✓ refuses the decision, writing nothing, when the run did not survive the restart
✓ names the stranded run when the resume fails after the decision was written
✓ treats a concurrent duplicate resume as benign, not as a failure
✓ does not block a decision when the suspended-run store is merely unreachable
Tests  48 passed (service-automation 持久化四套件) / 403 passed (plugin-approvals 全量)

所以 #4420 主体无需重做。 维护者可据此确认是否直接关单。


本 PR 修的是:那次修复唯一没能覆盖到的一条静默路径

#4420 的修复加的每一道闸门 —— assertRunResumable 预检、RESUME_TARGET_LOST 拒绝、RESUME_FAILED 抛错 —— 全都挂在 automation 引擎上。而在一个没有挂引擎的进程里,它们被包在同一个条件里一起跳过了:

if (result.finalized && result.runId && typeof this.automation?.resume === 'function') {

于是:决策落库、镜像状态字段推进、接口返回 HTTP 200 + resumed: false,且零日志。这正是 #4420 报告的症状本身 —— 只是发生在它的修复看不见的那个 composition 里。

可达路径与原 bug 同源:流程在有 automation 的进程里停在 approval 节点,决策却到达一个没有 automation 的进程(插件 init 失败,或跨版本重新 composition)。此时请求行仍带着 flow_run_id —— 那是这行数据自己声明有一个 run 正停在这个决策上。

原有测试其实把这个行为钉住了(leaves a composition with no automation engine exactly as it was),理由是「这里没有任何东西能 resume 它,预检不该无中生有地造一个失败」。该理由混淆了两件事:「没挂引擎」和「没有 run 在等」是不同的事实,只有后者是良性的

改动

决策依然成立,finalized / resumed 对所有既有调用方保持不变 —— 决策落库时人确实已经批了,回滚不在选项内;而对每个这类调用都报错会打断预检刻意保护的 standalone composition。变的只是「不再静默」:

  • error 级日志 —— 依 AGENTS.md 的 durability 规则:持久化状态与运行时状态不一致,而系统从外面看毫无异样,正是该规则针对的那一类;
  • 响应带 resumeError —— 让 resumed: false 带着原因和被搁浅的 run id 到达调用方,而不是让对方猜「到底试没试过 resume」。

复用已注册RESUME_FAILED 码(ADR-0112 ledger)与既有 resume 消息形状,不新造词汇 —— 因为要报告的事实是同一个:一个已记录的结果,其 run 没有推进。

覆盖全部 5 个 recorded-outcome resume 点:decide、改版次数超限的自动拒绝、sendBackresubmit,以及 recall 的两个分支(revise 窗口那支用的是 cancelRun 而非 resume)。

没有过度报告:不带 run 的请求保持安静 —— 它上面没有东西被搁浅,在那里报 error 正是会训练运维忽略 error 的镜像错误,而那恰恰是 #4420 当初那条 warn 没人读的原因。

测试

pnpm --filter @objectstack/plugin-approvals test        → 18 files, 403 passed
pnpm --filter @objectstack/plugin-approvals typecheck   → tsc --noEmit, 0 error
pnpm --filter @objectstack/service-automation test      → 55 files, 662 passed
pnpm check:durability-log-level      → 8 seam(s), all loud or rethrowing ✓
pnpm check:startup-registry-verdict  → 47 seam(s), none recording a verdict ✓
pnpm check:error-code-casing         → 2722 files, no lowercase codes ✓

新增 2 条回归测试(替换那条把静默钉住的旧测试):

  • still records the decision with no automation engine attached — but never silently
  • reports the same gap on a request that names no run — by staying quiet

约束遵守

packages/spec/** 零改动(故复用已注册的 RESUME_FAILED,不新增 ledger 码)、metadata-protocol/src/protocol.ts 零改动、content/docs/releases/ 零改动。改动面仅 packages/plugins/plugin-approvals/src/** + 一个 changeset。

待维护者定夺(见下)

本 PR 取的是「决策保留 + 响亮报告」。另一种读法是「拒绝这个决策」(视为坏 composition,返回 409)。后者语义更强,但会翻转上述被刻意钉住的设计决定,且可能打断合法的 standalone 部署 —— 故未擅自采用,列为 open question。

🤖 Generated with Claude Code

https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX


Generated by Claude Code

…g the run silently (#4420)

#4420's fix closed every path by which an approval decision could be recorded
while its flow stayed parked — except the one where none of its guards could
run. `assertRunResumable`, the `RESUME_TARGET_LOST` refusal and the
`RESUME_FAILED` throw all hang off the automation engine, and in a process with
no engine attached every one of them was skipped by the same
`typeof this.automation?.resume === 'function'` condition that wrapped the
resume itself. The decision was written, the mirrored status field advanced,
and the call answered HTTP 200 / `resumed: false` with nothing logged — #4420's
reported symptom, in the one composition its fix could not see.

Reachable exactly the way the original bug was: a run parks at an `approval`
node in a process that has the automation service, and the decision arrives in
one that does not. The row still carries a `flow_run_id`, which is its own
declaration that a run is parked on the decision.

The decision still stands — it is durable by then, and refusing every such call
would break the standalone compositions the pre-flight deliberately protects —
so `finalized` / `resumed` are unchanged for existing callers. The gap is now
reported: `error` level per AGENTS.md's durability rule (persisted state and
runtime state disagree while nothing looks broken), plus `resumeError` on the
response naming the stranded run. Reuses the registered `RESUME_FAILED` code
and the existing resume message shape rather than adding a vocabulary.

Applied at all five recorded-outcome resume sites: decide, the revision-limit
auto-rejection, sendBack, resubmit, and both branches of recall (whose
revise-window path needs `cancelRun`). A request naming no run stays quiet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NrmBxj8rK2uGCnh9aipjwX
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-approvals.

5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/plugin-approvals)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-approvals)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-approvals)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-approvals)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants