Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ea8aa30
docs(fibers): add accepted fibers RFC amended to the contribution model
mattzcarey Aug 27, 2026
77f4edb
feat(fibers): add agents/fibers durable replayable execution capability
mattzcarey Aug 27, 2026
e8ebbba
test(fibers): capability harness, workers suite, and export typing
mattzcarey Aug 27, 2026
02e8902
docs(fibers): reference page, design-doc updates, and changeset
mattzcarey Aug 27, 2026
ff7a399
refactor(fibers): declare definitions in the Fibers constructor
mattzcarey Aug 27, 2026
438557b
feat(fibers): custom recovery as { run, recover } definitions (Phase 2)
mattzcarey Aug 27, 2026
5057d36
feat(fibers): install on Agent and replatform Think, AIChatAgent, and…
mattzcarey Aug 27, 2026
3bb0e42
test(fibers): standalone + coexistence fixtures and a real process-ki…
mattzcarey Aug 27, 2026
b01f2ac
refactor(tasks): ship the capability as Tasks; add the Streams RFC
mattzcarey Aug 28, 2026
e51dd72
feat(streams): durable incremental output as a Lifecycle capability
mattzcarey Aug 28, 2026
8881dbd
feat(streams): batched reads via readBatches
mattzcarey Aug 28, 2026
04c5bac
feat(streams): replatform chat/think resumable streams onto the capab…
mattzcarey Aug 28, 2026
76a45e7
feat(streams): tags, up-to-date signal, and SSE serving
mattzcarey Aug 28, 2026
af10d0f
Merge origin/main: port Tasks to the Lifecycle job queue
mattzcarey Aug 28, 2026
5572330
refactor(tasks): remove recover — interruption handling is uniform re…
mattzcarey Aug 28, 2026
5e13fdc
refactor(tasks,streams,chat): quality pass — evidence API, shared tur…
mattzcarey Aug 28, 2026
b3eb767
chore: format docs with the repo-pinned oxfmt
mattzcarey Aug 28, 2026
9ac0239
fix(chat): recovery dispatch must not hold the Lifecycle job queue
mattzcarey Aug 29, 2026
4873ebf
chore: drop committed e2e probe persist dir; ignore .wrangler-* variants
mattzcarey Aug 29, 2026
b3f37ac
docs(design): rewrite the Tasks and Streams records; clean Agent task…
mattzcarey Aug 29, 2026
be2eead
chore: format design records with oxfmt write mode
mattzcarey Aug 29, 2026
221fa26
fix(tasks): drop unused TaskStep type import
mattzcarey Aug 29, 2026
66c29b2
fix(streams,tasks,chat): harden resume, join, and scoping edges
mattzcarey Aug 29, 2026
963faf6
refactor(lifecycle): define the job dispatch contract
mattzcarey Aug 29, 2026
7192f24
fix(lifecycle,tasks,streams): close review-flagged dispatch and resum…
mattzcarey Aug 30, 2026
8b3b910
test(lifecycle): make the stale-snapshot probe arming deterministic
mattzcarey Aug 30, 2026
a22d333
fix(tasks,streams,chat,think): close review-flagged durability gaps
mattzcarey Aug 30, 2026
9f40fdf
fix(streams): wake tailing readers when an aperture delete removes a …
mattzcarey Aug 30, 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
14 changes: 14 additions & 0 deletions .changeset/brave-queues-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"agents": patch
---

Define the Lifecycle job dispatch contract. Job ids are now scoped to their
owning capability: a cross-owner id collision throws instead of silently
replacing the other owner's job. A same-id `push()` or `reschedule()` made
while a job is dispatching supersedes the returned drive result, so a wake
pushed mid-drive can no longer be lost — and each due job is refetched
before dispatch, so a job replaced earlier in the same alarm cycle is
skipped instead of dispatched from its stale snapshot. A dispatch that
outlives its job's
hung timeout logs a warning and emits `job:slow_dispatch` telemetry —
`onJob` must stay bounded and detach unbounded work.
9 changes: 9 additions & 0 deletions .changeset/chilly-onions-migrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"agents": patch
"@cloudflare/ai-chat": patch
"@cloudflare/think": patch
---

Replatform chat's resumable streams onto the `agents/streams` capability.

`ResumableStream` is now a thin adapter over `Streams`: chat's in-flight turn output lives in the shared durable chunk log (`cf_agents_streams` / `cf_agents_stream_chunks`), packed ~10 wire chunks per stored segment for write economy, with completion/error mapped onto stream settlement and retention keyed off the stream row's `updated_at` (sweeps no longer scan the chunk table). Existing `cf_ai_chat_stream_*` tables migrate wholesale — including an in-flight stream — on first construction after upgrade, then are dropped. `AIChatAgent` and `Think` expose the backing capability as `readonly streams`, so any `streams.read()` consumer on the same Durable Object can observe chat streams. The chat wire protocol, replay handshake, and recovery behavior are unchanged.
9 changes: 9 additions & 0 deletions .changeset/mighty-rivers-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"agents": patch
---

Add `agents/streams`: durable incremental output as a Lifecycle capability (experimental).

One `Streams` instance per Durable Object owns an ordered, durable chunk log per stream with a monotonic cursor: `open()` (idempotent on the id), synchronous durable `append()` that wakes live readers, `close()`/`error()` settlement, replay-then-tail `read({ from, signal })` plus its batched form `readBatches({ from, signal, batchSize, onUpToDate })` (arrays per replay slice and per live-tail wakeup, with a caught-up-to-tail signal), indexed non-unique `tag`s for find-the-latest-stream-of-an-operation lookups (`open(id, { tag })` / `list({ tag })`), `sseResponse()` for one-call SSE serving with native `Last-Event-ID` resume and `up-to-date`/`done`/`error` control events, and `status()` reporting state, cursor, and last activity. Reads are independent of producer liveness; the capability needs no alarm, so it also works on facets.

Streams is the incremental-output half of the pattern the Tasks migration validated, composed without coupling: a task step appends to a stream and checkpoints `{ streamId, cursor }`, and its `recover` callback reads `streams.status()` as durable interruption evidence — proven across a real SIGKILL by the e2e suite, where recovery finalizes the stream at exactly the chunks that survived. Design record: `design/rfc-streams.md`.
13 changes: 13 additions & 0 deletions .changeset/young-buses-replay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"agents": patch
---

Add `agents/tasks`: durable, replayable background execution as a Lifecycle capability (experimental).

One `Tasks` instance per Durable Object owns any number of named Task definitions declared in its constructor (`new Tasks({ definitions: {...} })`, mirroring the Scheduler's callbacks map), so the registry is rebuilt on every wake and recovery of in-flight runs is correct by construction. Runs start with the typed `tasks.run(name, input, options)`, and `tasks.handle(name)` gives a typed lens scoped to one definition. A run survives process loss and deployments by replaying its handler from the top: completed `step.do()` steps return journaled results, `step.sleep()` / `step.sleepUntil()` consult persisted deadlines, and execution continues from the first unfinished step under generation fencing. Steps carry per-attempt retry and timeout policy, stable idempotency keys for external deduplication, and `step.status()` progress with a replay live gate that never re-publishes old progress as new.

There is no separate recovery mode: an unclean interruption replays the handler on the next wake, and handlers make replay safe with step idempotency keys for external writes and durable evidence (a stream's cursor, a rows-written count) read at the top of the work. The interrupted step is first-class evidence: `step.interrupted` is `{ name, attempt }` on a replay after process loss (`null` on clean attempts), and a `task:attempt:interrupted` event carries the same step. Clean step failures are not interruptions; the retry policy owns them.

`Agent` installs the capability automatically as experimental `this.tasks`, with subclass definitions declared on the overridable `taskDefinitions` field and framework-internal definitions attached through a composition-root aperture. The internal chat frameworks now run on it: Think and AIChatAgent chat turns and Think's messenger replies each execute as a journaled step with `stash()` persisted in host storage, and a replay whose live closure is gone branches into the unchanged ChatRecoveryEngine (and messenger recovery) on durable evidence. The legacy `runFiber()`/`startFiber()` APIs are unchanged and still recovered by their own scan; facet-hosted turns stay on the legacy engine until routed Fibers land.

Runs are durably accepted (`tasks.run()` returns a receipt; idempotency keys join existing runs), inspectable (`get`, `getByIdempotencyKey`, `list`), cooperatively cancellable, and retained until deleted. The capability stores run deadlines in its own tables and mirrors each non-terminal run as one job in the Lifecycle work queue (never touching the physical alarm), so it composes with the Scheduler and other capabilities on one shared, queue-derived alarm. Design record: `design/rfc-fibers.md` (shipped under the name Tasks).
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ packages/*/docs

# cloudflare/wrangler
.wrangler
.wrangler-*-state
.wrangler-*
.dev.vars

# e2e recovery harness miniflare/SQLite state
Expand Down
2 changes: 2 additions & 0 deletions design/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ Keep it concise. A few paragraphs is fine. These are records, not essays.
| `mcp.md` | design doc | Stateless, Legacy compatibility, Legacy sessionful, client, package boundary, and conformance architecture |
| `durable-object-lifecycle.md` | design doc | Lifecycle Objects, capability and host phases, Scheduler/alarm ownership, host context, identity, and always-hibernating WebSockets |
| `rfc-durable-object-lifecycle.md` | RFC | Constructor-composed Durable Object lifecycle with reusable components and always-hibernating WebSockets |
| `rfc-fibers.md` | RFC | Tasks (née Fibers) — durable replayable execution as one Lifecycle capability: named definitions, journaled steps, sleeps, optional recovery callback (accepted, amended) |
| `rfc-streams.md` | RFC | Streams — durable incremental output as a Lifecycle capability: chunk log, cursor, replay-then-tail reads, composed with Tasks via checkpointed cursors (proposed) |

## Relationship to `/docs`

Expand Down
6 changes: 6 additions & 0 deletions design/alarm-coordination.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ capabilities push jobs instead of contributing wake times.
used for wakes that must not be delayed by other work (deferred destroy).
- **Deadman pre-alarm** — armed before the event loop drives any due job, so
an isolate death mid-drive still wakes the object to resume its queue.
- **Tasks** — the capability for durable replayable execution. Every
non-terminal run's authoritative `next_at` deadline (acceptance, sleeps,
retries, and claim backstops all write it) is mirrored as one queue job
per run (`id = "task:" + the run id`, so a retime is a same-id push); the run's
wake dispatches through `onJob` and settles or reschedules via the drive
result.

## How the alarm is derived

Expand Down
9 changes: 6 additions & 3 deletions design/durable-object-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ recalculation, chooses the earliest contribution, runs every capability's
`onAlarm()` followed by the host's `onAlarm()`, then recalculates once more.

Capabilities own their durable work. Scheduler stores named callback rows in
its table; a future Fiber capability can store resumable jobs in its own table;
an MCP capability can store reconnect state in its own table. They coordinate
only through Lifecycle's alarm contract and do not depend on Scheduler.
its table; the Tasks capability stores replayable runs and step journals in
its own tables and contributes its earliest run deadline the same way
([rfc-fibers.md](./rfc-fibers.md)); an MCP capability can store reconnect
state in its own table. They coordinate only through Lifecycle's alarm
contract and do not depend on Scheduler.

A host can also implement `getNextAlarm()` for work not yet extracted into a
capability. Exclusive contributions replace ordinary wake-time candidates,
Expand Down Expand Up @@ -177,3 +179,4 @@ a migration fallback. It never writes a duplicate name.

- [Alarm coordination](./alarm-coordination.md)
- [Durable Object lifecycle composition](./rfc-durable-object-lifecycle.md)
- [Tasks (née Fibers): durable replayable execution as a Lifecycle capability](./rfc-fibers.md)
41 changes: 41 additions & 0 deletions design/lifecycle-work-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,47 @@ terminal failure completes it, and the host re-derives its jobs from
durable state); capability jobs dispatch outside ambient host context. Host `onAlarm()` survives: it runs once per alarm invocation after
due jobs are driven. Host `getNextAlarm()` is removed.

## The dispatch contract

Four named rules define what a job owner can and cannot rely on:

1. **Job ids are scoped to their owner.** Every queue verb — push
included — sees only the owner's own jobs. A same-id push replaces the
owner's job; a push whose id belongs to another owner throws instead
of clobbering. (Tasks additionally prefixes its wake jobs `task:` so
caller-selected run ids stay inside its own namespace.)
2. **Dispatch must be bounded.** The event loop drives due jobs inline
and in order, so one long `onJob` delays every other job on the
object — this is the queue's biggest behavioral bet, learned the hard
way in the chat replatform. Detach unbounded work (start it, persist
durable evidence, return) rather than awaiting it in the hook. The
driver cannot safely abandon owner code, so the rule is enforced by
visibility: a dispatch that outlives the job's `hungTimeoutSeconds`
(default 30s) logs a warning and emits `job:slow_dispatch`.
3. **Newer pushes win over drive results.** Every dispatched job carries
a durable in-flight marker; a same-id `push()` or `reschedule()` made
while the job executes clears it, and `applyOutcome` only applies a
drive result to a still-marked job. The drive loop also refetches each
due job before claiming it, so a job replaced earlier in the same
alarm cycle dispatches with fresh data — or, if no longer due, is
skipped. An owner can therefore never lose a wake it explicitly
pushed mid-drive. Owners that both push and return outcomes for the
same job (Tasks) should derive both from the same durable state so
they always agree.
4. **Platform failures abort the drive loop.** A platform-class failure
(superseded isolate, memory-limit reset, platform transient) preserves
the failing job and re-throws, deferring the _remaining_ due jobs to
the platform's alarm retry. This is deliberate: platform failures are
properties of the isolate, not the job, so later jobs would fail the
same way, and the retry runs on a fresh invocation.

Everything else about drive order — in particular the interleaving of
different owners' jobs within one alarm cycle — is unspecified. Owners may
not depend on cross-owner ordering; lanes, fairness, or parallel dispatch
of independent owners can arrive later without a contract change. At-least-once
delivery is the only delivery guarantee: a crash between a job's side
effects and its outcome re-runs the job, so `onJob` must be replay-safe.

## The event loop

The loop lives in its own module: `lifecycle/job-queue.ts` holds the pure
Expand Down
Loading
Loading