Skip to content

[automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420

Description

@baozhoutao

版本

  • @objectstack/*@17.0.0-rc.1(service-automation 17.0.0-rc.1)
  • 复现于 os-project-titanwind-ehr,2026-07-31

现象

含 approval 节点的 record_change 流:提交审批与审批决策跨进程重启时,决策静默失效——

  1. 进程 A:record_change 触发 qif_approval 流,approval 节点挂起,sys_approval_request 落库(带 flow_run_id),镜像字段 stage=qc_director/status=pending;
  2. dev 进程重启(进程 B,同一 DB);
  3. 审批人在 Console 上「批准」→ sys_approval_request.status 翻成 approved 落库成功、UI toast 成功;
  4. 但流永不 resume:下一节点的 approval request 不创建,记录镜像字段停在 qc_director / approved 成僵尸;日志零报错(resume(runId) 找不到 run 疑似被静默吞掉)。

对照:同一进程内提交+审批的全链路正常(实测 NCR 三级流 ①→② 推进无恙)——问题只出在跨重启。

定位

  • service-automation: persist suspended flow runs (durable resume across process restarts) #1518(persist suspended flow runs / durable resume across restarts)2026-06-02 标记 COMPLETED 关闭;
  • 但 17.0.0-rc.1 的 service-automation/dist/index.js 里 suspendedRuns = new Map() 仍是纯内存,resume 查不到即止;
  • dist 里出现的 sys_automation_run 标识符,在运行库中根本没建表(no such table: sys_automation_run)——持久化对象未注册或未走 schema 同步。

影响

审批流是天然的长挂起(人到岗才批,中间隔发版/重启是常态)。当前行为=每次发版都可能把在途审批全部变僵尸,且审批人侧毫无感知(approve 明明成功),属数据一致性级别的问题。

期望

  1. 挂起 run 持久化 + 重启后可 resume(service-automation: persist suspended flow runs (durable resume across process restarts) #1518 承诺的行为真正落地);
  2. 短期兜底:resume(runId) 找不到 run 时必须显式报错(approve 接口应失败或至少告警),不能落一半状态静默成功。

Activity

  1. self-assigned this
    on Aug 1, 2026
  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    分诊:入队,并标记为本次发版最该处理的一条(归运行时/服务面车道)

    维护者在发版前要求扫「未入队但值得发版前做」的单,本条是我扫出来的第一优先。此前它有 assignee 但没有任何队列标签,也就是说它不在任何 PM 的派发视野里 —— 补 pm:queue。

    车道:落点 packages/services/service-automation + approvals,属 os-zhuang 主会话(session_015Br2xsJsczFsTR9bvbh2Ny)车道,本会话(session_018iARDqtrhQgz6fVHDeDkbQ,packages/lint / skills/** / content/docs/**)不认领、不派发,仅做分诊与移交。

    为什么它排第一

    1. 它是冲着正在发的这个版本来的。报告里写明 @objectstack/*@17.0.0-rc.1,现场是真实项目(os-project-titanwind-ehr,2026-07-31),不是构造出来的场景。
    2. 失败形态是静默的:审批人点「批准」→ sys_approval_request.status 落库成 approved、UI 提示成功 → 流永不 resume、下一节点不创建、记录镜像字段停成僵尸、日志零报错。审批人侧完全无感。
    3. 触发条件恰好就是发版。审批流天然长挂起(人到岗才批),中间隔一次进程重启是常态 —— 而发版就是最典型的那次重启。用报告自己的话:「每次发版都可能把在途审批全部变僵尸」。
    4. 它同时是一条 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): hook condition 的 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

  3. claude commented on Aug 3, 2026

    @claude
    Contributor

    @os-zhuang 过期认领核查:本单是队列里唯一的 p0 服务面 bug,当前挂在你名下,但无认领评论、远端无分支、无 PR;且你的会话 session_015Br2xsJsczFsTR9bvbh2Ny 已于 12:34Z 在 #4604 交班并声明「在飞清零」。

    若确实无人在做,请回一条确认;本会话(session_01NrmBxj8rK2uGCnh9aipjwX,已在 #4604 登记接管 engine/services 车道)拟于下一轮按 SKILL.md 过期认领规程将其解绑归队并优先派发 —— 它比当前队列里任何一单都值钱,不应被一个旧 assignee 字段挡住。若你在做,以你为准,我让行。


    Generated by Claude Code

  4. claude commented on Aug 3, 2026

    @claude
    Contributor

    🔒 认领: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

  5. claude commented on Aug 3, 2026

    @claude
    Contributor

    ✅ 验收通过 —— PR #4969(draft,等 CI);前提证伪 + 一条决策记录

    前提更正(公开归档,后续读者以此为准)

    issue 是 7-31 对 17.0.0-rc.1 的现场报告;dev 实测确认其两条「期望」在 rc.2 已全部交付(changeset 2826d1e),并在 origin/main 上跑通了既有回归作为基线证据:

    1. durable store 由 AutomationServicePlugin.start() 在默认 suspendedRunStore: 'auto' 下接线(plugin.ts:596)—— 正常宿主路径,非仅测试;
    2. sys_automation_run 在 init() 注册(start() 重试),optionalDependencies 声明了当时现场失效的加载顺序;
    3. 跨重启 resume 回归(approval-restart-resume.test.ts 6 例)与响亮失败(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

  6. claude commented on Aug 4, 2026

    @claude
    Contributor

    决策记录收口:维护者 2026-08-04 确认维持已交付的方案 A(落库 + 响亮上报),方案 B(409 硬拒)不做。此项关闭。


    Generated by Claude Code

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions