Filed out of #9510, which required this measured rather than assumed: what happens to the retry budget when a paused run is later resumed and then fails — does it inherit the remaining attempts, or start fresh?
Measured — the answer is neither
packages/services/service-automation/src/engine.ts, origin/main @ ed4ca5999. Two independent findings, either of which alone settles it:
- The continuation cannot carry attempt state.
SuspendedRun declares runId, flowName, flowVersion, nodeId, nodeType, variables, steps, context, startedAt, startTime, correlation, screen. There is no attempt counter and no errorHandling block on it, and the durable stores round-trip exactly those fields. - It would have no reader if it did. The retry loop is reachable only from
execute()'s catch (strategy === 'retry' → retryExecution). resumeInternal's catch never consults flow.errorHandling — it records the failed run, fails any awaiting ancestors, and returns. The resume path has no retry loop at all.
So a resumed run that fails gets exactly one attempt. Not the remaining budget, not a fresh one: the declared policy stops applying at the pause.
Pinned as current behaviour — both pause sites, same number — in packages/services/service-automation/src/retry-attempt-pause.test.ts (#9510), so that whatever this card decides is a deliberate change and not an accident.
Why it is worth a card rather than a shrug
errorHandling.strategy: 'retry' is authored on the FLOW. Nothing in the flow schema, the docs or the runtime says it applies to the pre-pause segment only, and an author combining a flaky connector call with an approval node (the ordinary shape #9510 is about) reasonably reads it as covering the run. Today the post-approval half of that flow is unprotected, silently: the same node failing before the pause is retried and after the pause is not.
It predates #9510 and is unchanged by it — a run that paused on its FIRST attempt through execute() has always landed here. #9510 merely makes the paused-on-retry run reach the same (unprotected) state instead of being lost outright.
Not decided here
Three readings, and the choice is a contract decision rather than an implementation one:
- A. Deliberate and documented — a pause ENDS the retry-governed segment;
strategy describes one synchronous dispatch. Cheapest, and arguably right: the retry policy's backoff/jitter model assumes an in-process loop, which a pause of arbitrary duration is not. Costs nothing but a doc sentence and a pin. - B. Resume inherits the remaining budget — needs a new field on
SuspendedRun plus a store migration (sys_automation_run), and has to answer what "remaining" means across a restart and across a re-published flow whose maxRetries changed mid-pause. - C. Resume starts fresh — no snapshot change, but it makes total attempts a function of how many times a run happened to pause, which is the least defensible of the three.
Recommendation on the axes #9510's ruling used: A, with the behaviour documented on errorHandling in the flow schema and in content/docs/automation/. It serves the real authoring need (an author who wants the post-approval half protected can put that half's own failure handling in the flow), it is the honest description of what a durable pause is, and it does not widen a persisted shape for a capability nothing has asked for. B is the option to revisit if a real deployment produces the demand.
Refs
Filed out of #9510, which required this measured rather than assumed: what happens to the retry budget when a paused run is later resumed and then fails — does it inherit the remaining attempts, or start fresh?
Measured — the answer is neither
packages/services/service-automation/src/engine.ts,origin/main@ed4ca5999. Two independent findings, either of which alone settles it:SuspendedRundeclaresrunId,flowName,flowVersion,nodeId,nodeType,variables,steps,context,startedAt,startTime,correlation,screen. There is no attempt counter and noerrorHandlingblock on it, and the durable stores round-trip exactly those fields.execute()'s catch (strategy === 'retry'→retryExecution).resumeInternal's catch never consultsflow.errorHandling— it records the failed run, fails any awaiting ancestors, and returns. The resume path has no retry loop at all.So a resumed run that fails gets exactly one attempt. Not the remaining budget, not a fresh one: the declared policy stops applying at the pause.
Pinned as current behaviour — both pause sites, same number — in
packages/services/service-automation/src/retry-attempt-pause.test.ts(#9510), so that whatever this card decides is a deliberate change and not an accident.Why it is worth a card rather than a shrug
errorHandling.strategy: 'retry'is authored on the FLOW. Nothing in the flow schema, the docs or the runtime says it applies to the pre-pause segment only, and an author combining a flaky connector call with anapprovalnode (the ordinary shape #9510 is about) reasonably reads it as covering the run. Today the post-approval half of that flow is unprotected, silently: the same node failing before the pause is retried and after the pause is not.It predates #9510 and is unchanged by it — a run that paused on its FIRST attempt through
execute()has always landed here. #9510 merely makes the paused-on-retry run reach the same (unprotected) state instead of being lost outright.Not decided here
Three readings, and the choice is a contract decision rather than an implementation one:
strategydescribes one synchronous dispatch. Cheapest, and arguably right: the retry policy's backoff/jitter model assumes an in-process loop, which a pause of arbitrary duration is not. Costs nothing but a doc sentence and a pin.SuspendedRunplus a store migration (sys_automation_run), and has to answer what "remaining" means across a restart and across a re-published flow whosemaxRetrieschanged mid-pause.Recommendation on the axes #9510's ruling used: A, with the behaviour documented on
errorHandlingin the flow schema and incontent/docs/automation/. It serves the real authoring need (an author who wants the post-approval half protected can put that half's own failure handling in the flow), it is the honest description of what a durable pause is, and it does not widen a persisted shape for a capability nothing has asked for. B is the option to revisit if a real deployment produces the demand.Refs
executeWithoutRetryhas noisSuspendSignalarm #9510 — the card that required this measured; its ruling explicitly declined to settle it from the armchair