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
fix(spec,service-automation)!: errorHandling.maxRetries has one default, and strategy: 'retry' states its count (#4247) (#4266)
`flow.errorHandling.maxRetries` was declared twice with different values:
`FlowSchema` said `.default(0)`, while the engine's `retryExecution` read
`errorHandling.maxRetries ?? 3`. `??` fires only on `undefined`, so the winner
was decided by the ROUTE a flow took into the engine, not by what its author
wrote.
Reading the call graph closed one side of that: `retryExecution` has a single
caller, `this.flows` is written only by `registerFlow` (post-parse) and the
version-history rollback (an already-parsed snapshot), so no unparsed flow ever
reaches the engine. The behaviour actually shipping was the parsed one —
`strategy: 'retry'` with no count retried ZERO times, a declared capability the
runtime did not deliver.
The engine now keeps no defaults of its own: `retryExecution` takes the parsed
`NonNullable<FlowParsed['errorHandling']>` and destructures all five knobs, no
`??`. The parsed parameter type is what keeps a second set of defaults from
growing back — a knob the spec stops defaulting becomes a compile error rather
than a silent engine-side guess (Prime Directive #12).
BREAKING: `strategy: 'retry'` now requires `maxRetries` >= 1. With the engine's
copy gone an unstated count is unambiguously 0, and 'retry' with 0 attempts is
`strategy: 'fail'` under another label. Rather than pick 0 or 3 for the author,
the schema refuses the combination in both spellings (omitted → defaulted 0,
and an explicit 0) with the prescription in the message; a retry re-runs the
WHOLE flow, side effects included, so the count is the author's to state.
`maxRetries: 0` stays legal under 'fail'/'continue', which never read it.
The migration chain surfaces this as the semantic TODO
`flow-retry-max-retries-required` — there is no lossless rewrite, so
`os migrate meta` delegates the choice instead of guessing it.
Closes#4247
Copy file name to clipboardExpand all lines: content/docs/automation/flows.mdx
+37-4Lines changed: 37 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -665,7 +665,7 @@ The two recovery mechanisms operate at different scopes and do not compound:
665
665
|| Scope | On failure |
666
666
| :--- | :--- | :--- |
667
667
|`fault` edge | one node | traversal continues from the handler; the run completes |
668
-
|`errorHandling: { strategy: 'retry'}`| the whole flow | the flow re-runs **from the start**|
668
+
|`errorHandling: { strategy: 'retry', maxRetries: n }`| the whole flow | the flow re-runs **from the start**, up to `n` more times|
669
669
670
670
A failure a fault edge handled is not a flow failure, so it does **not** consume
671
671
a retry. That matters because flow-level retry replays every node that already
@@ -707,13 +707,46 @@ errorHandling: {
707
707
708
708
| Property | Type | Description |
709
709
| :--- | :--- | :--- |
710
-
|`strategy`|`enum`|`'fail'` (stop) or `'retry'` (re-run the whole flow). `'continue'` parses but the engine branches only on `'retry'`, so it behaves exactly like `'fail'` — use a `fault` edge to keep going past a failed node |
|`retryDelayMs`|`number`| Delay between retries (ms) |
710
+
|`strategy`|`enum`|`'fail'` (stop) or `'retry'` (re-run the whole flow). `'continue'` parses but the engine branches only on `'retry'`, so it behaves exactly like `'fail'` — use a `fault` edge to keep going past a failed node (default `'fail'`) |
711
+
|`maxRetries`|`number`|Retry attempts **after** the initial one, `0`–`10`. Under `strategy: 'retry'` it must be at least `1` and there is no default — see below (default `0`, i.e. no retries, for the strategies that never retry) |
712
+
|`retryDelayMs`|`number`| Delay between retries (ms) (default `1000`) |
Copy file name to clipboardExpand all lines: docs/protocol-upgrade-guide.md
+5Lines changed: 5 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -142,6 +142,8 @@ On the wire contract it also retires the `/analytics/query` request ENVELOPE (#3
142
142
143
143
The close-out sweep finishes the enforce-or-remove worklist across the remaining types: action `shortcut`/`bulkEnabled` (no keydown path; the multi-select toolbar reads the view's bulkActions), flow `active`/`template`/node `outputSchema`/errorHandling `fallbackNodeId` (`active: false` never stopped a flow — `status` is the enforced lifecycle; faults route via per-node fault edges), the inert view keys (list `responsive`/`performance`, form `data`/`defaultSort`/`aria` — list aria/data stay live), dashboard and widget `aria`/`performance`, `agent.knowledge` (declaring sources never scoped retrieval — absorbs the former topics→sources rename), and `skill.triggerPhrases` (phrases were never matched; routing is triggerConditions + the agent allowlist). All pure lossless deletes, each tombstoned at its schema with the prescription.
144
144
145
+
One flow key changes WITHOUT a lossless target: `errorHandling.maxRetries` (#4247). It carried two defaults — `.default(0)` in FlowSchema and `maxRetries ?? 3` in the engine's retryExecution — and because `??` fires only on `undefined`, an unstated count meant 0 retries for a flow parsed by the schema and 3 for a definition handed to the engine directly: the retry count was a function of the route in, not of the authored flow. The engine's copy is deleted (it reads the parsed block, no fallback), which makes an unstated count unambiguously 0 — and `strategy: 'retry'` that retries zero times is `strategy: 'fail'` under another name, the declared-not-delivered shape ADR-0049 exists to close. The schema therefore requires `maxRetries` at least 1 under `'retry'`, in both spellings (omitted, and an explicit 0). This is the one v17 flow change the chain cannot apply for you: choosing the count is a judgment about re-running the WHOLE flow with its side effects, so it is a semantic TODO rather than a rewrite.
146
+
145
147
### Mechanical (applied for you)
146
148
147
149
| Conversion | Surface | Change | Load window |
@@ -169,6 +171,9 @@ The close-out sweep finishes the enforce-or-remove worklist across the remaining
169
171
170
172
### Semantic (delegated to you, with acceptance criteria)
171
173
174
+
-**`flow-retry-max-retries-required`** — `flow.errorHandling.maxRetries (under strategy: 'retry')` → an explicit count >= 1 (e.g. maxRetries: 3), or strategy: 'fail'
175
+
- Why not automatic: maxRetries had two defaults — FlowSchema `.default(0)` and the engine's `maxRetries ?? 3` — so an unstated count retried 0 times through the schema and 3 times through a hand-built definition (#4247). With the engine's copy removed the unstated count is unambiguously 0, and retrying zero times is exactly `strategy: 'fail'`, so the schema now refuses the combination instead of it silently doing nothing. There is no lossless rewrite: 0 preserves the behaviour a parsed flow got but contradicts what its author wrote, and any positive count is a NEW decision about re-running the whole flow with its side effects. That choice is the author's.
176
+
- Done when: Every flow declaring `errorHandling.strategy: 'retry'` also declares `maxRetries` >= 1, and each count was chosen knowing a retry replays the flow FROM THE START (records re-created, callouts re-fired); flows that never actually wanted retries say `strategy: 'fail'`. No flow fails to register with the maxRetries prescription.
172
177
-**`analytics-query-request-envelope-retired`** — `api.analyticsQueryRequest.query` → bare AnalyticsQuery body (top-level cube/measures/dimensions/where/...)
173
178
- Why not automatic: The { cube, query: {...} } envelope was an HTTP-wire dialect of the retired degraded analytics shim (#3891), never stored in stack metadata — there is no source for the chain to rewrite. Callers of POST /analytics/query and /analytics/sql must move the query.* fields to the body top level themselves.
174
179
- Done when: Every /analytics/query and /analytics/sql call sends the bare AnalyticsQuery shape and succeeds; no request answers 400 VALIDATION_FAILED with the envelope prescription.
0 commit comments