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
{{ message }}
Repository navigation
Commit 216b066
Browse filesBrowse the repository at this point in the historyBrowse files
fix(service-automation): a completed run is no longer answered failed — or re-executed under strategy: retry — by its own history write (#17565)
* fix(service-automation): guard the two initial-execution completion history writes (#16274)
`execute()` and `executeWithoutRetry()` called `recordLog({ status:
'completed' })` from inside the `try` whose `catch` exists for node failures,
so a throw out of a history write on a run that had already finished was
handled as though a node had thrown.
Measured on the unpatched tree, and sharper than the report assumed: the false
`failed` is handed to `retryExecution` by the strategy branch, whose loop reads
`result.success` and re-executes the whole flow — `1 + maxRetries` runs of every
node for one logical success, unattended, with one `failed` history row per
attempt. Controls: the same flow on healthy sinks runs the node once; a genuine
node failure runs it `1 + maxRetries` times.
Guarded at each call site in the shape the resume path already landed: report
the swallowed failure once at `error` with its consequence and fix, recompute
the summary with the same pure function. No `catch` arm's meaning is widened.
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
* chore(gates): teach check:durability-log-level the run-history seam (#16274)
AGENTS.md's rule for this gate: "it cannot discover a new seam, only stop
known ones from regressing; found a new one, add it to
DURABILITY_CRITICAL_CALLEES in the same PR that fixes it."
`recordLog` is that seam. Measured: the population goes 29 -> 33 catch seams,
the four newly covered are this card's two guards plus the two the resume path
already landed, all four are judged LOUD, and the gate stays green with zero
unrecognised constructs. The empty shrink-only baseline stays empty.
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
* chore(gates): carry `recordLog` into the swallow-family census copy (#16274)
`measure-durability-swallow-family.mjs` keeps a deliberate by-value copy of the
gate's `DURABILITY_CRITICAL_CALLEES` under `origin: 'gate-vocabulary'`, and its
`--self-test=gated` reddens on drift — which it did, naming `recordLog` as
"declared by the gate, missing from the copy". Its own remedy text: a name the
gate gained belongs in the copy.
check:swallow-census-controls: 21 copied gate-vocabulary names now match the
gate's declaration, exit 0.
Claude-Session: https://claude.ai/code/session_01ToDPcx9AESFubJkDiFMtKW
Co-authored-by: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
A run whose nodes all succeeded is no longer answered `failed` — or, under `errorHandling.strategy: 'retry'`, RE-EXECUTED — because its terminal run-history write threw (#16274)
6
+
7
+
`AutomationEngine.execute()` and `executeWithoutRetry()` each called `recordLog({ status: 'completed' })` from inside the `try` whose `catch` exists for **node** failures, so a throw out of a history write on a run that had already finished successfully was handled as though a node had thrown. This is the initial-execution half of the pattern fixed on the resume path in 17.4.0; that fix deliberately scoped these two sites out.
8
+
9
+
**The consequence was measured, and it is a double run, not just a mislabelled one.**`execute()`'s node-failure arm ends at the retry strategy branch, which hands the false `failed` result to the retry loop; the loop reads `result.success` and therefore re-enters `executeWithoutRetry()` — the whole flow, every node, again. Driven with `maxRetries: 2`: a flow whose node always succeeded ran it **three** times and wrote three `failed` rows, unattended, inside one `execute()` call, with the node's side effects repeated each time. Controls on the same instrument: the identical flow on healthy sinks runs the node once, and a genuine node failure runs it three times (retry working correctly).
10
+
11
+
**What can throw there is a host surface, not in-repo code** — which is why it could not be reproduced from inside the package and why the package owed the fix:
12
+
13
+
- the run-summary line `logger.info(line, meta)`, on by default (`runSummaryLog: 'info'`) and calling a **host-injected**`Logger`. This one needs no store at all.
14
+
-`store.recordTerminal(record)` throwing **synchronously**, before it returns a promise — the `void write.catch(...)` beneath that call only ever sees a returned promise's rejection. Both stores shipped in this package are `async` methods and cannot do it, but `SuspendedRunStore` is an exported interface whose `recordTerminal` is optional, so a host store is unconstrained. (A store returning a non-thenable escapes identically: `write.catch` is then itself a synchronous `TypeError`.)
15
+
16
+
On that second variant the old code did not even answer `failed`: the node-failure arm's own `recordLog({ status: 'failed' })` threw again out of the same store and escaped `execute()` entirely — a rejected promise where `AutomationResult` is declared.
17
+
18
+
What changes:
19
+
20
+
-**Each completion-path history write is guarded at its own call site**, restoring the invariant that call's own documentation states: a history write must never block or break the run that produced it. The caller is told the truth — `success: true`, no `status`, the flow's `successMessage`, and a `summary` recomputed by the same pure function `recordLog` runs first — the node runs exactly once, and one `completed` row is recorded rather than `1 + maxRetries``failed` ones.
21
+
-**The swallowed failure is reported once per run at `error`**, with the consequence and the fix in the first line: the run completed, its terminal history row never landed, nothing retries it, and the run must not be re-run. The thrown text rides the structured slot.
22
+
23
+
⛔ No `catch` arm's meaning is widened: a genuine node failure still reaches the node-failure arm, is still recorded `failed`, still carries the node's own text, and is still retried the full `1 + maxRetries` times.
0 commit comments