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&&typeofthis.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-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 3, 2026
@github-actions

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@xuyushun441-sys@claude