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).
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).