From 25b16929322f5def024225f51a941e45283b4645 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Sat, 3 Oct 2026 14:43:46 -0700 Subject: [PATCH] feat(examples): keep alchemy-cloudflare Durable Objects running on pending I/O Move both Workers to compatibility date 2026-10-01. From that date, a pending call to another Durable Object, a container, `waitUntil` or a timer keeps a Durable Object in memory with no client connected, for up to 15 minutes per call. A turn waiting on a container command through the Computer is now covered without its HTTP client. Keepalive stays: pending I/O does not survive a deploy or crash, and its alarm is what wakes a cut-off session so it continues the turn. Its comments now say so, and drop the claim that outbound fetches did not count as activity. Co-Authored-By: Claude Opus 5.5 HumanLayer-Session: https://app.dev.codelayer.gg/sessions/01a0f3fc-13b0-758f-a343-fae04e83a7a9 --- examples/alchemy-cloudflare/src/ChatSession.ts | 4 ++-- examples/alchemy-cloudflare/src/Keepalive.ts | 10 ++++++---- examples/alchemy-cloudflare/src/Worker.ts | 4 +++- .../alchemy-cloudflare/src/computer/ComputerWorker.ts | 4 +++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/alchemy-cloudflare/src/ChatSession.ts b/examples/alchemy-cloudflare/src/ChatSession.ts index 4a89a54..96188e0 100644 --- a/examples/alchemy-cloudflare/src/ChatSession.ts +++ b/examples/alchemy-cloudflare/src/ChatSession.ts @@ -7,8 +7,8 @@ * The agent reads and changes the repos' files through fold's file tools, runs commands in the workspace's * shell through {@link bashTool}, and loads skills from each repo's `.claude/skills` and `.agents/skills`. * - * Every turn runs under a {@link Keepalive} lease so the object is not evicted mid-run. A turn can still - * be cut off - a crash, a deploy - leaving the root's user message with no finished run after it. When a + * A turn can be cut off - a crash, a deploy - leaving the root's user message with no finished run after it. + * Every turn runs under a {@link Keepalive} lease, whose alarm wakes an object cut off this way; when a * written log activates in that state, the object nudges the root to continue. * Fold fills any tool call the cut left without a result, and the model can resume a cut-off subagent * by id itself. A turn that keeps getting cut off stops being nudged after {@link MAX_RESTART_NUDGES}. diff --git a/examples/alchemy-cloudflare/src/Keepalive.ts b/examples/alchemy-cloudflare/src/Keepalive.ts index 9ff09a6..6effd6b 100644 --- a/examples/alchemy-cloudflare/src/Keepalive.ts +++ b/examples/alchemy-cloudflare/src/Keepalive.ts @@ -1,8 +1,10 @@ /** - * Keeps a Durable Object from being evicted while work runs. A turn's model calls are outgoing fetches, - * which do not count as activity, so a turn that outlives its caller could be evicted mid-run. Like the - * Cloudflare Agents SDK's `keepAliveWhile`, each piece of work holds a lease, and while any lease is held - * the object's alarm fires every {@link HEARTBEAT_MILLIS} and re-arms itself. + * Wakes a Durable Object that lost a running turn. Pending work - model calls, calls to the Computer, timers - + * keeps the object in memory without a connected client (compatibility date 2026-10-01 and later), but a + * deploy or crash can still end it mid-turn, and nothing else would wake it before the next message. So + * each piece of work holds a lease, and while any lease is held the object's alarm fires every + * {@link HEARTBEAT_MILLIS} and re-arms itself. An alarm that finds the object restarted wakes it, and the + * object continues the cut-off turn as it starts (see ChatSession). */ import * as Cloudflare from 'alchemy/Cloudflare' import { Clock, Context, Effect, Layer, Ref } from 'effect' diff --git a/examples/alchemy-cloudflare/src/Worker.ts b/examples/alchemy-cloudflare/src/Worker.ts index a9579f7..b616a9b 100644 --- a/examples/alchemy-cloudflare/src/Worker.ts +++ b/examples/alchemy-cloudflare/src/Worker.ts @@ -13,7 +13,9 @@ export default Cloudflare.Worker( 'ChatWorker', { main: import.meta.url, - compatibility: { flags: ['nodejs_compat'], date: '2026-08-31' }, + // 2026-10-01: pending calls to other Durable Objects, containers and timers keep a Durable Object in + // memory with no client connected, so a turn whose client has gone runs on. + compatibility: { flags: ['nodejs_compat'], date: '2026-10-01' }, }, Effect.gen(function* () { return { fetch: yield* HttpRouter.toHttpEffect(ChatRoutes) } diff --git a/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts index 9a92ef2..0debbf2 100644 --- a/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts +++ b/examples/alchemy-cloudflare/src/computer/ComputerWorker.ts @@ -10,7 +10,9 @@ import { COMPUTER_WORKER_NAME } from './Contract' export const ComputerWorker = Cloudflare.Worker('ComputerWorker', { name: COMPUTER_WORKER_NAME, main: './src/computer/Computer.ts', - compatibility: { flags: ['nodejs_compat'], date: '2026-08-31' }, + // 2026-10-01: pending calls to other Durable Objects, containers and timers keep a Durable Object in + // memory with no client connected, so a command whose caller has gone runs on. + compatibility: { flags: ['nodejs_compat'], date: '2026-10-01' }, env: { // The Computer Durable Object with its container. Named COMPUTER_BINDING: the shell and the container // call back into the Computer through this binding.