diff --git a/.changeset/blob-offload.md b/.changeset/blob-offload.md
new file mode 100644
index 0000000..8a46da6
--- /dev/null
+++ b/.changeset/blob-offload.md
@@ -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.
diff --git a/.changeset/child-runs.md b/.changeset/child-runs.md
new file mode 100644
index 0000000..3be0a62
--- /dev/null
+++ b/.changeset/child-runs.md
@@ -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`.
diff --git a/.changeset/definition-resolver.md b/.changeset/definition-resolver.md
new file mode 100644
index 0000000..9314e40
--- /dev/null
+++ b/.changeset/definition-resolver.md
@@ -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`.
diff --git a/.changeset/effect-reliability.md b/.changeset/effect-reliability.md
new file mode 100644
index 0000000..5e278e9
--- /dev/null
+++ b/.changeset/effect-reliability.md
@@ -0,0 +1,9 @@
+---
+'@swirls/looms': minor
+---
+
+Worker callbacks now answer `{ status: 'accepted' | 'stale' | 'cancelled', cancelRequested }` instead of the run state. `createLooms().workerCallback` and `createLoomsClient().workerCallback` return that result. `cancelRequested` tells an attempt that is already running to stop, because a dispatch that has left the host cannot be recalled. A stale report records nothing.
+
+`failEffect({ error, nonRetryable, retryAfterMs })` fails the current attempt. `nonRetryable` ends the effect even when the retry policy has attempts left. `retryAfterMs` waits at least that long before the next attempt and never shortens the policy backoff. Worker `fail` callbacks take the same two fields.
+
+A heartbeat may include `details`. The latest details are kept across retries and included on the next `EffectWorkerTask`, so a resumed attempt can continue from the recorded progress.
diff --git a/.changeset/hibernating-websockets.md b/.changeset/hibernating-websockets.md
new file mode 100644
index 0000000..152ff72
--- /dev/null
+++ b/.changeset/hibernating-websockets.md
@@ -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.
diff --git a/.changeset/otel-trace.md b/.changeset/otel-trace.md
new file mode 100644
index 0000000..3580c53
--- /dev/null
+++ b/.changeset/otel-trace.md
@@ -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.
diff --git a/.changeset/queue-worker.md b/.changeset/queue-worker.md
new file mode 100644
index 0000000..d3b2f78
--- /dev/null
+++ b/.changeset/queue-worker.md
@@ -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.
diff --git a/.changeset/run-context.md b/.changeset/run-context.md
new file mode 100644
index 0000000..d38fe7a
--- /dev/null
+++ b/.changeset/run-context.md
@@ -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.
diff --git a/.changeset/run-grants.md b/.changeset/run-grants.md
new file mode 100644
index 0000000..4e1f007
--- /dev/null
+++ b/.changeset/run-grants.md
@@ -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.
diff --git a/.oxlintrc.json b/.oxlintrc.json
index e56ea0a..4c413cb 100644
--- a/.oxlintrc.json
+++ b/.oxlintrc.json
@@ -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",
diff --git a/apps/docs/src/docs-manifest.json b/apps/docs/src/docs-manifest.json
index 4b05ea8..1fdc569 100644
--- a/apps/docs/src/docs-manifest.json
+++ b/apps/docs/src/docs-manifest.json
@@ -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",
diff --git a/apps/docs/src/routes/docs.reliability.tsx b/apps/docs/src/routes/docs.reliability.tsx
index 243e287..4ccc211 100644
--- a/apps/docs/src/routes/docs.reliability.tsx
+++ b/apps/docs/src/routes/docs.reliability.tsx
@@ -67,7 +67,13 @@ function Page() {
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.
+ 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.