Joan Comadran
Field notes

AI & Dev Trends

Durable AI agents: what happens when your agent dies mid-task

Vercel just open-sourced durable-harness to keep Claude Code, Codex and other harnesses alive through crashes and timeouts. The pattern underneath matters more than the package: a step log, a queue, and tools that know whether they can run twice.

October 21, 202610 min read

On October 19, Vercel published Making every harness durable and open-sourced durable-harness, a package that keeps coding-agent harnesses (Pi, OpenCode, fx, Claude Code and Codex) running through crashes, function timeouts and redeploys. The fine print first, because it matters: the README calls it experimental, it's on 0.0.x (0.0.1-beta.6 on npm as of October 21, 2026), and the API may change between releases. The Claude Code and Codex adapters are labelled experimental on top of that.

So don't rebuild your stack on it this week. But the problem it solves is old and boring and will outlive the package: an agent is a loop that keeps its progress in memory, and memory dies with the process. Durable AI agents are the fix for that, and you can apply the idea with or without Vercel.

What a durable AI agent actually is

A durable agent records what it has done somewhere other than the process doing it. Vercel's durable agents guide puts the distinction well: conversation history preserves model context, execution records establish which operations completed.

That second part is the one people skip. A saved transcript lets the agent remember what was said. It doesn't tell you whether the refund went through or the email left before the function got killed. An agent restarting with only its transcript can redo everything (paying for the tokens again, sending the email again) or guess. Both are bad.

How agents actually die

Nothing exotic. A function hits its maxDuration, the instance runs out of memory, you deploy mid-turn, or the browser tab closes and takes the stream with it. The longer the turn, the sooner it meets one of these.

I hit the cheap version of this with Claient, my WhatsApp bot. Its webhook runs on a serverless function with maxDuration = 60, and one incoming message can trigger several Claude calls. When the function got killed after sending the reply but before marking the message as handled, Meta retried the webhook and the lead got the same answer twice. I wrote up the fix, a time budget plus idempotency per message, in the WhatsApp bot post. That's a 60-second turn. Coding agents run for minutes or hours and wait on CI, so they get the same bug with more zeros.

The pattern: intent, outcome, queue

Here's how durable-harness does it, per Vercel's harness docs as of October 21, 2026. It's the same shape Inngest, Restate and Temporal use for their durable agents, so it's worth understanding once.

  1. Before each model call or tool call, write an intent to a durable log.
  2. After the call finishes, write the outcome.
  3. Back the whole turn with a queue message. If the function dies, the queue hands the turn to a new function, which reads the log and continues.

After a crash, the log tells you three things: what finished, what was running, and what never started. Finished calls are read back from the log and never run again, which is why you aren't billed twice for a model call that already completed. A model call that was running just runs again. Between calls, nothing repeats.

The interesting case is the tool that was running. The docs are honest about an edge you should design for: a call that finished just before the crash may not be in the log yet, and then it counts as running. In other words, "running" means "maybe done". That's the whole reason replay rules exist.

The replay rule is the actual decision

In durable-harness, every tool you give the agent declares what happens if a crash interrupts it. 'safe' means it can run again. 'never' means it won't run twice: the model gets told the call may or may not have happened, and it decides whether to check first. Here's what that looks like for an agent that publishes blog posts, where a duplicate is embarrassing rather than expensive:

ts
import { pi } from 'durable-harness/pi';
import type { HostTool } from 'durable-harness';
import { findPostBySlug, publishPost } from './cms'; // your own code

const tools: HostTool[] = [
  {
    name: 'find_post',
    description: 'Checks whether a post with this slug already exists',
    replay: 'safe', // a read: running it twice costs nothing
    inputSchema: { type: 'object', properties: { slug: { type: 'string' } }, required: ['slug'] },
    execute: async ({ slug }) => JSON.stringify(await findPostBySlug(slug)),
  },
  {
    name: 'publish_post',
    description: 'Publishes a post. Call find_post first if unsure.',
    replay: 'never', // a write: never rerun automatically
    inputSchema: { type: 'object', properties: { slug: { type: 'string' } }, required: ['slug'] },
    execute: async ({ slug }) => JSON.stringify(await publishPost(slug)),
  },
];

export const agent = pi({ model: 'anthropic/claude-sonnet-5.5', tools });

Notice the pair. A 'never' tool without a 'safe' way to check its effect leaves the model with "possibly ran" and nothing to do about it, so it either retries blindly or gives up. Give every write a matching read.

Then admit that 'never' isn't exactly-once. It means the harness won't rerun the call on its own. It can't un-send something that left the building half a second before the crash. Vercel's guide answers its own FAQ "Does durable execution prevent duplicate tool actions?" with "No", and recommends an idempotency key the receiving service enforces. Its Workflow SDK example uses the step ID, which stays stable across retries of the same step:

ts
import { getStepMetadata } from 'workflow';

export async function sendInvoice(invoiceId: string) {
  'use step';
  const { stepId } = getStepMetadata();

  const res = await fetch(BILLING_API, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Idempotency-Key': `invoice:${stepId}` },
    body: JSON.stringify({ invoiceId }),
  });
  if (!res.ok) throw new Error(`Billing failed: ${res.status}`);
  return res.json();
}

Step IDs differ between runs, so to dedupe across separate runs you need a business identifier or a unique constraint in your database. Claient does the latter: each WhatsApp message ID is claimed atomically in Postgres before processing, with a cap of three attempts so a message that always fails gets abandoned instead of retried forever. It's also one more argument for what I wrote in MCP vs function calling: tools that write to your own systems are easier to make replay-safe when they live in your own code.

Where durable-harness runs each harness

HarnessRuns in
Pi, OpenCode, fxYour function, with files in a virtual workspace or a Vercel Sandbox
Claude Code, Codex (experimental)Only a Vercel Sandbox
eveIts own app, durable by default

The log lives behind a World, the storage-and-queue interface of the open-source Workflow SDK: Vercel's on Vercel, files on disk locally, or Postgres if you self-host, which per the blog post takes two environment variables. Claude Code and Codex are kept out of shared functions because they expect a real shell and filesystem and aren't safe for multi-tenant use. The virtual workspace is a bash-compatible shell with no network and no real binaries, and the docs say it holds about 500 files, so it's for small tasks, not your monorepo.

One documented limitation worth reading before you trust it with builds: with Pi and OpenCode, a turn starts in the chat route's own request, which drives it for up to 60 seconds before the queue route takes over, and a command still running at that handover is stopped, not rerun. A long npm install early in a turn can get cut off. The workaround in the docs is drive: 'queue' on agent.chatRoute(), and Vercel says a fix is on the way.

If you're writing your own agent loop rather than wrapping a coding harness, the same ideas come as 'use workflow' and 'use step' in the Workflow SDK, with WorkflowAgent from @ai-sdk/workflow for the AI SDK loop. Or Inngest, Restate or Temporal if you're not on Vercel. The rule that carries over everywhere: one step boundary per model call and per tool call. Wrapping the whole loop in a single step records nothing until the loop ends, which is durability in name only.

When you don't need any of this

Plenty of agents are request-response: a message comes in, a few model calls and tools run, an answer goes out, all in seconds. For those, a queue plus a step log is infrastructure you'll maintain for a failure you could handle with a retry. Claient's webhook doesn't use a workflow engine. Its 60-second webhook gets a time budget, an iteration cap on the tool loop and message-level idempotency, and the project notes list a queue as the next step only if load grows.

You need durability when a turn can outlive its process: tasks measured in minutes or hours, waits on CI or on a human approval, or crashes that would burn real money in repeated tokens. Idempotent side effects you need either way. That part isn't optional at any size.

Checklist before an agent gets write access

  1. List every tool and mark it safe or never. Reads are safe; anything that sends, charges, publishes or appends is never.
  2. Give every never-tool a safe tool that checks whether its effect already happened.
  3. Push dedup to the receiver: an idempotency key, or a unique constraint on a business ID.
  4. One step boundary per model call and tool call.
  5. Cap iterations and spend, because a resumed agent can still loop. I covered caps in Claude Code cost per month and keeping Claude's token bill under control.
  6. Kill it on purpose. Vercel's guide suggests interrupting a run between steps and retrying an external action to confirm nothing duplicates. If you've never seen your agent recover, you don't know that it does.
  7. Plan for deploys. On Vercel, existing runs stay pinned to the deployment that started them, so your database schema has to stay compatible with code that may resume later.

FAQ

What is a durable AI agent?

An agent whose progress is recorded outside the process running it. Each model call and tool call goes to a log, so after a crash, timeout or redeploy another process reuses the finished results and continues from the first unfinished call. Chat history alone isn't durability: it records what was said, not what actually ran.

Does durable execution prevent duplicate tool calls?

Not on its own. It stops completed steps from running again, but a call in flight when the process died may already have reached the outside world. For emails, payments or publishing you still need an idempotency key or a unique constraint on the receiving side, and a way for the agent to check.

What is Vercel durable-harness?

An experimental open-source package from Vercel (Apache 2.0, 0.0.1-beta.6 on npm as of October 21, 2026) that makes Pi, OpenCode, fx, Claude Code and Codex durable. It logs every call to a Workflow SDK World (Vercel, local files or Postgres), backs each turn with a queue message, and lets each tool be marked safe or never to replay.

Do I need durable execution for my AI agent?

Only if a turn can outlive its process: tasks that take minutes or hours, waits for CI or a human, or crashes that would cost real money in repeated tokens. A request-response agent that finishes in seconds usually needs a time budget, an iteration cap and idempotent side effects, not a workflow engine.

Durable execution is one of those things that sounds like a framework feature and is really a discipline: write down what you did before you claim you did it, and never trust "probably ran". It's also why every write-action shows up in what an AI agent costs to build. If you want an agent built that way from day one, here's how I work.

Work with me

Want this in your product?

I build AI agents, apps and websites for companies.

Let's talk →

Keep reading

03