Skip to content

Commit ab94656

Browse files
docs(flows): split the subflow-strand paragraph into its three cases (#20377)
Fixes #17940 Clause-②: no ## What was wrong `content/docs/automation/flows.mdx`'s subflow-chain repair paragraph conflated three distinct outcomes into one block of prose, and its closing sentence — "an ancestor is never *stranded*, because resuming it is not what moves it" — was written for exactly one of them. #15556 (PR #17908, merged `c8a006fc41`) shipped a case the paragraph never described, where that sentence is false: the child completes, `bubbleToParent` resumes the parent, and the parent's own downstream node throws. There the bubble is exactly what moves the parent, and the parent itself lands on the engine's `'stranded'` exit. ## Before > A child that fails terminally after the pause fails every waiting ancestor, so > no run is stranded as resumable-forever. **When the child's failure is a > strand** — its resume consumed the pause and a downstream node threw — each > ancestor's consumed pause is recorded too, and the repair verb puts the whole > chain back in one call: `POST …/runs/{runId}/restore-suspension` on **any** > member re-arms every member, deepest first, so the continuation re-issued on > the run you named flows back up through the ancestors instead of completing a > leaf into a parent that never continues. Re-arming an ancestor is all the verb > does — an ancestor is never *stranded*, because resuming it is not what moves > it. A cascade from a child that is **not** repairable records no ancestor > snapshot, deliberately, so the verb never promises a chain repair it could not > finish. ## After (revised per PM review — no self-reference, repair verb named) > A child that fails terminally after the pause fails every waiting ancestor, so > none of them is left stranded as resumable-forever. **When the child's failure > is a strand** — its resume consumed the pause and a downstream node threw — > each ancestor's consumed pause is recorded too, and the repair verb puts the > whole chain back in one call: `POST …/runs/{runId}/restore-suspension` on > **any** member re-arms every member, deepest first, so the continuation > re-issued on the run you named flows back up through the ancestors instead of > completing a leaf into a parent that never continues. Re-arming an ancestor is > all the verb does in this case — no ancestor is stranded here, because > resuming it is not what moves it. A cascade from a child that is **not** > repairable records no ancestor snapshot, deliberately, so the verb never > promises a chain repair it could not finish. > > **A third case is the opposite: the bubble itself is what strands an > ancestor.** The child completes cleanly, `bubbleToParent` resumes the parent > on the child's behalf, and the parent's own downstream node throws. There the > bubble — not a resume the caller issued — is exactly what moves the parent, > and it is the parent, not the child, that lands on the engine's `'stranded'` > exit, terminal. The child's own resume genuinely succeeded, so its resumer > (an approvals decision door, a wait timer) is told the resume succeeded; as > of #15556 an approval `decide()` also reports the stranded parent on > `resumeFailure` (`{ code: 'RESUME_FAILED', runId: '<parent>', status: > 'stranded', repairable: true }`). Repair it the same way: the same > `restore-suspension` verb (`restoreConsumedSuspension` underneath), issued on > the parent's run id — the `runId` `resumeFailure` names, not the child's. ## Code measured on `origin/main` `a88a1bb39` (not copied from the card) - `packages/services/service-automation/src/engine.ts:1737` — the `SubflowParentStrand` interface (`runId`, `repairable: true`, `error`), recorded only on the arm `AutomationResult.status` calls `'stranded'`. - `packages/services/service-automation/src/engine.ts:7434` — `bubbleToParent`: `if (parentRes.status === 'stranded')` records the `SubflowParentStrand` under the **child's** own run id. - `packages/services/service-automation/src/engine.ts:7511` — `takeSubflowParentStrand(childRunId)`, the delete-on-read hand-off. - `packages/plugins/plugin-approvals/src/approval-service.ts:3476` — the approvals decision door's `resumeFailure` on a `bubbleStrand`: `{ code: 'RESUME_FAILED', runId: bubbleStrand.runId, status: 'stranded', repairable: bubbleStrand.repairable }` — matches the card's claimed shape. - `packages/services/service-automation/src/engine.ts:7994` — `restoreConsumedSuspension`, the repair verb for the parent strand. - `packages/runtime/src/domains/automation.ts:2752` — the REST door, `POST /:name/runs/:runId/restore-suspension`, routes `parts[2]` (the `:runId` path segment) straight into `automationService.restoreConsumedSuspension(parts[2], …)` — so the same `restore-suspension` verb an operator calls in case (a)/(b) is what a third-case operator calls too, on the parent's run id (the `runId` `resumeFailure` names). Binding honoured (thread comment `5697194225`): #17541 owns the naming of any new `AutomationResult.status` member. This PR coins none — `'stranded'` is the status the engine and the approvals door already use today. ## PM review addendum PM review verified all code anchors and asked for two prose fixes, applied in commit `28c110796`: 1. Dropped the two sentences that referred to the page's own prose ("that sentence is scoped to…" / "the claim above does not hold for it") and restated the scoping as behaviour. 2. Named the third case's repair verb the same way the other two cases do — the `restore-suspension` REST verb (`restoreConsumedSuspension` underneath), issued on the parent's run id — instead of only the engine method name. ## Gates run (docs-only change, no changeset — `content/docs/**` is not a published package surface) `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands content/docs/automation/flows.mdx` derived 40 command(s), same 40 before and after the revision. All 40 ran green both times (reconciled with `--ran`: `40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN`). One-time prerequisite builds these gates needed (`@objectstack/formula` + `@objectstack/lint`, and `@objectstack/client` + `@objectstack/client-react`) — neither package's source was touched by this diff. Full command list and outputs are in the report comment on #17940. Serial neighbour: draft PR #20344 edits the same file at `:1357` and below; this diff's hunk sits at `:1104`–`:1129`, 245+ lines above it — no overlap. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c577e66 commit ab94656

1 file changed

Lines changed: 24 additions & 11 deletions

File tree

‎content/docs/automation/flows.mdx‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,17 +1102,30 @@ the chain resolves from either end:
11021102
parked on an `approval`, resuming the parent is refused too.
11031103

11041104
A child that fails terminally after the pause fails every waiting ancestor, so
1105-
no run is stranded as resumable-forever. **When the child's failure is a
1106-
strand** — its resume consumed the pause and a downstream node threw — each
1107-
ancestor's consumed pause is recorded too, and the repair verb puts the whole
1108-
chain back in one call: `POST …/runs/{runId}/restore-suspension` on **any**
1109-
member re-arms every member, deepest first, so the continuation re-issued on
1110-
the run you named flows back up through the ancestors instead of completing a
1111-
leaf into a parent that never continues. Re-arming an ancestor is all the verb
1112-
does — an ancestor is never *stranded*, because resuming it is not what moves
1113-
it. A cascade from a child that is **not** repairable records no ancestor
1114-
snapshot, deliberately, so the verb never promises a chain repair it could not
1115-
finish.
1105+
none of them is left stranded as resumable-forever. **When the child's failure
1106+
is a strand** — its resume consumed the pause and a downstream node threw —
1107+
each ancestor's consumed pause is recorded too, and the repair verb puts the
1108+
whole chain back in one call: `POST …/runs/{runId}/restore-suspension` on
1109+
**any** member re-arms every member, deepest first, so the continuation
1110+
re-issued on the run you named flows back up through the ancestors instead of
1111+
completing a leaf into a parent that never continues. Re-arming an ancestor is
1112+
all the verb does in this case — no ancestor is stranded here, because
1113+
resuming it is not what moves it. A cascade from a child that is **not**
1114+
repairable records no ancestor snapshot, deliberately, so the verb never
1115+
promises a chain repair it could not finish.
1116+
1117+
**A third case is the opposite: the bubble itself is what strands an
1118+
ancestor.** The child completes cleanly, `bubbleToParent` resumes the parent
1119+
on the child's behalf, and the parent's own downstream node throws. There the
1120+
bubble — not a resume the caller issued — is exactly what moves the parent,
1121+
and it is the parent, not the child, that lands on the engine's `'stranded'`
1122+
exit, terminal. The child's own resume genuinely succeeded, so its resumer
1123+
(an approvals decision door, a wait timer) is told the resume succeeded; as
1124+
of #15556 an approval `decide()` also reports the stranded parent on
1125+
`resumeFailure` (`{ code: 'RESUME_FAILED', runId: '<parent>', status:
1126+
'stranded', repairable: true }`). Repair it the same way: the same
1127+
`restore-suspension` verb (`restoreConsumedSuspension` underneath), issued on
1128+
the parent's run id — the `runId` `resumeFailure` names, not the child's.
11161129

11171130
See the worked example pair in the showcase app:
11181131
`showcase_project_closure` invokes the reusable `showcase_closure_signoff`

0 commit comments

Comments
 (0)