Repository navigation
Conversation
FarinM
force-pushed
the
intr-688-add-a-parked-state-that-stops-a-workflow-deployments-child
branch
3 times, most recently
from
November 2, 2026 13:53
a90aedc to
9a55d0b
Compare
FarinM
force-pushed
the
intr-688-add-a-parked-state-that-stops-a-workflow-deployments-child
branch
4 times, most recently
from
November 3, 2026 17:45
afbe688 to
1e3fceb
Compare
Base automatically changed from
intr-606-persist-step-state-on-the-hub-so-workflows-fail-over-without
to
main
November 3, 2026 17:50
FarinM
force-pushed
the
intr-688-add-a-parked-state-that-stops-a-workflow-deployments-child
branch
3 times, most recently
from
November 4, 2026 14:58
c1387c3 to
844152f
Compare
FarinM
marked this pull request as ready for review
November 4, 2026 15:18
FarinM
force-pushed
the
intr-688-add-a-parked-state-that-stops-a-workflow-deployments-child
branch
2 times, most recently
from
November 6, 2026 00:44
39cf558 to
ef248eb
Compare
A childWorkflow step relays its child's parks to the parent one at a time and holds the rest in memory. When the process stopped while an approval waited there, resume found the child parked on an approval the parent had never relayed, with its snapshot in neither log, and failed the run. The snapshot was durable all along: the parked step stores it with its pending operation. Resume now reads it from there through the env's new `loadParkedApproval` and relays the approval, and the sidecar wires its step-store read into the top-level and nested run envs. The lookup that follows a relay down to the step that parked moves into the runtime, so re-registering parked correlations and resume share it.
A host that stops a workflow child has to know whether the trigger it handed the child survives the stop. A run's started promise settles once its RunStarted is durable, or reports that the run settled without one or resumed from a log that had already ended.
A write puts its files in the checkout before it commits, so one that failed in between left an event there that the resumed run never reads. Boot recovery read the checkout, so it could hold back the mail that started such a run, and nothing delivered it again. The dispatch's check of whether a run started read the checkout too, so it then waited on a run that never resumed. Both now read committed history. Boot recovery reads one committed snapshot, which parses each tree once, since a walk of runs/ would otherwise parse that tree again for every run.
A recycle or crash respawn moved every mail still in processing back to the inbox, so mail a live run had already taken reached the new child again: a turn the run already had stalled the dispatch on a re-park that never came, and the start mail came back as a new turn. A respawn now leaves that mail for the resumed run and consumes it once the run next parks or ends. Mail of a run that had already ended goes back to the inbox, where the dispatch refuses it. A recycle replayed that mail before it killed the old child, which could still take mail in between. The replay now waits for the old child to be gone, for the writes it asked for, and for a boot replay still running, which would put back mail the replacement takes. A write waiting on a merge from a child that exited fails instead of holding the repository lock those replays need.
A host that stops a workflow child loses whatever the child was doing, so it may stop the child only while no run works and no committed input is still on its way to the run that waits for it. On `quiet.request` the child answers that the run has ended, or whether it is quiet together with the earliest timer its scheduler has queued, so the host can wake it for that timer. Waits on signals and child handles take a required `onWake`, so whatever wakes a waiting step counts it as working before the step stops counting itself. Scheduler recovery no longer queues a timer whose step settled after it was set, or whose run ended, since the answer would report a wake for it that does nothing.
The supervisor, recycle, wiring, run-child and parked-correlation tests made their data dirs under the system temp dir and left them there, so each run of those files added more. They now remove them once their tests are done.
Only a fresh run was given its trigger payload, so a run resumed after a restart or respawn read null for trigger.payload in every later step, though its RunStarted records the payload.
A restart can find a loop or onTrigger body holding parks its container never relayed, as it can a childWorkflow child. Resume handed the container a single token for the park it had relayed, so the others were never relayed and the run waited forever. Both containers now resume through the childWorkflow's relay plan, which relays every park the body holds that the container never did. The plan also counts an approval park a body step stored but crashed before logging, and re-registering parked correlations reads bodies the same way, so both find the same parks. A body whose log is gone starts over: the container re-links the relay it holds for it once, and fails the step when input was delivered after the spawn, which it cannot hand down again. A re-established relay lets a nested container hand down a signal its parent took before the restart. The Hub's parked-run check reads the body's log too, so an onTrigger body parked on a signal its section never relayed counts as parked. A timed step can leave an approval and go on, so a body may no longer hold the approval its container relays. A loop or onTrigger container now retires that relay and carries on, whether the body left before the restart or while the relay waits. A spawn step still fails on it. SignalAwaitAbandoned now accepts the approval park of a step that spawned a child, which changes which logs replay as valid. A plan that only retires a relay holds no park, so the parked-run check does not count it. A loop or onTrigger body its container resumes is given its input, so a body whose log is gone starts over with it. An onTrigger section recovers that input from its own log: the run's trigger payload for its first event, and what its input park delivered for a later one.
The recycle policy kept the first child's start time, so once a max-uptime recycle had run, every later tick recycled the replacement again.
A shutdown during a respawn did not wait for its replay of processing/, which could write after the host removed the run repository and recreate it. A replay that has not begun when shutdown starts no longer runs.
The supervisor needs only this shape, but importing it loaded the whole store adapter into the sidecar's boot, along with everything the adapter depends on.
The runtime's subscribe contract replays from a seq, but the child's proxy store only tails from head, so a consumer missed whatever landed before it subscribed. A timed gate woken by its due timer waited forever on a TimerFired already in its log.
A re-park read the run's state twice. A TimerFired between the reads left no timer to adopt, so the gate minted a second TimerSet with a fresh deadline. A gate that lost its timer watch also waited on with no deadline at all, so that now fails the run. A gate its own timer moved also resumes when another gate shares its name. The refusal there guards against binding a signal's payload to the wrong gate, and a timeout has no payload.
A container handed a decision down as soon as it had it. A body still resuming acts on a park it read before the decision landed, so an agent step looked crashed and failed.
The drain timeout signed and committed its CancelRequested itself, with the substrate and signing bindings threaded in for it alone. It now hands the cancellation to a commit the supervisor injects, so the supervisor decides how a cancellation it owns is committed. What gets committed is unchanged.
It also drops "on spawn" from the recovery's log messages, since the function need not know what called it.
A deployment waiting on an approval, a signal or its next trigger kept its workflow child running for the whole wait. The supervisor now stops the child once it reports quiet twice, and a re-registration delivers waiting mail, since mail that wakes the child from standby would otherwise wait out the 300 s backstop. A supervisor in standby respawns its child for mail, a signal, a cancellation, its earliest pending timer or a wake request. A host that records each standby through recordStandby can bring the deployment back with restoreInStandby after a restart, with no child running. A new input.taken frame answers a trigger or signal that asks for it once the child has committed the input or declined it, so an input the child took is not dropped with the stopped child's writes. A dispatch now consumes a signal's mail only once that answer says the child committed it, since another step's park or the run's end says nothing about this mail. A declined mail is rejected if the committed log shows its run ended. On a live run it fails the dispatch and stays in processing/ until a respawn replays it, since forcing a recycle to retry it could loop on a persistent fault.
A signal committed while a resumed run plans moves a step the run has just read as parked, which fails an agent step or loses a section's input. A resume guard cannot tell this from a crash after the decision was taken, when re-invoking the step could run a tool twice, so the child holds the signal instead.
The supervisor can put a waiting deployment's child in standby, but nothing outside the sidecar could ask it to. The Hub can now send standby and wake commands over the link, and confirms a standby request only once it holds the history the stopped child left. The deployment stays routed, so an input wakes it; the sidecar reports that wake, holding the report through a link outage, and every hello says whether the child is in standby. The run record keeps what the child was parked on, so a sidecar restart keeps the deployment in standby without the child while its history is unchanged. The sidecar bounds the length of a control answer's error, and the Hub rejects a longer one. An answer that fails validation rejects the pending control it names, which could otherwise wait forever.
A user could not ask for a deployment to be put in standby or woken. The API records the request and the lifecycle sweep carries it out against the sidecar, so the call does not wait for every run to go quiet. The Hub now also acts on what the sidecar reports about standby: a child in standby that nobody asked for is recorded waking and woken, and a hello finding the child running ends a recorded standby. Moving a deployment to replacement capacity resets its standby state, since the replacement starts awake: a standby still wanted is asked for again there, and a wake has nothing left to do.
FarinM
force-pushed
the
intr-688-add-a-parked-state-that-stops-a-workflow-deployments-child
branch
from
November 8, 2026 22:00
ef248eb to
65a681a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A deployment waiting on an approval, a signal, mail or its next trigger kept its workflow child running for the whole wait. Now a user can put it in standby: the sidecar stops the child once every run only waits on outside input, and runs it again on mail, a signal, an approval decision, a due timer, a cancellation or a wake request. The deployment keeps its capacity and stays routed.
What changes
quiet.requestthe child reports whether its runs are quiet, plus the earliest timer it has queued. A map or a step with a timeout counts as work until it ends, since a fresh child could not continue it.hellowhether the child is in standby. It refuses standby while mail waits for the child.POST .../standbyand.../wakerecord the request, and the lifecycle sweep carries it out in the background. The Hub recordsstandbyonly once it holds the child's history. Cancellation and expiry end standby, and replacement capacity starts awake and re-applies standby once every run waits.Design decisions
API changes
POST /api/tenants/:tenantId/workflows/runs/:runId/standbyand.../wake, both needingmanage. They return202while pending,204when already in that state, and409for a terminal or cancelling run or one without usable sidecar capacity.standby: { state, changedAt, wakeAt, reason }, withstateone ofawake,entering,standbyorwaking.Known limitations
enteringfor as long as the run is live, with no error recorded; the sweep only logs each retry at info level.Future work