Repository navigation
[automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420
Description
Activity
- added 4 commits that reference this issue
on Aug 1, 2026 - added 3 commits that reference this issue
on Aug 2, 2026 - addedbugSomething isn't workingSomething isn't workingpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVP
on Aug 3, 2026 xuyushun441-sys commented
on Aug 3, 2026 CollaboratorMore actions分诊:入队,并标记为本次发版最该处理的一条(归运行时/服务面车道)
维护者在发版前要求扫「未入队但值得发版前做」的单,本条是我扫出来的第一优先。此前它有 assignee 但没有任何队列标签,也就是说它不在任何 PM 的派发视野里 —— 补
pm:queue。车道:落点
packages/services/service-automation+ approvals,属os-zhuang主会话(session_015Br2xsJsczFsTR9bvbh2Ny)车道,本会话(session_018iARDqtrhQgz6fVHDeDkbQ,packages/lint/skills/**/content/docs/**)不认领、不派发,仅做分诊与移交。为什么它排第一
- 它是冲着正在发的这个版本来的。报告里写明
@objectstack/*@17.0.0-rc.1,现场是真实项目(os-project-titanwind-ehr,2026-07-31),不是构造出来的场景。 - 失败形态是静默的:审批人点「批准」→
sys_approval_request.status落库成approved、UI 提示成功 → 流永不 resume、下一节点不创建、记录镜像字段停成僵尸、日志零报错。审批人侧完全无感。 - 触发条件恰好就是发版。审批流天然长挂起(人到岗才批),中间隔一次进程重启是常态 —— 而发版就是最典型的那次重启。用报告自己的话:「每次发版都可能把在途审批全部变僵尸」。
- 它同时是一条 declared ≠ delivered:service-automation: persist suspended flow runs (durable resume across process restarts) #1518(persist suspended flow runs / durable resume across restarts)已于 2026-06-02 标记 COMPLETED 关闭,但 rc.1 的
service-automation/dist里suspendedRuns = new Map()仍是纯内存,sys_automation_run在运行库里根本没建表。一个被宣告完成的能力在待发布版本里不生效 —— 这正是我们这两天在 bug(objectql): hook 条件对着「只含本次更新字段」的残缺 record 求值,取不到的字段被吞成 false —— 审计 hook 静默不触发 #4770 / bug(objectql/docs): hookcondition的 CEL 作用域只绑定record—— 文档教的previous.x/ctx.record根本不存在,过渡型条件写不出来 #4784 / fix(metadata-protocol): deleteManyData has the same fake-atomic as batchData, updateManyData ignores atomic entirely #4620 反复修的同一个形状,只是这条的后果是数据一致性级别的。
给接手方的建议(不构成指示,车道是你的)
issue 的「期望」里两条优先级不同,建议拆开看:
- 第 2 条(兜底)必须进本次发版:
resume(runId)找不到 run 时显式报错,approve 接口失败或至少告警。这是小改动,且它把「静默数据损坏」降级成「响亮的失败」—— 即便持久化来不及做,有它就不会再有人在不知情的情况下丢掉在途审批。 - **第 1 条(挂起 run 持久化 + 重启后可 resume)**是 service-automation: persist suspended flow runs (durable resume across process restarts) #1518 承诺的完整行为,工作量大得多,能不能进这个版本由你和维护者判断。
如果只能做一件事,做第 2 条。
⚠️ 派发前请照例做前提复核:issue 写于 07-31,service-automation这几天有改动,请对着当前main确认suspendedRuns仍是内存 Map、sys_automation_run仍未建表 —— 今天已经有两条 issue 的前提被我核出已过期(#4728 的「包冻结」、#4763 的验收项)。
Generated by Claude Code
- 它是冲着正在发的这个版本来的。报告里写明
@os-zhuang 过期认领核查:本单是队列里唯一的 p0 服务面 bug,当前挂在你名下,但无认领评论、远端无分支、无 PR;且你的会话
session_015Br2xsJsczFsTR9bvbh2Ny已于 12:34Z 在 #4604 交班并声明「在飞清零」。若确实无人在做,请回一条确认;本会话(
session_01NrmBxj8rK2uGCnh9aipjwX,已在 #4604 登记接管 engine/services 车道)拟于下一轮按 SKILL.md 过期认领规程将其解绑归队并优先派发 —— 它比当前队列里任何一单都值钱,不应被一个旧 assignee 字段挡住。若你在做,以你为准,我让行。
Generated by Claude Code
🔒 认领:PM 循环第 1 轮追加(p0 加急)
会话:session_01NrmBxj8rK2uGCnh9aipjwX(账号xuyushun441-sys)
分支:claude/issue-4420-suspended-run-durability
Worktree:objectstack-issue-4420
域:domain:services
文件面:packages/services/service-automation/src/**(engine.ts 挂起/resume 路径、durable store 接线)+ 必要时packages/plugins/plugin-approvals的 resume 调用点过期认领处置记录:原 assignee
os-zhuang无认领评论、无分支、无 PR,其会话已在 #4604 交班声明在飞清零;16:37Z 质询后维护者直接确认「没人在做,解绑归队直接派」。据此按 SKILL.md 规程解绑并重新认领。派发前提复核(对 origin/main 实测,更新 issue 的 rc.1 现场):
suspendedRuns = new Map()仍在(engine.ts:1086)—— 但其 TSDoc 已写明存在 Optional durable backing;ObjectStoreSuspendedRunStore/sys_automation_run平台对象在 CHANGELOG 均有落地记录(含 retention 声明、engine-owned 锁读)。- 即:service-automation: persist suspended flow runs (durable resume across process restarts) #1518 的持久化机制已存在,issue 现场的失效更可能是默认宿主不接线 / 对象未注册同步(「生产者在哪」形状)。已写进派发词,dev 先诊断接线,再修。
⛔ 硬约束:
packages/spec/**及生成物零改动;metadata-protocol/src/protocol.ts零改动(在飞 #4721)。
全局在飞不相交已核:与 #4889/#4821/#4884/#4875/#4774/#4721/#4636 文件面均不交。
Generated by Claude Code
✅ 验收通过 —— PR #4969(draft,等 CI);前提证伪 + 一条决策记录
前提更正(公开归档,后续读者以此为准)
issue 是 7-31 对 17.0.0-rc.1 的现场报告;dev 实测确认其两条「期望」在 rc.2 已全部交付(changeset 2826d1e),并在 origin/main 上跑通了既有回归作为基线证据:
- durable store 由
AutomationServicePlugin.start()在默认suspendedRunStore: 'auto'下接线(plugin.ts:596)—— 正常宿主路径,非仅测试; sys_automation_run在 init() 注册(start() 重试),optionalDependencies声明了当时现场失效的加载顺序;- 跨重启 resume 回归(
approval-restart-resume.test.ts6 例)与响亮失败(RUN_NOT_FOUND / STORE_UNAVAILABLE +assertRunResumable预检 + 409/503/500 映射)在改动前就绿。
本 PR 实际修的:最后一条静默路径
上述守卫全部挂在 automation 引擎上 —— 无引擎的组合里同一个
typeof this.automation?.resume === 'function'条件把它们全部静默跳过:决策落库、镜像字段推进、200、零日志 —— 恰是 #4420 的症状形状。现在决策仍成立(不 breaking),但绝不静默:error 级日志 + 机器可读resumeError点名搁浅的 run,复用已注册的 RESUME_FAILED 码(spec 零改动),五个决策出口(decide / auto-rejection / sendBack / resubmit / recall×2)全覆盖。📌 决策记录(维护者可否决,不阻塞落地)
无引擎组合下、请求带
flow_run_id时,是否应当直接 409 拒绝(方案 B)而非「落库+响亮上报」(方案 A,已交付)? dev 的两轴分析很完整(见 PR 正文):B 更硬地兑现「flow_run_id即声明」,但会把持有历史行的 standalone 部署从「有报告的缺口」变成「硬故障」,且推翻另一位作者钉过的测试决定。PM 采纳 A 落地;B 若要做,须维护者明示,届时单开 issue。下游收尾(不归本 PR)
- 现场部署(os-project-titanwind-ehr)已僵尸化的行不会被自动修复(changelog 明确
releaseDeadRunRequests只扫 pending;inspectStrandedRequests只报告不改写)—— 需操作员处置; - 建议现场在 rc.2+ 复验一次:若同症状再现,那是第二成因,开新单,不复用本号。
验证:plugin-approvals 403 全过 / service-automation 662 全过 / 三个仓库闸门(durability-log-level、startup-registry-verdict、error-code-casing)全绿。⛔ spec / protocol.ts / releases 零触碰(逐一核实)。
CI 绿后转 ready 入合并队列。
Generated by Claude Code
- durable store 由
- added 6 commits that reference this issue
on Aug 4, 2026 - added a commit that references this issue
on Sep 8, 2026 - added a commit that references this issue
on Sep 9, 2026 - added a commit that references this issue
on Sep 17, 2026
版本
@objectstack/*@17.0.0-rc.1(service-automation 17.0.0-rc.1)现象
含
approval节点的 record_change 流:提交审批与审批决策跨进程重启时,决策静默失效——qif_approval流,approval节点挂起,sys_approval_request落库(带flow_run_id),镜像字段 stage=qc_director/status=pending;sys_approval_request.status翻成approved落库成功、UI toast 成功;qc_director / approved成僵尸;日志零报错(resume(runId)找不到 run 疑似被静默吞掉)。对照:同一进程内提交+审批的全链路正常(实测 NCR 三级流 ①→② 推进无恙)——问题只出在跨重启。
定位
service-automation/dist/index.js里suspendedRuns = new Map()仍是纯内存,resume 查不到即止;sys_automation_run标识符,在运行库中根本没建表(no such table: sys_automation_run)——持久化对象未注册或未走 schema 同步。影响
审批流是天然的长挂起(人到岗才批,中间隔发版/重启是常态)。当前行为=每次发版都可能把在途审批全部变僵尸,且审批人侧毫无感知(approve 明明成功),属数据一致性级别的问题。
期望
resume(runId)找不到 run 时必须显式报错(approve 接口应失败或至少告警),不能落一半状态静默成功。