Skip to content

fix(workflow): enforce composite scopes, recovery and state budgets - #179

Merged
EricWvi merged 44 commits into
ora-space:mainfrom
ElephantGit:fix/660-workflow-contract-guards
Oct 26, 2026
Merged

EricWvi merged 44 commits into
ora-space:mainfrom
ElephantGit:fix/660-workflow-contract-guards

Conversation

@ElephantGit

@ElephantGit ElephantGit commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Composite workflows could publish invalid regions, read outside their allowed scope, lose failed rounds, and exceed their serialized state budget. This replacement preserves #177 history and adds scope guards, container failure settlement, loopExit priority, recovery validation, iteration ledgers, unique body attempts, and capacity rollback. The editor exposes named loop outputs and carried variables, preserves imported container layout, and explains publication failures.

Timed-out body sessions retain their attempt as a dispatch barrier and request termination. A terminal result permits iteration continuation while preserving the turn-timeout diagnosis. If end confirmation expires, the node records session_end_deadline_exceeded and the run records session end deadline exceeded, retains the failed round, and releases its Workspace without dispatching another session. The same behavior is tested for loop and iteration.

Capacity checks accept identical committed state within the hard limits, so a failure already admitted into the reserve is not rejected during rescheduling. New failed iteration ledger entries can use the failure reserve even under the continue strategy. Historical failed members and ledger entries do not enlarge subsequent ordinary writes; adding even one byte above the ordinary budget is rejected when no new failure is being recorded.

Output compatibility: strict bindings apply to all published snapshots, including new runs from old snapshots and recovered in-flight runs. No bindings produce an empty object; assigned bindings produce named outputs; missing values fail with output_value_unset and invalid paths fail with output_invalid_path. Upgrading can produce new failures or additional named outputs. No snapshot migration or execution version is added; committed terminal results are not recomputed. Top-level condition declaration compatibility is preserved.

Validation for head 0500f72cc77f9ff1861dd7785d5591f07623cecd:

  • Linux / Go 1.27.1 / PostgreSQL 17.11: complete PostgreSQL-required go test ./... and go test -race ./... pass; format checks pass; golangci-lint reports 0 issues.
  • PostgreSQL regressions cover failure settlement near 8 MiB for iteration fail/continue and loop, identical-state replay, ordinary growth rejection, hard-limit rejection, historical failed ledger entries, and loop timeout confirmation/expiry with archive and Workspace cleanup assertions.
  • Running the new regressions against the previous budget/drain implementations reproduces all three capacity failures and the loop end-confirmation diagnosis mismatch.
  • Backend CI and Frontend CI pass for this head.
  • TestWorkflowNearLimitCASMeasurement remains the reproducible near-limit CAS measurement entry point.

ADR status: these three remain approved. This PR supplies implementation and direct backend evidence; complete UI and real-stack acceptance remain outstanding:

  • decisions/cloud/workflow/20261025-variable-aggregator-execution.md
  • decisions/cloud/workflow/20261025-loop-and-loop-exit-execution.md
  • decisions/cloud/workflow/20261025-iteration-foreach-execution.md

Remaining UI and real-stack acceptance is tracked by ora-space/circus#659. Separate specs implementation runs under ora-space/circus#658 will reconcile evidence after product review and acceptance. Session end-confirmation diagnostics follow the implemented decisions/cloud/workflow/20261019-workflow-session-end-after-initial-turn.md D4.

Refs ora-space/circus#660

wanglongan587 and others added 30 commits October 25, 2026 11:53

@creegon2 creegon2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查对象:head d52c5f14532ef6e1ca1289f4d21c23584bc572fc,对照 specs main 上三份 approved ADR(aggregator / loop / iteration)及其核心用例,以及 #630、#657、#660 里 owner 的结论。只读源码和测试,没有在本机跑 Go 和前端测试,测试结果以你在 #660 贴的 Linux + PostgreSQL 记录和 CI 为准。

#660 首轮的九项在代码层都已修复,前驱规则和区域隔离、iteration 门禁接线、iteration 恢复校验、1 MiB / 8 MiB 预算都做得扎实。但还有三个阻塞问题,以及一批应修问题,所以先 Request changes。

阻塞

B1. loopExit 不会被优先派发,被选中后也不会停掉本轮其余成员(internal/core/workflow_loop_run.go:330-358、:589-596)

body 的就绪节点按文档顺序取,对 loopExit 没有任何优先处理。workflowLoopExitSucceeded 要等整个 body 跑完(hasPending / workflowBodyInFlight 都结束)才检查。

失败场景:body 为 entry → C,C 的 yes 分支同时连到 exit(loopExit) 和 W(agent),C 选中 yes。不管 W 和 exit 在文档里谁先谁后,W 都会被派发并真实启动会话,不会被记为 inactive。

这违反了 loop ADR D4("调度器优先派发它;这一轮剩下的未执行成员记为既有的 inactive,不再执行")、不变量 4,以及用例"选中才早退…剩余成员为 inactive 且不再派发"。TestWorkflowAdvanceLoopExitsEarly 的 body 只有 entry→exit,测不出这个问题。

B2. body 里的 agent 有多条失败路径绕过容器,直接把整个运行结束为 failed(internal/core/workflow_run_session.go:118、:137、:152、:194,以及 :470-474 的 failWorkflowAgentNode)

只有 commitWorkflowAgentTerminal(:285 起)在 node.Container != "" 时把失败交回容器处理。以下路径都对 body 成员直接调用 finishWorkflowRun(..., "failed", ...):

  • Node 不可达;
  • turn / start / end deadline;
  • workspace_unavailable、executor_unconfigured、插件缺失等。

失败场景:iteration 用 continue 策略,第 3 轮的 body agent 超过 turn deadline,结果是:

  • 整个运行直接失败,没有被 continue 吸收;
  • 第 3 轮没有写账本条目,也没有归档;
  • iteration 节点在已终态的运行里仍是 running。

loop 同样会留下一个 running 的 Loop。这违反了 iteration D4("两种策略都先把失败轮写入账本并归档")、D6(continue 吸收区域内失败),也违反 loop D1(节点状态与终态一致)。#660 里写的"body agent 失败交回容器处理"只覆盖了会话正常返回结果这一条路径。

F1. 前端变量目录不认识 loop 的作用域(frontend/src/features/workflows/runtime/variable-catalog.ts:202 及 :142-163)

nodeCatalogEntries 给所有非 condition 节点都加了一个 output 根,loop 也不例外。后端对 loop 只声明 carried 变量名和 loopConfig.outputs[].name,不声明 output。#630 也明确说过不照抄 desktop 那个从不写入的 {loop}.output。

失败场景:

  • 新建一个 loop(默认输出 result),连到 Output 节点,再给 Output 加绑定。下拉里只有"循环 1 · output",没有 result。WorkflowCatalogSelector 不能手输,作者在界面上没法把 Output 绑到 loop 的结果。选了 output 再发布,会被 output_selector_undeclared 拒绝;aggregator 同理,报 aggregator_selector_undeclared。
  • loop body 内节点的目录只按边取祖先,owningIterationOf 只认 iteration。所以 loop 的 carried 根和 loop 的外层祖先都不在选项里,而 ADR D1/D3 允许读它们。

应修

后端

  • S1. loop 恢复时不做一致性校验,loop_invalid_round 从来不会产生(workflow_loop_run.go:57-76;workflow_scheduler.go:580-590 只校验 iteration)。
    • 持久化的 round 缺失或为 0、而 completed 里已有 2 轮时,恢复后会从第 1 轮重跑:body agent 被重新派发,并追加一条重复归档。
    • round 超过 maxIterations 时,succeed 策略会在越界之后报成功。
    • 违反 loop D6、不变量 1 和 6。请参照 iteration 的 workflow_iteration_recovery.go 补上。
  • S2. 64 KiB 失败预留只在容器本身已失败或已取消时才生效(workflow_state_budget.go:116-125、:183-193、:199-220)。
    • 失败场景:start → loop(已 succeeded,状态约 8 MiB−64 KiB−100B) → agentA。agentA 失败,带回 1 KB 错误文本,写入超过普通上限,于是:
      • 回滚到上一版,agentA 的失败记录丢掉,仍停在 running;
      • 已经 succeeded 的 Loop 被改成 failed,loop.reason 却还是 condition_satisfied。
    • 如果同样位置发生的是用户取消,运行最终是 failed 而不是 cancelled。
    • 违反 loop D1"取消、失败记录与旧归档不因为体积检查被丢弃",以及用例"接近预算时仍能提交失败与取消记录"。TestWorkflowBudgetCASPreservesCommittedState 的上一版状态很小,没有走到预留空间。
  • S3. 两处没有用有效容器 data.containerId ?? parentId:workflow_collaboration.go:843(workflowStartNode)和 workflow_graph.go:328(validateWorkflowReservedFields)只看 containerId。
    • 失败场景:loop body 的 start 只带 parentId,又在节点数组里排在外层 start 前面。启动表单和 kickoff 提示词会取自 body 的 start,保留字段校验也会因此误拒。
  • S4. 部分 loop 门禁错误码与 D2 表不一致(workflow_loop_graph.go 第 332/336、349、354、423/427、440 行)。
    • 代码报的是 loop_variable_duplicate、loop_variable_type_unsupported、loop_variables_not_a_list、loop_output_duplicate、loop_outputs_not_a_list。
    • D2 表里只有 loop_variable_invalid 和 loop_output_invalid。
    • 要么改代码,要么先修订 ADR。
  • S5. 以下已实现的义务没有直接测试,请在本轮补上,落地时核心用例要用到:
    • loop:
      • succeed 耗尽导出最后一轮;
      • 最后一轮命中 until 时优先于耗尽;
      • loop_exit_output_unset;
      • body 内有 agent 的多轮数据库测试(重试按轮计次、跨轮 attempt);
      • loop body 内 aggregator 的作用域。
    • sys.* / globals 豁免前驱校验但仍须声明:aggregator 和 iteration 源都要测。
    • iteration:
      • condition 未选中分支记 inactive 并归档;
      • 收集目标本轮未运行;
      • 迭代源不是数组;
      • continue 策略下超限仍零轮失败;
      • 轮中有在途会话时取消。
    • aggregator:
      • aggregator_invalid_config;
      • false / 0 / "" / [] / {} 这些值也算已赋值(现在只测了 null)。

前端和文档

  • F2. 旧图里没标端口的 condition 连线会画在 IF 上,运行时却走 ELSE。
    • React Flow 遇到缺 sourceHandle 的边,会取第一个 source 端口(第一个 case),而后端 workflow_scheduler.go:475-478 把空 handle 当作 else。现在只在新建连线时把同源旧边改写成 else(use-workflow-draft.ts:352-362)。
    • 失败场景:打开一张旧图,条件→A 没有 sourceHandle。画布显示"IF → A",实际只有所有 case 都不命中才会走到 A。
    • 建议在画布投影层把空 handle 显示为 else,不写回草稿。
  • F3. 从调色板新建的 loop、用按钮加的 loopExit,在本次编辑会话里不属于 loop,保存重载后位置会跳。
    • createWorkflowLoopGroup(runtime/node-factory.ts:190-219)只写 data.containerId、不写 parentId,坐标却是相对坐标;desktop 的同一个工厂会同时写 parentId。addLoopExit(use-workflow-draft.ts:340)写的是绝对坐标 loop.x+320,也不写 parentId。
    • 失败场景:把 loop 放在 (600,300)。它的成员出现在画布左上角 (40,145);重载后才被移进 loop,loopExit 则跑到 2·loop.x+320。
  • F4. loop 检查器所有选择框共用同一组选项(workflow-loop-fields.tsx:588-632)。
    • initial 选"变量"时,可以选到 body 成员和 carried 根,发布时会被 loop_initial_invalid 拒绝(D3:initial 在进入 loop 时按外层根求值)。
    • 成员判定只看 containerId(:610),没有回落到 parentId。
    • 选项里还有 loopExit 的 output,后端不声明这个根。
  • F5. 发布门禁新增的错误码,在界面上都只显示通用提示(workflow-publish-error.tsx:50)。
    • 涉及 aggregator_selector_not_upstream、loop_*、iteration_*、output_*,统一显示成"请检查此处的节点、连线或变量配置"。
    • #630 要求非法配置返回节点、字段和原因,至少应在界面上显示具体原因。
    • iteration 的 maxIterations 输入框也没有 max=100。
  • F6. 文档把 junction 写成了可以发布(docs/workflows/workflows.md:26、docs/workflows/workflows.en.md:35)。代码里 workflowExecutableNodeKinds 不含 junction,#630 也要求它继续被门禁拒绝。
  • F7. loop 检查器底部那句固定说明(workflow-loop-fields.tsx:101-106:"每轮将 Agent 输出反馈为…导出为 result")在作者改过 feedback、until 或 outputs 之后就不对了。

需要确认的契约问题(请在 #660 / #657 说明,必要时请 owner 定)

  1. workflow_graph.go:450/452 把外层 condition 的 selector 从"全图已声明的根"收紧成了"传递前驱的根"。三份 ADR 只对 body 节点、aggregator 候选和 iteration 源这样要求。这样做会让以前能发布的普通 DAG 草稿(condition 引用了非前驱节点)无法再发布,属于契约变化。
  2. loop 状态里多了一个 loop.reason 字段,loop ADR D1 的 JSON 形状里没有它。owner 要求 node_states 的形状严格以 D1 为准:要么去掉这个字段,要么先在 #657 说明并修订 ADR。
  3. workflow_scheduler.go:844 让 output 节点执行 data.outputs 绑定,缺值时运行失败。存量快照里如果已经带着这个字段(比如从 desktop 导入后发布过),新建运行的行为会变。请确认存量里有没有这种快照。
  4. 导入时 runtime/graph-import.ts:299-312 会丢掉 parentId、initialWidth、initialHeight。desktop 的 iteration 图导入后,区域成员会变成顶层节点,发布时报 iteration_body_empty。这是已有代码的问题,不是本 PR 引入的,但它挡住了 #630 的 wire 对等验收。请说明由本 PR 修,还是交给 #659。

建议(不阻塞)

  • iteration 收集目标是区域成员、但根没有声明时,报的是 iteration_collect_unsupported。按 D2 应报 iteration_collect_outside_region(workflow_iteration_graph.go:37-38)。
  • iteration 区域里允许放 start(workflow_loop_graph.go:181-186)。但运行时把它当作 Start,门禁却把所有入度为 0 的成员都当作入口,两边口径不一致。
  • loop 的 carried 变量和某个输出同名时,workflowVisibleRoots 会把两个都删掉,导致误拒(workflow_loop_graph.go:110-119)。
  • 每推进一步都要对整份 node_states 做一次 JSON 编码。100 轮的纯同步区域、状态接近 7 MiB 时,一个事务里要做数百次约 7 MiB 的编码。现在的近上限测量只测了单次 CAS,没有覆盖这种情况。
  • 前端零碎问题:
    • loop 检查器里三个控件的 id 漏了行号(:307、:356、:482),有多行时 DOM id 会重复;
    • 画布不拦截跨容器连线;
    • 改了 carried 变量的类型后,常量初值不会按新类型重新解析;
    • node-catalog.test.ts:46-48 改成用被测函数自己算期望值,等于自己验证自己;
    • aggregator 面板的注释(workflow-aggregator-fields.tsx:20-24)已经过时;
    • docs 的"相关决策"没有链接三份新 ADR。

删除的代码行都检查过:没有发现被删除或削弱的既有校验,只有 node-catalog.test.ts 那一条断言变弱了。

改完 B1、B2、F1,以及应修的各项后,请在 #660 重新请我复审。合并仍由 owner 决定。

@creegon2 creegon2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

第二轮复审,审的是 head 4150c44 相对 d52c5f1 的改动,只读了代码和测试。正式结论等你补完 #681 的回归、CI 全绿、请整体复审时再给;先把这轮查到的发出来,可以一起改。

第一轮的各项:B1、B2、F1、S1、S3–S5、F2–F7,以及四个契约问题,在代码层都已修复,基本都有直接测试。B1 的 TestWorkflowLoopExitPreemptsReadyAgent 覆盖了两种文档顺序,旧代码会让这个测试失败,可以证明修复有效。F5 漏了两个码:output_bindings_not_a_list 和 output_selector_unsupported 还没进对照表,界面仍显示通用提示。

本轮改动带来的两个新问题(应修)

  1. iteration 用 continue 吸收 turn 或 end 超时之后,Node 上那个超时的会话没有被结束,下一轮会在同一个 Workspace 里再启动一个会话。
    • 位置:workflow_run_session.go:137、:195、:478-484。
    • 原因:
      • 以前这两种超时会直接 finishWorkflowRun,运行一结束就释放 Workspace,旧会话也随之终止。
      • 现在 body 成员只提交 running。turn 超时这条路径没有发 end_session;end 超时这条路径的 end_session 始终没有得到确认。
      • 每个运行只限制一个"还没注册的" agent_session 工作项(:10),已经注册、但还没出结果的旧执行挡不住新派发。
    • 失败场景:iteration(continue) 有 2 项,第 1 轮的 body Agent 超过 turn 截止时间。iteration 记一条失败账本后进入第 2 轮,新会话启动时,第 1 轮的会话还在同一个 Workspace 里运行、写文件,违反了一个运行同一时间只能执行一个会话的串行规则。TestWorkflowBodyOperationalFailuresReachContainer 只有 1 项,测不出这个问题。
    • 建议:吸收这类失败前,先给旧的 attempt 发 end_session{cancelled},等它确认或超过确认窗口,再进入下一轮。
    • 我没有核实 Node 侧是否禁止同一 Workspace 同时跑多个会话。如果禁止,结果会变成下一轮启动失败,同样不对。
  2. 失败预留会在正常推进时被提前用掉,S2 的问题换了个场景又出现了。
    • 位置:workflow_state_budget.go:33-38。
    • 原因:现在只要状态表里有任何 failed 或 cancelled 节点,就允许用满 8 MiB。iteration 用 continue、最后一轮失败时,失败成员会留在状态表里(workflow_iteration_run.go:251-254),之后这个运行的每一次正常提交都能用掉那 64 KiB 预留。
    • 失败场景:start → iteration(continue,最后一项失败) → agentB → agentC。
      1. agentB 的输出把状态推到 8 MiB−200B,因为状态表里有那个失败成员,这次提交被允许。
      2. agentC 失败,带回 1 KB 错误文本,超过 8 MiB。
      3. 结果:回滚到上一版,agentC 的失败记录丢掉,已经成功的 iteration 被改成 failed / workflow_state_size_exceeded。
    • 建议:只在这次提交新增了 failed/cancelled,或者运行这次提交后进入 failed/cancelled 时,才放开预留;不要看历史状态。

建议(不阻塞)

  • 新加的 loop 输出或 carried 变量名字为空时,下游下拉里会出现 l. 这种空名选项(variable-catalog.ts:206-217)。请过滤掉,和 resolveLoopChoices 保持一致。
  • workflow-loop-fields.tsx:112-114:正常 loop 下会留一个带边框的空框。
  • graph-import.ts:305 原样保留 parentId,没有检查它指向的节点是否存在。建议导入时校验,或者丢弃悬空的 parentId。导入的测试只测了解析阶段会保留,没有测导出再导入。
  • B2 的 loop 路径没有直接测试。建议补一条:loop body 的 Agent turn 超时,断言 loop 为 failed、失败轮已归档、运行失败。
  • TestWorkflowIterationContinueRejectsInvalidSourceBeforeFirstRound 对两种输入都同时接受两个错误码,请按输入分别断言。

核对过、没有发现问题的地方:

  • loopExit 优先:exit 就绪时不会有在途成员;
  • 恢复校验:已结束的 loop、第 1 轮还没有归档、已取消的运行,这几种合法状态都不会被拒;
  • 取消和失败同时发生:最后以 cancelled 结束;
  • 没有容器的图:预算行为和以前一致;
  • 前端作用域:和后端 workflowScopeRoots 的规则一致,没有把合法选项去掉。

@creegon2 creegon2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

本次复审 head b684d9e。

#681 部分可以通过。 TestWorkflowHistoricalOutputStrictBindings 在真实 PostgreSQL 上覆盖了 5 类情况 × 新建和在途恢复两个阶段:

  • 恢复时用不同的 run input,验证读的是已提交的前驱值;
  • 验证不会泄漏部分成功的输出;
  • 验证不会改写冻结快照。

中英文文档和 PR 描述都写明了升级后的兼容性变化。CI 全绿。

仍需修改。 我在上一条审查意见里(head 4150c44,评论时间 07:40Z)提了两个新问题。本轮相对 4150c44 只改了 docs 和 workflow_output_compatibility_db_test.go,这两个问题都还在:

  1. 超时会话没有结束。 iteration 用 continue 吸收 turn 或 end 超时后,没有结束 Node 上那个超时的会话,下一轮会在同一个 Workspace 里再启动一个会话。涉及 workflow_run_session.go:137、:195、:478-484。
  2. 失败预留被提前占用。 只要状态表里有历史上的 failed 或 cancelled 成员,正常推进也会用掉 64 KiB 失败预留,之后真正的失败记录会被回滚丢掉,已经成功的容器也会被改成 failed。涉及 workflow_state_budget.go:33-38。

另外,F5 还漏了两个码没进对照表:output_bindings_not_a_list 和 output_selector_unsupported。

这两条的失败场景和修改建议都在上一条审查意见里。改完、补上直接测试并且 CI 全绿后,请再找我复审。

@creegon2 creegon2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这次审的是 head 0491ee9,相对 b684d9e 有三项修复。

三项修复本身都对,也都有能测出旧问题的直接测试。 两个新测试 TestWorkflowIterationTimeoutDrainsBeforeNextRound 和 TestWorkflowHistoricalFailureDoesNotUnlockReserve 在 b684d9e 上都会失败,在新 head 上都通过。我们还另外跑了几个场景,都符合预期:

  • 两项 iteration(continue),第 1 轮 turn 超时:确认之前调度多少次都停在第 0 轮,end_session 只有 1 条;确认后进入第 1 轮,attempt=2;一直收不到确认时运行失败、Workspace 被释放,不会启动第二个会话。
  • 等待确认期间用户取消:复用同一条命令,运行以 cancelled 结束,没有卡住。
  • 等待确认期间 Node 掉线:等满确认窗口后运行失败并释放 Workspace。
  • 旧 attempt 迟到的结果按 run/node/attempt 区分,不会算到新 attempt 上;顶层(不在容器里的)agent 的行为没有变。

应修:失败记录用预留空间提交之后,下一次调度开头的预算检查会把这份已提交的状态判为超限,容器真实的失败原因因此丢失。

位置:workflow_scheduler.go:665 的检查,和 workflow_state_budget.go:108-131 一起出问题。

过程:

  1. body 失败按容器分支提交,这次提交用了预留空间。
  2. 下一次调度一开始,:665 对这份没有任何变化的已提交状态又做一次预算检查。这时这个失败已经算历史失败,预留不再开放,rejectWorkflowStateOverflow 于是把运行判为失败。
  3. 这次检查发生在容器自己转为 failed 之前。容器转为 failed 本身也是一次新的失败转移,本来是可以用预留的。

我们在 PostgreSQL 上实际跑出来了。场景:iteration 只有一项,body 是 agent,状态距普通上限还剩 40 字节,会话以 interrupted 结束。

策略 b684d9e 0491ee9
fail 运行 failed,原因 interrupted 运行 failed,原因 workflow_state_size_exceeded(实际 8,323,107 字节,低于 8 MiB)
continue 运行 succeeded,failed_count=1 运行 failed,原因 workflow_state_size_exceeded

超时排空后的失败结算,以及 loop 的 body 失败,也会走到同一个检查。TestWorkflowHistoricalFailureDoesNotUnlockReserve 直接调用 settleWorkflowRunStates,之后没有再跑一次调度,所以覆盖不到这个问题。

建议::665 不要拒绝和已提交版本完全相同的状态,这份状态在提交时已经检查过了,只需要检查本次调度新写入的部分。另外补一个端到端测试:在状态接近上限时让 body 失败,再跑一次调度,断言运行的失败原因是 body 的原因。

建议(不阻塞)

  1. 原因字符串和 D4 不完全一致。

    • 超时后等确认,窗口用完时(workflow_session_drain.go:20-26),节点和运行的原因都写成 session_turn_deadline_exceeded;D4 规定这种情况节点原因是 session_end_deadline_exceeded。
    • 容器路径的运行错误(workflow_run_session.go:214)写的是 session_end_deadline_exceeded,顶层路径(:217)写的是 "session end deadline exceeded"。

    "失败并释放、不再派发"这部分行为符合 D4。请统一这几个字符串,或者在 PR 描述里写明超时原因优先于结束确认原因。

  2. loop 的 body turn 超时还没有测试。 确认成功、确认窗口用完这两种情况都没有覆盖,对应 workflow_session_drain.go:47-49 的 loop 分支。上一轮已经提过一次。

  3. 可选的提速。 Node 已经掉线时,drainWorkflowTimedOutSession 仍然要等满 5 分钟窗口才结束。可以参考 runWorkspaceNodeUnknown 提前判断。结果是对的,只是慢。

改完应修项、补上端到端测试并且 CI 全绿后,请再找我复审。

@creegon2 creegon2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

复审 head 0500f72(相对 0491ee9),Approve。

上一轮应修项已修

  • 重新调度时,同一版本、内容完全相同的已提交状态,普通预算不再拒绝(workflow_state_budget.go:115-120)。
  • 容器转为 failed,或者新追加的失败账本条目,算作新的失败转移,可以使用预留。
  • 局部 1 MiB 上限和 8 MiB 硬上限照常检查。内容只要有任何不同(同版本但多 1 字节),也照常检查。
  • 历史失败不会重新开放额度:只有本次新追加、状态为 failed 的账本条目才开放预留,之后成功轮次的条目不开放。
  • TestWorkflowReservedBodyFailureSurvivesRescheduling 真的跑了第二次调度,测的正是上一轮出问题的那条路径。iteration fail、continue 和 loop 三种策略都覆盖到了。

建议已处理

  • 确认窗口用完时,节点原因是 session_end_deadline_exceeded,运行原因是 session end deadline exceeded,容器路径和顶层路径一致。
  • 迟到的 cancelled 不会把超时改成用户取消。
  • loop 的超时等待补了确认成功和确认窗口用完两种测试。

已有检查和测试没有放松

diff 里测试都是新增断言,或者把历史场景改得更严,没有删除任何断言。

后续可以补的(不阻塞合并)

  1. 最后一轮失败时,最终输出复制可能用到预留。 continue 策略下最后一轮失败,同一次提交既追加失败账本,又把所有轮次的输出复制进 state.Output。预留对整次提交开放,所以这些普通的输出复制也可能用到那 64 KiB。多用的部分不超过 64 KiB,也不会超过硬上限。可以在文档里写明,或者把预留只限于账本条目。
  2. 两项以上的 continue,第一轮失败后状态超过普通上限时,下一轮的普通写入会以 workflow_state_size_exceeded 结束。 这符合"历史失败不扩大普通写入额度",但目前测试只有一项、文档也没写,建议补一个两项的测试并在文档写一句。
  3. finishWorkflowUndrainedSession 的 reason 参数只用在节点上,运行错误是写死的(workflow_session_drain.go:34、:52)。现在调用方都传同一个值,所以结果没问题;建议改名,或者让运行错误也从参数得出。

四轮审查的问题都已修复,CI(Backend 四个集成分片、packages、最终门禁和 Frontend)全部通过。合并由 owner 决定。三份 ADR 的 implemented 落地按 #658 在产品合并后另开运行,真实栈证据由 #659 承接。

@EricWvi
EricWvi merged commit ecf0d23 into ora-space:main Oct 26, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants