Skip to content

feat(runtime)!: launch workflow children as their own runs - #39

Closed
dylnslck wants to merge 25 commits into
mainfrom
feat/child-runs
Closed

dylnslck wants to merge 25 commits into
mainfrom
feat/child-runs

Conversation

@dylnslck

Copy link
Copy Markdown
Contributor

Problem and change

Workflow children always ran as threads inside the parent run, so a child could not live on its own Durable Object or survive independently of the parent log. Spawn, map, fanout, and loop can now start a child run when the definition sets childExecution: 'run'.

The child run id is derived from the parent run and the child thread id, so a retry addresses the same run. When the child finishes it appends runtime.child.settled to the parent, and that event completes the parent's wait. childClose: 'terminate' cancels those children when the parent closes. childClose: 'wait' cancels the children first and cancels the parent only after they reach a terminal status. The default stays in-process threads.

Launches that do not set close stay fire-and-forget. Completing the parent does not cancel them.

Review notes

RunLauncher now requires signal and cancel. HTTP signal still rejects reserved runtime events. Child settlement uses accept, which keeps the event origin and coalesces when the target run is already waking, so a parent wake can notify itself without waiting on that wake.

A crash after runtime.child.close.requested is committed and before launcher.cancel returns will not retry the child cancel. The flag is written first so a follow-up wake does not cancel twice.

This branch is based on main and will conflict with the open effect-reliability, run-context, blob-offload, definition-resolver, and hibernating-websocket pull requests where they touch production.md, the launcher, and the runtime facade.

Checked with bun x tsc -p packages/{core,workflow,runtime,actor,cloudflare,debugger}/tsconfig.build.json --noEmit, bun x oxlint --deny-warnings on the changed sources, and bun test src in packages/runtime, packages/workflow, packages/core, packages/actor, packages/cloudflare, and packages/debugger. bun run verify was not run.

Existing contributor checklist

  • bun run verify passes
  • Changeset added if @swirls/looms behavior changes (bun run changeset)
  • Docs updated for API / architecture changes

Made with Cursor

dylnslck and others added 25 commits October 23, 2026 10:13
BREAKING CHANGE: POST /runs/:runId/effects/:effectId/:attempt/:action returns { status, cancelRequested } instead of the run state. createLooms().workerCallback and the HTTP client return the same shape. Heartbeats may carry details that the next attempt's task includes. failEffect and worker fail callbacks can mark an attempt non-retryable or set retryAfterMs.

Co-authored-by: Cursor <cursoragent@cursor.com>
Attributes are immutable labels on the start event, run state, and summary, and authorization can see them. Secrets stay in a side store for handlers and in-process dispatch, and are kept out of events, snapshots, and HTTP responses.

Co-authored-by: Cursor <cursoragent@cursor.com>
…b store

Strings over 64 KiB, and any event payload that would still exceed 1 MiB, are stored by SHA-256. Reads and snapshots resolve the references, and trim deletes a body once nothing retained points at it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Start, spawn, and recovery can load a definition that was not registered on the module, pin that version on the run, and fail a missing start before any event is written.

BREAKING CHANGE: Evaluator definitions require version, and lookup takes the name and version. defineEvaluator still defaults the version to v1.

Co-authored-by: Cursor <cursoragent@cursor.com>
Spawn, map, fanout, and loop can start a child run that signals the parent when it finishes. Cancelling the parent either stops those children immediately or waits until they finish cancelling.

BREAKING CHANGE: RunLauncher implementations must provide signal and cancel.

Co-authored-by: Cursor <cursoragent@cursor.com>
…full fold

Run attributes are immutable and recorded on runtime.run.started, so authorization reads seq 1 and only falls back to loading the run when the log is trimmed. Store failures now fail the request instead of authorizing without attributes. Also fixes the start-body attribute decode, which did not type-check.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… runs

Dropping a reference used to list and read every run's manifest, so each trim cost grew with the number of runs in the bucket. Each run now writes refs/<digest>/<run> markers, and collecting a body lists only that digest's markers.

The old scheme also lost bodies when two Durable Objects shared a bucket. retain now writes markers before checking that each new body exists and fails the append if one was deleted. collect lists markers again after deleting and restores the body if a reference appeared. BlobBytes gains exists, and R2BucketLike gains head, so the check does not download the body.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…text

A transport that persists tasks cannot carry secrets, so its worker needs another way to read them. POST /runs/:runId/effects/:effectId/:attempt/secrets returns the secret context only while that attempt is started and unsettled, and stale or cancelled otherwise. It uses worker access, so it stays closed until the host configures worker auth, and responses are not cacheable. client.workerSecrets wraps it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The retry is scheduled from the wake's clock, which is read before the event timestamp, so comparing against the event could fail by a millisecond. The assertion also no longer skips when the payload is missing.

Co-authored-by: Cursor <cursoragent@cursor.com>
@dylnslck

Copy link
Copy Markdown
Contributor Author

Superseded by squash-merge of the stack tip (#46), which already includes this change set after #34 landed on main.

@dylnslck dylnslck closed this Oct 24, 2026
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