Skip to content

[automation] getSuspendedScreen 只读内存热缓存 —— 重启后一个仍然活着的挂起 run 取不到它的 screen(404) #4515

Closed

Description

@os-zhuang

在做 #4470(让验证 harness 走真实持久化路径)时发现的,与那张卡本身无关,单独记录。

问题

AutomationEngine.getSuspendedScreen 只查内存热缓存:

// packages/services/service-automation/src/engine.ts
getSuspendedScreen(runId: string): ScreenSpec | null {
    return this.suspendedRuns.get(runId)?.screen ?? null;
}

它是同步的,所以没有能力去问挂起存储。而 SuspendedRun.screen 是会持久化的(sys_automation_run.screen_json),resume() 也确实会在热缓存未命中时经 loadSuspendedRunStrict 从库里冷读回来。

于是两条路径对同一个 run 给出相反的答案:

进程重启后,对一个 durably 挂起的 screen run 结果
POST …/runs/:runId/resume 正常继续(从库里 rehydrate)
GET …/runs/:runId/screen 404 No pending screen for run

为什么这有实际影响

这条路由的存在意义就是「刷新安全的重新拉取」——屏幕流 runner 在用户刷新页面、或换设备继续时用它把表单再画出来。而它恰恰在最需要它的那一刻失效:进程重启之后。用户看到的是「这个审批/表单不见了」,但 run 其实活得好好的,resume 也还能打通——只是客户端已经拿不到该渲染什么了。

这也正是 ADR-0019 持久化挂起要解决的场景:暂停要能跨进程存活。存活了,但它的渲染契约没跟上。

复现

packages/qa/dogfood/test/flow-durable-suspend.dogfood.test.ts(#4502 引入)最初就是这样写的,在这一步红:

AssertionError: screen not rehydrated:
{"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"No pending screen for run","httpStatus":404}}

修复方向(需要决策)

直接的修法是让它跟 resume 一样能读存储,但那要把方法变成异步,而它是公开契约的一部分:

// packages/spec/src/contracts/automation-service.ts
getSuspendedScreen?(runId: string): ScreenSpec | null;

改成 Promise<ScreenSpec | null> 是破坏性变更,需要同时改 packages/runtime/src/domains/automation.ts 里的路由调用点和任何其它消费方。所以这条没有顺手修,留给能拍板协议变更的人。

另一种不破坏签名的思路是在 resume 之外增加一个异步的 loadSuspendedScreen(runId),让路由改用它,把同步版本留作热路径快查——代价是两个名字做一件事。

参考

Activity

  1. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    决策请求(PM 循环第 1 轮分诊):本 issue 的修复方向涉及公开契约变更,按规程不派发、请你拍板。已打 needs-user-decision 标签。

    两个候选方案

    方案 A:把契约方法改为异步 —— getSuspendedScreen(runId): Promise<ScreenSpec | null>,同步改 packages/spec/src/contracts/automation-service.ts、packages/runtime/src/domains/automation.ts 路由调用点及其他消费方。破坏性变更。

    方案 B:新增异步孪生方法 —— 保留同步版作热路径快查,加 loadSuspendedScreen(runId): Promise<…>,路由改用异步版。签名不破坏,代价是两个名字做一件事。

    两轴分析

    ① 项目长远合理性

    • ADR-0019 已把挂起做成持久化的,「取挂起 screen」本质上就是一个可能触达存储的操作——同步签名是对这个本质的误报。方案 A 让契约恢复诚实。
    • 方案 B 是典型的 workaround(Prime Directive [WIP] Fix error in step four of the action run #5):同步版会永久留在契约里,每个新消费者都要「知道」该用哪个;两个名字一件事,与「一个契约一个来源」相悖。破坏性变更的迁移成本是有界的——这是内部契约,消费方可枚举,v5 本来就在做破坏性收敛。

    ② 防 AI 写代码犯错

    • 同步版正是 AI 最容易踩的那类陷阱:调用它在热进程里完全正常,只在重启后 404,常规测试根本照不出来——"looks right, works in dev, fails in production"。方案 A 把陷阱从契约里删除;方案 B 只是把陷阱文档化,而依赖「AI 记得读文档」正是我们不信任的东西。
    • declared = enforced:契约声明的行为应当就是运行时兑现的行为。A 达成;B 留下一个声明上看不出缺陷的方法。

    建议

    方案 A,按契约变更走:spec 契约改异步 → 路由与消费方同步迁移 → 一个 changeset 记录破坏性变更。若你担心一步到位的波及面,可以 A 的分步变体:先加异步版并把所有内部调用迁过去,同一个版本窗口内删除同步版(要有明确的删除排期,不能让 B 的状态常驻)。

    拍板后请回复评论(选 A / A 分步 / B / 其他),摘掉 needs-user-decision 或直接告诉我,即可进入下一轮派发。


    Generated by Claude Code

  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    同意方案A

  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    维护者已拍板:方案 A(契约方法改异步,破坏性变更)。已摘除 needs-user-decision,转为可派发实现任务。

    排期说明:列为下一轮首位而非本轮立即派发——本轮已追加派发 #4525(spec 建模 DecisionOutputDef.required),两者都会改动 packages/spec 的公共表面并重新生成同一份 api-surface 基线,同批并行必然在生成产物上撞车;按批次独立性规则串行,#4525 的 PR 进入合并流程后即派发本条。


    Generated by Claude Code

  4. self-assigned this
    on Aug 2, 2026
  5. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    认领:PM 循环第 2 轮(维护者已拍板方案 A:契约方法改异步)
    分支:claude/issue-4515-suspended-screen-async
    Worktree:objectstack-issue-4515
    前置条件已满足(#4525 → PR #4561 已进入合并流程,且实测为纯测试改动、与本任务无生成基线冲突)。如另有会话已在做本 issue,以更早的认领评论为准,本认领让行。


    Generated by Claude Code

  6. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 验收:ACCEPT → PR #4566(转正并武装 merge-when-ready,检查全绿自动入队)。

    按你拍板的方案 A 落地:契约 getSuspendedScreen 改异步、无同步残留;引擎复用 resume 的同一个 loadSuspendedRun(一个 loader 两个调用方,零复制);全部消费方同批迁移。语义边界保持:不存在/已恢复/非 screen 挂起仍是 null(404 保持 404),存储故障降级 null 而 hasSuspendedRun 保持严格抛错。dogfood 层补上真实冷启动的 screen 重取断言(#4564 合入后可测):stop() → 冷启动 → GET …/screen 200 且字段契约完整、重取不消费挂起、run 消费后 screen 随之 404。回滚验证两层齐备,正是 issue 记录的那个 404。changeset:minor + 显式 BREAKING + FROM→TO 迁移。


    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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions