Skip to content

Commit 9dd022a

Browse files
claude[bot]claude
andauthored
fix(service-automation): keep a dying loop's completed body steps in the run log (#14185)
A `loop` that died mid-sweep discarded its body's `childSteps` wholesale: the engine splices them into the run's step list only after a SUCCESSFUL node result, and a loop whose body throws never produces a result at all. A run that genuinely performed 3 flag writes and 2 notifies before dying reported `{selected: 5, acted: 0}` and kept no step record of writes already committed. The danger is the direction, not the arithmetic: `acted: 0` on a failed sweep reads as "nothing happened, safe to re-run", which for a non-idempotent body invites double-execution. A dying container now carries its completed body steps out on the thrown error -- a non-enumerable symbol brand, the idiom #3863 already uses to mark un-routable guard refusals on this same throw path -- and the engine's catch path folds them into the run log where the success path splices `childSteps`. The error is rethrown unchanged, so failure propagation, `fault`-edge routing, `$error` and the run's reported error are all untouched. Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs Co-authored-by: Claude <noreply@anthropic.com>
1 parent 67192ce commit 9dd022a

5 files changed

Lines changed: 599 additions & 12 deletions

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
---
4+
5+
fix(service-automation): a dying `loop` no longer discards the record of the writes it already made (#13803)
6+
7+
A `loop` that died mid-sweep reported `acted: 0` for a run that had genuinely
8+
written rows. The engine splices a container's `childSteps` into the run log
9+
only after a **successful** node result, and a `loop` whose body throws never
10+
produces a result — the throw unwinds past the splice, taking the accumulated
11+
body steps with the stack frame. A five-row sweep failing on the third row had
12+
committed three flag writes and two notifies, and reported
13+
`{ selected: 5, acted: 0 }` with no step record of any of them.
14+
15+
The direction is what made it a bug rather than a miscount. `acted: 0` on a
16+
failed sweep reads as "nothing happened, safe to re-run", so for a
17+
non-idempotent body — notifications, counters, external calls — the summary
18+
invited double-execution of writes that had already landed. The run summary is
19+
the platform's own honesty instrument (#4354); on this path it was wrong in the
20+
one direction that causes harm.
21+
22+
A dying container now carries its completed body steps out on the thrown error
23+
(a non-enumerable symbol brand, the same idiom #3863 uses to mark un-routable
24+
guard refusals on this identical throw path), and the engine's catch path folds
25+
them into the run log in the same position the success path splices
26+
`childSteps` — behind the container's own step, ahead of any `fault` handler's.
27+
The same five-row sweep now reports `acted: 5` and keeps `loop-body` steps for
28+
iterations 0, 1 and 2, with the failure attributed to iteration 2.
29+
30+
**Record-only: failure propagation is untouched.** The error is rethrown with
31+
its identity, message and guard-refusal marking intact, so which failures a
32+
`fault` edge routes, what `$error` holds, what the run reports as its error and
33+
the `EXECUTION_ERROR` code on the container's own step are all exactly what
34+
they were. The alternative shape — having `loop` swallow the failure and return
35+
`{ success: false }` — was rejected for precisely this reason: it would have
36+
made a guard refusal raised inside a loop body routable by a `fault` edge on
37+
the loop, the one-edge switch #3863 exists to prevent.
38+
39+
A sweep that fails before writing anything still reports `acted: 0`, because
40+
there that is the honest answer; an all-succeeding sweep and the
41+
`try_catch`-contained path are byte-identical to before.

0 commit comments

Comments
 (0)