Skip to content

docs(flows): split the subflow-strand paragraph into its three cases - #20377

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-17940-flows-subflow-strand
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-17940-flows-subflow-strand

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

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

The paragraph conflated three outcomes of a paused subflow chain into one
prose block, and its closing sentence ("an ancestor is never stranded,
because resuming it is not what moves it") was written for the child-strand
case only. #15556 (PR #17908, merged c8a006f) shipped a third case the
paragraph never described: the child completes, bubbleToParent resumes the
parent, and the parent's own downstream node throws — the parent itself
lands on the engine's 'stranded' exit and is reported on resumeFailure.

Measured against origin/main a88a1bb:
- packages/services/service-automation/src/engine.ts:1737 (SubflowParentStrand),
  :7434 (the 'stranded' exit bubbleToParent records it on)
- packages/plugins/plugin-approvals/src/approval-service.ts:3476 (the
  resumeFailure shape: RESUME_FAILED / stranded / repairable)
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 28, 2026
…n call

PM review on PR #20377: remove the two sentences that talk about the page's
own prose ("that sentence is scoped to..." / "the claim above does not
hold for it") and state the scoping as behaviour instead. Name the third
case's repair verb the same way the other two cases do -- the
`restore-suspension` REST verb (routed to `restoreConsumedSuspension` at
packages/runtime/src/domains/automation.ts:2752), issued on the parent's
run id, the one `resumeFailure` names -- rather than only the engine
method name.
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 05:14
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit ab94656 Sep 28, 2026
39 of 41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-17940-flows-subflow-strand branch September 28, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(flows): "an ancestor is never stranded" is falsified by the subflow-parent strand #15556 makes reportable

1 participant