Skip to content

Trace a provider failure out of a sub-workflow child run - #2132

Merged
ppXD merged 1 commit into
mainfrom
fix/trace-a-provider-failure-out-of-a-sub-workflow
Oct 19, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/trace-a-provider-failure-out-of-a-sub-workflow

Conversation

@ppXD

@ppXD ppXD commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • A provider failure inside a flow.subworkflow child run that ends the parent now reaches the parent's run page: the completion path that resumes the parent reads the child's decisive failure with the same IRunFailureReader the child's own page asks, carries it on the resume payload, and the sub-workflow node fails with it beside its unchanged sentence (NodeResult.CarriedFailure). The parent's node.failed, and a map/loop container above it, record it with a FailureOrigin naming the deepest child run that met it, the node that ended that run, and when that run recorded it.
  • The parent's remedy is the child's remedy (same code, credential, repositories, actions and resolution, for the caller's standing in the parent's team), plus an additive Origin (run, workflow, node) read live under the asker's team: a run that is not the team's, or is gone, is never named and changes nothing else. The cause leads with "This failed in the sub-workflow ... (run ...)", the card links to that run, and Re-run still replays the parent, which starts a fresh child.
  • "Fixed since the failure" is measured from when the child recorded the failure (FailureOrigin.At), not from the parent's own record. A parent waiting on several sub-workflows is resumed only when the slowest has finished, so its record can be far later than the failure; judged from it, a token replaced from the child's page in between read as older than the failure on the parent's page while the child's page said it was fixed. A failure that kept no time is judged from the carrying record's, as before.
  • Nested sub-workflows chain to the deepest origin independent of finish order; a retried node carries the last attempt's child; a child that failed on anything else, handled the failure, handled it then failed on something else, or was cancelled carries nothing; if reading the child's failure fails the parent is still resumed.
  • Additive and non-breaking: no migration; the failure, the resume payload and the remedy each gain an optional member that older readers skip; rows and payloads written before read exactly as they did.

Test plan

  • Unit: full suite, 14809 passed (new: wire shape of the origin with and without its time, a build that predates the time reads the new record, the carried time rides through the node untouched)
  • Integration through the real engine: parent to sub-workflow to child git.list_prs against a dead GitLab credential with an owner, at top level, in a map, in a loop and three levels deep; a fan-out of two children where the token is replaced between the child's failure and the slow sibling's end (parent and child pages agree it is fixed); a carried failure judged from its kept time or, with none, from its record's; non-provider child failure, handled failure, handled-then-failed and cancelled child yield no remedy; an origin that is not the team's is never named; stranded-parent backstop in StuckRunReconcilerFlowTests; 505 passed in the related classes
  • HTTP E2E: parent run detail as Owner, Member and Viewer, Re-run on the parent starts a fresh child, a parent recorded before the field answers as it did; 16 passed
  • Mutation checks: dropping the stamped time fails the fan-out, nested and resume-payload tests; judging from the parent's record time fails the fan-out test and the theory
  • Frontend: origin line and link covered by ProviderRemedyCard.test.tsx and RunRemedyCard.test.tsx (unchanged by the latest revision)
  • Known follow-up: a plain map/loop container's failure is still judged from the container's record time, because the cell failure reader returns no time (on the base branch, not introduced here)

A provider failure inside a flow.subworkflow child run that ended the
parent was recorded on the child's own ledger and nowhere the parent's
page could find it. The parent's node failed with the sentence
"Sub-workflow run failed: Node 'x' failed." and nothing else, so the
parent's run page said "Node 'sub' failed." with no card, no credential
and no repair, while the child's page showed all three.

When a child run ends failed, the completion path that resumes its
parent now reads the child's decisive failure with the reader the
child's own run page asks (IRunFailureReader, so the two can never name
different failures) and puts it on the resume payload together with the
child run, the node that ended it and when the child recorded it. That
path is the one the engine's completion hook and the reconciler's
stranded-parent backstop both run, so a parent recovered after a lost
hook learns the same thing. The sub-workflow node still fails with its
own sentence, untouched, and the failure rides beside it on the new
NodeResult.CarriedFailure; the engine writes it on the parent's
node.failed record exactly as it writes one read off an exception, and a
container the node sits in (a map or loop) carries it as it carries any
cell's.

The parent's remedy is the child's remedy: same code, credential,
repositories, buttons and resolution, computed for the caller's standing
in the parent's team, which is the child's by construction. The
failure's new Origin names the run it happened in, and the remedy gains
an additive Origin (run, workflow, node) that is read live under the
asker's team: a run that is not the team's, or is gone, is never named
and does not change the remedy. A failure that names no repository is
traced through the child's launch scope, as the child's page does, not
the parent's. Whether the credential has been fixed since is measured
from when the child recorded the failure, not from the parent's own
record: a parent waiting on several sub-workflows is resumed only when
the slowest has finished, so its record can be far later than the
failure, and a token replaced from the child's page in between would
read as older than the failure on the parent's. A failure that kept no
time is judged from the record that carries it, as before. The cause
leads with a sentence naming the sub-workflow and run, and the card
shows it as a link that opens that run. Re-run is still the page's own:
it replays the parent, which starts a fresh child.

A failure that travelled through nested sub-workflows keeps the origin
it was first stamped with, so every level names the deepest run that met
it, independent of the order the levels finish. A retried node carries
the last attempt's child. A child that failed on anything else, handled
the failure with an error edge, handled it and then failed on something
else, or was cancelled carries nothing, so the parent keeps the sentence
it always had. Reading the failure is a courtesy to the page: if the
read fails the parent is still resumed.

Additive throughout: no migration. The failure, the resume payload and
the remedy each gain an optional member that older readers ignore, and
rows and payloads written before read exactly as they did.
@ppXD
ppXD merged commit e52b04a into main Oct 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant