Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
565d49b
feat(runtime)!: report worker callback outcome instead of run state
dylnslck Oct 23, 2026
948d3f1
feat(runtime): keep run attributes public and secrets beside the log
dylnslck Oct 23, 2026
529bf6c
feat(runtime): offload oversized JSON through a content-addressed blo…
dylnslck Oct 23, 2026
2b78886
feat(runtime)!: resolve definitions through a cached host resolver
dylnslck Oct 23, 2026
e98675d
feat(runtime): hibernate Durable Object event sockets
dylnslck Oct 23, 2026
1b5e8da
feat(runtime)!: launch workflow children as their own runs
dylnslck Oct 23, 2026
d004ad4
feat(runtime): authorize browser event streams with a signed run grant
dylnslck Oct 23, 2026
2bfe492
feat(cloudflare): add native Cloudflare Queues effect worker and push…
dylnslck Oct 23, 2026
6ca8aa4
feat(otel): propagate W3C trace context across runs, effects, worker …
dylnslck Oct 23, 2026
774d17f
fix(core): set run trace context without conditional spreads
dylnslck Oct 23, 2026
a4ac6f5
fix(runtime): authorize run routes from the start event instead of a …
dylnslck Oct 23, 2026
95da24b
style: format definition resolver sources
dylnslck Oct 23, 2026
6ebb530
style: format and lint websocket transport sources
dylnslck Oct 23, 2026
7e93e51
fix(core): track blob references per digest so trim never scans other…
dylnslck Oct 23, 2026
f43390e
refactor(runtime): build effect failure payloads from the protocol type
dylnslck Oct 23, 2026
be5a6ab
chore: merge feat/effect-reliability into feat/run-context
dylnslck Oct 23, 2026
8b628d3
feat(runtime): let a running worker attempt read the run's secret con…
dylnslck Oct 23, 2026
3a263c8
chore: merge feat/run-context into feat/blob-offload
dylnslck Oct 23, 2026
f5e88c6
chore: merge feat/blob-offload into feat/definition-resolver
dylnslck Oct 23, 2026
affe14f
chore: merge feat/definition-resolver into feat/child-runs
dylnslck Oct 23, 2026
cdbb170
test(runtime): check child runs inherit attributes and secrets
dylnslck Oct 23, 2026
aae7667
chore: merge feat/child-runs into feat/otel-trace
dylnslck Oct 23, 2026
3f5702c
feat(runtime): carry the caller's W3C trace context through a run
dylnslck Oct 23, 2026
897c9af
chore: merge feat/otel-trace into feat/hibernating-websockets
dylnslck Oct 23, 2026
54a65c4
chore: merge feat/hibernating-websockets into feat/grant-auth
dylnslck Oct 23, 2026
6c874b7
fix(cloudflare): authorize socket upgrades with the run's attributes
dylnslck Oct 23, 2026
b28489e
chore: merge feat/hibernating-websockets into feat/grant-auth
dylnslck Oct 23, 2026
d77e5b8
feat(cloudflare): echo a subprotocol grant on the socket upgrade
dylnslck Oct 23, 2026
75026dc
chore: merge feat/grant-auth into feat/queue-worker
dylnslck Oct 23, 2026
c66540e
test(runtime): measure retryAfterMs from before the run starts
dylnslck Oct 23, 2026
1e1ece3
feat(cloudflare): rebuild the queue transport on the worker callback API
dylnslck Oct 23, 2026
1e965cb
chore: merge feat/effect-reliability into feat/run-context
dylnslck Oct 23, 2026
9dd8306
chore: merge feat/run-context into feat/blob-offload
dylnslck Oct 23, 2026
1431bcc
chore: merge feat/blob-offload into feat/definition-resolver
dylnslck Oct 23, 2026
d189031
chore: merge feat/definition-resolver into feat/child-runs
dylnslck Oct 23, 2026
0e57782
chore: merge feat/child-runs into feat/otel-trace
dylnslck Oct 23, 2026
db8e34b
chore: merge feat/otel-trace into feat/hibernating-websockets
dylnslck Oct 23, 2026
198abe2
chore: merge feat/hibernating-websockets into feat/grant-auth
dylnslck Oct 23, 2026
a720a94
chore: merge feat/grant-auth into feat/queue-worker
dylnslck Oct 23, 2026
2a69c4e
chore: merge main into feat/effect-reliability
dylnslck Oct 23, 2026
a889664
chore: merge main into feat/run-context
dylnslck Oct 23, 2026
e0a8aaf
chore: merge main into feat/blob-offload
dylnslck Oct 23, 2026
4fa355e
chore: merge main into feat/definition-resolver
dylnslck Oct 23, 2026
94367cf
chore: merge main into feat/child-runs
dylnslck Oct 23, 2026
0434142
chore: merge main into feat/otel-trace
dylnslck Oct 23, 2026
10fe1f9
chore: merge main into feat/hibernating-websockets
dylnslck Oct 23, 2026
3407854
chore: merge main into feat/grant-auth
dylnslck Oct 23, 2026
6ef1eed
chore: merge main into feat/queue-worker
dylnslck Oct 23, 2026
1806817
feat(testing): replay recorded run logs in CI and document effect con…
dylnslck Oct 24, 2026
fcc5c8a
chore: merge main into feat/upgrade-safety
dylnslck Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blob-offload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swirls/looms': minor
---

`createLooms({ blobStore })` and `LoomsDurableObject` `blobs` store strings over 64 KiB by SHA-256 and keep a reference in the event or snapshot. A payload that is still over the 1 MiB event limit is stored as one blob. Reads, subscribers, folds, projections, and snapshot loads resolve the reference, so a run can complete with a payload larger than that limit. Trim removes a blob body once nothing retained still points at it. `memoryBlobStore`, `filesystemBlobStore`, and `r2BlobStore` are the included stores. Without a blob store, events over 1 MiB and snapshots over 8 MiB are still rejected.
9 changes: 9 additions & 0 deletions .changeset/child-runs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@swirls/looms': minor
---

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

A workflow can set `childExecution: 'run'` so spawn, map, fanout, and loop start a child run instead of a thread. The child run id is stable across retries. When the child finishes it signals `runtime.child.settled`, which completes the parent's wait. `childClose` is `terminate` or `wait`. The default remains in-process threads.

BREAKING CHANGE: `RunLauncher` implementations must provide `signal` and `cancel`.
7 changes: 7 additions & 0 deletions .changeset/definition-resolver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@swirls/looms': minor
---

Definitions can be loaded at start, spawn, and recovery through `definitionResolver`, with registered definitions checked first and an LRU cache in front. A missing definition fails the start before any event is written, and the accepted version stays pinned on the run. Evaluator definitions now include `version`, defaulting to `v1`, and lookup takes the name and version.

BREAKING CHANGE: `EvaluatorDefinition` requires `version`, and evaluator lookup is `(name, version)` instead of `(name)`. `defineEvaluator` still defaults the version to `v1`.
5 changes: 5 additions & 0 deletions .changeset/hibernating-websockets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swirls/looms': minor
---

Durable Objects accept a hibernating WebSocket on `GET /runs/:id/events`. The socket replays the retained log, then receives later events without a subscription held in memory. SSE sends the same `hello` and `resync` frames. A cursor below the retained head is told to resume from `head` instead of silently skipping trimmed events. `subscribeEvents` and the React store take `transport: "websocket" | "sse" | "auto"`; the default remains SSE, and a numeric reconnect delay still works.
5 changes: 5 additions & 0 deletions .changeset/otel-trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swirls/looms': minor
---

Runs carry the caller's W3C trace context. `POST /runs` reads the `traceparent` and `tracestate` headers, and `start`, `startRun`, and `client.startRun` accept `trace`. A valid context is recorded on `runtime.run.started` and `startIdentity.trace`, then passed unchanged to effect handlers (`ctx.trace`), worker tasks (`task.trace`), and every launched or child run unless the launch sets its own.
9 changes: 9 additions & 0 deletions .changeset/queue-worker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@swirls/looms': minor
---

feat(cloudflare): run worker effects over Cloudflare Queues

`cloudflareQueueWorker` sends each attempt to a queue as JSON, with the run's trace context and the attempt's deadlines; inputs over 100 KB travel as a blob reference and secrets stay off the queue. `queueConsumer` (push) and `createQueuePullConsumer` with `cloudflareQueuePullSource` (HTTP pull) run handlers that heartbeat, read secrets through the worker callback API, and stop when the run is cancelled. The consumer acks after `started`, so Looms owns retries; duplicate and stale deliveries are dropped, and dead-lettered messages are recovered by the schedule-to-start timeout. The pull consumer batches acks, backs off when idle, reports the backlog, and drains on `stop()`. `createMemoryQueue` gives tests the same lease and dead-letter behavior.

In the runtime, `EffectWorker.dispatchSemantics: 'acknowledged'` fails a rejected dispatch under the retry policy instead of parking it as ambiguous, and creating a runtime fails if such a worker claims an effect without `retry.scheduleToStartTimeoutMs`. Worker tasks carry `deadlines`, and `maxInFlightWorkerEffects` caps the worker attempts one run has in flight.
5 changes: 5 additions & 0 deletions .changeset/run-context.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@swirls/looms': minor
---

Runs accept immutable `attributes` and a `secrets` context. Attributes are recorded on `runtime.run.started`, the run state, and the run summary, and are passed to `authorize` when present. Secrets stay in a side store: they are available to effect handlers and in-process worker dispatch, and they are not written to events, snapshots, or run responses. A worker whose transport persists tasks reads them with `POST /runs/:runId/effects/:effectId/:attempt/secrets` (`client.workerSecrets`), which only answers the attempt that is currently running.
7 changes: 7 additions & 0 deletions .changeset/run-grants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@swirls/looms': minor
---

feat(runtime): authorize browser event streams with a signed run grant

`grantAuth` checks a short-lived HMAC grant from the `grant` query parameter or a `v1.` WebSocket subprotocol. The grant allows one principal to read one run, which is the credential a browser can send on EventSource or WebSocket. The Durable Object socket echoes a subprotocol grant on the upgrade, and `grantSubprotocol` gives other hosts the value to echo.
10 changes: 10 additions & 0 deletions .changeset/upgrade-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@swirls/looms': minor
---

feat(testing): replay recorded run logs in CI and document effect contracts

Adds a production-shaped event-log corpus under `packages/testing/corpus`, folded
deterministically in CI with `assertCorpusEntry`. Documents versioning effect
types (`payments.charge@v2`) and worker `handles` so fleets can roll out beside
each other without Temporal-style patches.
3 changes: 2 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,8 @@
"files": [
"apps/demo/src/server.ts",
"packages/debugger/src/server/**/*.ts",
"packages/s2/src/lite.ts"
"packages/s2/src/lite.ts",
"packages/runtime/src/filesystem-blob-store.ts"
],
"rules": {
"effecttsgo/global-fetch": "off",
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/docs-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,7 @@
"level": 2
}
],
"text": "Reliability and recovery Durability depends on the host and storage you choose. In-memory examples lose history on exit. SQLite persists locally; production actor hosts must preserve one writer per run and provide wake scheduling. Replay and recovery are different operations Historical replay folds recorded events to reconstruct state and does not dispatch effects. Recovery loads a snapshot and later events, then resumes unfinished work. An effect that lacks a recorded outcome may execute again. Looms does not promise exactly-once actions across arbitrary external services. A crash after an external action Looms requests an external action. The service performs it. The host stops before recording success. Recovery cannot infer success from the local log alone. Pass ctx.effectId as an idempotency key where the external API supports it. Check that the service retains the key for as long as you may need to retry the action. For unsupported APIs, store a durable operation ID, query the provider for the outcome, or reconcile before retrying. Never assume adding an arbitrary header makes an API idempotent. Retries and errors Effect retry policies count total attempts: maxAttempts: 3 means one initial attempt and up to two retries. Attempt starts and retry deadlines are recorded before the runtime waits, so a restart preserves the retry budget and backoff. Configure start, heartbeat, and execution timeouts for work delivered to external workers. Worker callbacks include the effect ID and attempt number; stale callbacks cannot complete a newer attempt. Exhaustion records runtime.effect.failed . Signals and webhook delivery Use the embed start/signal idempotencyKey option with a stable runId for retries of the same logical request. Verify webhook signatures and transform allowed webhook payloads into typed events on the server. Deduplicate by the provider delivery ID and correlate by run, thread, and domain identifiers. Retrying with a fresh key defeats deduplication. Interrupted model streams A model call can produce partial deltas before failing. Those deltas may already be recorded; resuming work may incur another model call and different output. Render partial content as provisional, inspect completion events, and decide how your application handles abandoned partial turns. The adapter does not guarantee exactly-once billing or identical regenerated text. Snapshots, retention, and projections A compatible snapshot bounds replay work; it is not a backup or a schema migration. Trimming events removes older audit and time-travel history and can prevent rebuilding new projections from the beginning. Define a retention and archive policy before enabling trimAfterSnapshot. Projectors run after local commit. Durable delivery stores a cursor per projector name and version, retries without advancing that cursor, and dead-letters exhausted batches. A run can finish before an external index or webhook catches up, so monitor lag and keep projector writes idempotent. Test recovery Test a restart while parked, an interruption during an external action, duplicate webhook delivery, and a deployment with existing runs. The testing guide provides the local restart test. Review versioning before changing definitions. ) }"
"text": "Reliability and recovery Durability depends on the host and storage you choose. In-memory examples lose history on exit. SQLite persists locally; production actor hosts must preserve one writer per run and provide wake scheduling. Replay and recovery are different operations Historical replay folds recorded events to reconstruct state and does not dispatch effects. Recovery loads a snapshot and later events, then resumes unfinished work. An effect that lacks a recorded outcome may execute again. Looms does not promise exactly-once actions across arbitrary external services. A crash after an external action Looms requests an external action. The service performs it. The host stops before recording success. Recovery cannot infer success from the local log alone. Pass ctx.effectId as an idempotency key where the external API supports it. Check that the service retains the key for as long as you may need to retry the action. For unsupported APIs, store a durable operation ID, query the provider for the outcome, or reconcile before retrying. Never assume adding an arbitrary header makes an API idempotent. Retries and errors Effect retry policies count total attempts: maxAttempts: 3 means one initial attempt and up to two retries. Attempt starts and retry deadlines are recorded before the runtime waits, so a restart preserves the retry budget and backoff. Configure start, heartbeat, and execution timeouts for work delivered to external workers. Worker callbacks include the effect ID and attempt number; stale callbacks cannot complete a newer attempt. Exhaustion records runtime.effect.failed . A handler can stop that budget early with failEffect and nonRetryable: true , or ask the next attempt to wait at least retryAfterMs without shortening the policy backoff. A worker heartbeat can carry details ; the next dispatch of that effect includes the latest details so the attempt can resume. The callback response is {'{ status, cancelRequested }'} , not the run state. cancelRequested tells an attempt that is already running to stop. Signals and webhook delivery Use the embed start/signal idempotencyKey option with a stable runId for retries of the same logical request. Verify webhook signatures and transform allowed webhook payloads into typed events on the server. Deduplicate by the provider delivery ID and correlate by run, thread, and domain identifiers. Retrying with a fresh key defeats deduplication. Interrupted model streams A model call can produce partial deltas before failing. Those deltas may already be recorded; resuming work may incur another model call and different output. Render partial content as provisional, inspect completion events, and decide how your application handles abandoned partial turns. The adapter does not guarantee exactly-once billing or identical regenerated text. Snapshots, retention, and projections A compatible snapshot bounds replay work; it is not a backup or a schema migration. Trimming events removes older audit and time-travel history and can prevent rebuilding new projections from the beginning. Define a retention and archive policy before enabling trimAfterSnapshot. Projectors run after local commit. Durable delivery stores a cursor per projector name and version, retries without advancing that cursor, and dead-letters exhausted batches. A run can finish before an external index or webhook catches up, so monitor lag and keep projector writes idempotent. Test recovery Test a restart while parked, an interruption during an external action, duplicate webhook delivery, and a deployment with existing runs. The testing guide provides the local restart test. Review versioning before changing definitions. ) }"
},
{
"path": "/docs/schedules",
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions docs/modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Each module owns its definitions. `workflow({ definitions: [checkout] })` instal

At initialization, Looms rejects duplicate `(kind, name)` registrations and definitions whose kind is not implemented by their owning module. Starting or spawning an unregistered definition fails before any run events are written. An unsupported thread kind is rejected the same way; if such an event somehow appears in a log, fold marks the thread failed instead of inventing empty state. Pass all child definitions to their owning modules too.

`defineWorkflow({ childExecution: 'run' })` launches each spawned, mapped, or looped child as its own run instead of a thread in the parent. The child run id is derived from the parent run and the child thread id, so a retry finds the same run. The child appends `runtime.child.settled` to the parent when it finishes, and that event completes the parent's wait. `childClose: 'terminate'` (the default) cancels those runs when the parent is cancelled. `childClose: 'wait'` cancels the children first and cancels the parent only after they have finished. A missing `RunLauncher` fails the launch. The default `childExecution` is `'thread'`, which keeps the child inside the parent run.

`defineModule(options, setup)` returns a complete runtime module. The callback receives typed `effect`, `projection`, `thread`, `input`, and `emit` builders. Only returned members are installed; access them through `payments.effects.charge` and `payments.projections.ledger`. Event schemas can be inline or supplied as a reusable `defineEventCatalog`.

Modules that implement a custom thread kind can return `definitions` alongside `threads`. The runtime gathers these for `start`, HTTP starts, and cross-module child spawning.
Expand Down
Loading
Loading