You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
service-automation: a PAUSING map inside a contained region leaves its progress state behind — later loop iterations skip items and the exhausted map returns success having run nothing #15646
Found while implementing #15616 (PR branch claude/issue-15616-map-loop-iteration-state). Same variable, different arm — and #15616's fix deliberately does not close it. Filed by the os-dev seat; domain:*, type and priority are triage's.
This card is about the second arm. runRegion converts a durable pause inside a structured region into an error (durable pause inside a structured region ... is not supported) — but the executor has already written its progress state into the enclosing scope by then. The pause never happens, so nothing ever consumes that state; it is pure residue. If the region's error is contained by a try_catch, the next entry to the same map node reads the residue as progress.
Measured
On the real AutomationEngine, loop { body: [ try_catch { try: [ map ] , catch } ] }, 3 iterations x 2 items, per-item child flow pausing on its first node:
ran = [] (not one item's subflow ever completed)
caughtCount = 2 (three iterations, only TWO reached the catch region)
stateSeen = [ { started: 2, results: [] }, { started: 2, results: [] } ]
run = success
summary.failed = 0
Read it iteration by iteration:
iteration 1 — map starts item 0, it pauses, the region converts that to an error, try_catch contains it. Residue: started = 1.
iteration 3 — map re-enters, reads started = 2 === collection.length, concludes there is nothing to start and returns success. It ran nothing and it did not even fail.
So the tail of the sweep goes silently green, which is #15616's symptom reached through the other arm.
#15616's fix is a lifetime correction on the terminal path and it is complete for what that card measured (a synchronously-completing map in a loop body). This path never reaches a terminal return at all — the executor's last act is the suspend write — so no spelling of the terminal delete can clear it. Closing this one means deciding who owns cleanup when a suspend is refused, and the honest options are engine-shaped rather than map-shaped:
A. runRegion clears what the refused node wrote. Correct in principle, but the engine has no per-executor notion of "state this node wrote", so it needs one — a rollback/scope-ownership seam, not a patch.
C. Declare the containment illegal at authoring time and refuse the flow at parse/validate, so the shape never runs.
⇒ Needs a ruling on which layer owns it. Not guessed here.
Adjacent, measured, NOT filed separately
Note summary.failed = 0 above, with two contained failures in that run. The errors are thrown by runRegion itself rather than by a node executor, so no node step is marked failure, and failed — a fold of nodes[].failures — sums to zero. That is the same tension #15617 already carries (failed declared as a node fold versus the summary declared to answer "what did this run cause"), so it is recorded here as a second data point for that card rather than filed as a duplicate. #15617 remains open and is not addressed by this card or by #15616.
Refs: #15616 (the card this was found under) - #15617 (the failed fold question this run is a data point for).
os-decision-facets
① 项目长远合理性:A 缩小特例 —— 它修的是「suspend 被拒时谁负责清理」这一整类,任何未来在 suspend 前写状态的执行器都受益;B 按卡片自身测量只覆盖一半形状,是特例增生;C 不新增引擎契约,只把一个运行时早已存在的拒绝前移到编写期。
② 实际业务拉动:今天撞上的是任何写出 loop → try_catch → map(子流程会暂停)的作者。实测 ran = [] 且 run = success、summary.failed = 0,零信号。三个触发要素都是普通编排件,⛔ 不是边角构造。
③ 防 AI 犯错:C 最强 —— 形状在编写期即不可声明,属「响亮拒绝优于静默容忍」;B 最弱 —— 响亮但只覆盖一半,AI 写 try_catch 变体时照样静默、却以为自己受保护;A 对作者不可见。
④ 创业阶段不扩散:C 删的是一个运行时从不兑现的可写形状,属 remove 而非 declare-and-maintain;A 新增引擎机制(rollback / scope-ownership 接缝),扩散最大。
Found while implementing #15616 (PR branch
claude/issue-15616-map-loop-iteration-state). Same variable, different arm — and #15616's fix deliberately does not close it. Filed by theos-devseat;domain:*, type and priority are triage's.What #15616 fixed, and what this is
mapkeeps its progress through a collection innodeId.$mapState. It writes that key in two places:mapnode inside aloopbody runs its collection ONCE — iterations 2..n do nothing, reportsuccess, and the run completes green #15616 turns into adelete, because the state was outliving the node's own execution and being read back as progress by the next entry to it;suspend: true— this one is load-bearing and service-automation: amapnode inside aloopbody runs its collection ONCE — iterations 2..n do nothing, reportsuccess, and the run completes green #15616 leaves it exactly as it is. It is the whole durable-pause mechanism:resumeInternalrebuilds the variable scope from the snapshot taken at that suspend, so it is the only write to the key a resume can ever read.This card is about the second arm.
runRegionconverts a durable pause inside a structured region into an error (durable pause inside a structured region ... is not supported) — but the executor has already written its progress state into the enclosing scope by then. The pause never happens, so nothing ever consumes that state; it is pure residue. If the region's error is contained by atry_catch, the next entry to the same map node reads the residue as progress.Measured
On the real
AutomationEngine,loop { body: [ try_catch { try: [ map ] , catch } ] }, 3 iterations x 2 items, per-item child flow pausing on its first node:Read it iteration by iteration:
try_catchcontains it. Residue:started = 1.started = 1, skips item 0, starts item 1, pauses, contained. Residue:started = 2.started = 2 === collection.length, concludes there is nothing to start and returnssuccess. It ran nothing and it did not even fail.So the tail of the sweep goes silently green, which is #15616's symptom reached through the other arm.
Why it was not folded into #15616
#15616's fix is a lifetime correction on the terminal path and it is complete for what that card measured (a synchronously-completing map in a loop body). This path never reaches a terminal return at all — the executor's last act is the suspend write — so no spelling of the terminal delete can clear it. Closing this one means deciding who owns cleanup when a suspend is refused, and the honest options are engine-shaped rather than map-shaped:
runRegionclears what the refused node wrote. Correct in principle, but the engine has no per-executor notion of "state this node wrote", so it needs one — a rollback/scope-ownership seam, not a patch.maprefuses to start a pausing item when it is inside a region, i.e. fails up front instead of writing state and being refused afterwards. Cheaper and louder, butmapcan only detect aloopbody today (viacurrentLoopIteration), not atry_catchorparallelregion, so it would close part of the shape and leave the rest — the thing service-automation: amapnode inside aloopbody runs its collection ONCE — iterations 2..n do nothing, reportsuccess, and the run completes green #15616's own scope note warns against.⇒ Needs a ruling on which layer owns it. Not guessed here.
Adjacent, measured, NOT filed separately
Note
summary.failed = 0above, with two contained failures in that run. The errors are thrown byrunRegionitself rather than by a node executor, so no node step is markedfailure, andfailed— a fold ofnodes[].failures— sums to zero. That is the same tension #15617 already carries (faileddeclared as a node fold versus the summary declared to answer "what did this run cause"), so it is recorded here as a second data point for that card rather than filed as a duplicate. #15617 remains open and is not addressed by this card or by #15616.Refs: #15616 (the card this was found under) - #15617 (the
failedfold question this run is a data point for).os-decision-facets
① 项目长远合理性:A 缩小特例 —— 它修的是「suspend 被拒时谁负责清理」这一整类,任何未来在 suspend 前写状态的执行器都受益;B 按卡片自身测量只覆盖一半形状,是特例增生;C 不新增引擎契约,只把一个运行时早已存在的拒绝前移到编写期。
② 实际业务拉动:今天撞上的是任何写出
loop → try_catch → map(子流程会暂停)的作者。实测ran = []且run = success、summary.failed = 0,零信号。三个触发要素都是普通编排件,⛔ 不是边角构造。③ 防 AI 犯错:C 最强 —— 形状在编写期即不可声明,属「响亮拒绝优于静默容忍」;B 最弱 —— 响亮但只覆盖一半,AI 写
try_catch变体时照样静默、却以为自己受保护;A 对作者不可见。④ 创业阶段不扩散:C 删的是一个运行时从不兑现的可写形状,属 remove 而非 declare-and-maintain;A 新增引擎机制(rollback / scope-ownership 接缝),扩散最大。
Prior rulings read: success,service-automation,pausing,inside,contained,region,leaves,progress,state,behind,later,loop (+6 more) → 82 hits; ADR-0094 D2, ADR-0020 D3, ADR-0067 D2, ADR-0076 D1, ADR-0076 D11, ADR-0119 D1, ADR-0119 D4, ADR-0128 D4, ADR-0130 D1 — ⭐ 本席逐个读了与本题最近的两个,⛔ 不是只抄工具输出:ADR-0119 是事务 / 原子批(
transaction进IObjectQLEngine、batchData.atomic),与本题无关,属refused/success一类通用词的假阳性;ADR-0019(approval-as-flow-node) 管 approval 作为 durable-pause 节点与resume的拒绝语义,⛔ 未裁「区域内 suspend 被拒后的残留归谁清理」。⇒ 无既有裁决答本题,本卡是决策而非执行。推荐:C 先行,A 另立卡。自检「只看①选 A;②③④ 是否翻转:③ 强烈翻向 C(半修比不修更危险),④ 翻向 C(A 扩引擎面),② 两者皆能止住静默、不分胜负 ⇒ 翻转成立,落在 C」。
置信缺口:C 的可行性未验。 本席只测到
canonicalize-stored-flow与parse-config已在静态遍历嵌套 body,⛔ 未测到存在一个会拒绝 flow 的 validate 层。C 若不可静态判,推荐退回 A;这一条必须由实施者先验再动手。Generated by Claude Code