Coding Agents: The Transcript Is the Only State
In the first week of October Cloudflare put Claude Code, Codex and OpenCode behind one experimental harness interface, and Temporal ran the Pi coding agent as a Workflow whose Worker gets killed every 15 to 40 seconds. Both treat a session as a server workload: the transcript holds the truth, and a side effect cut off in flight is reported as interrupted or outcome unknown rather than retried blind.
A coding agent's turn usually ends in a tool call with a side effect: a push, a comment on the pull request, a message to the channel. The process hosting the agent sends that request, and before the reply lands the host evicts it, the pod is rescheduled or the laptop goes to sleep. The remote may well have done the work. Whatever restarts the run knows only that the call began, if it knows that much: the CLI's session file sits on the disk of the host that just died, and a run restarted on a fresh machine begins again from the prompt. Most loops then run the step again, and the comment appears twice.
QA Wolf described the small version on 1 October: an unsupervised agent investigates a failed test, files a bug report, and the machine dies before it can post the link in Slack. Run the job again and the team gets a second report. At fleet scale, on hosts that evict and reschedule as routine, that stops being an edge case, and in the first week of October the runtime vendors answered it. Cloudflare's Agents SDK 0.27.0, released on 7 October, added four experimental harnesses. Among them are ContainerHarness, which runs an agent CLI such as Claude Code or Codex in a Cloudflare Container, and OpenCodeHarness, which hosts OpenCode v2 in a Durable Object behind the same interface as the Pi harness. Temporal described pi-temporal in a post on 8 October: each Pi session is a Workflow, each step an Activity (each model call and tool call its own in stepped mode), and the demo kills whichever Worker is doing the work every 15 to 40 seconds.
The transcript is the only state
Each of these runs a session the way a workflow engine runs a long job: one instance that outlives any process. A turn is a model call followed by the tool calls the model asked for, and each is a step the engine records before it moves on. In pi-temporal's stepped mode (PI_TEMPORAL_STEPPED=1) the Activities are runModelCall, runToolCall and sealStep; by default one runStep Activity holds the whole step. Temporal's post rests on one sentence: “The transcript is the only conversation state.” On Cloudflare that transcript sits in the Durable Object's own SQLite, in tables prefixed pi_, and an evicted object is restarted by its alarm, after which Pi reopens its storage.
A session file on the dead host's disk is not state, and neither is the terminal the operator reads; the project files need a snapshot of their own, covered below. The CLI inside becomes a replaceable part. Cloudflare's harnesses share one interface whatever runs behind them, and Vercel's AI SDK keeps a harness package per CLI. The checkpoint idea is the one in the LangGraph guide; here the thing checkpointed is somebody else's CLI.
An interrupted call is outcome unknown
The hard case is the step that was running when the Worker died. A model call can be repeated at the cost of tokens. A tool call with a side effect cannot be assumed either way, which is why pi-temporal writes a dispatch claim beside the session file before the tool runs. A retry that finds the claim without a result reports an unknown outcome instead of repeating the call. Pi Durable draws the same line at the tool definition, where a tool marked replay: "safe" runs again after a crash and anything else is reported to the model as interrupted.
The server at the far end often decides which tools are safe. Reads and searches are. Pushing the same commit twice is harmless, because the second push finds the branch already there. GitHub refuses a second pull request from a head branch to a base that already has one. A comment, a message, an issue or a deploy simply happens twice. In Temporal (TypeScript SDK 1.24 below) that matrix is the retry policy on the Activity:
import { proxyActivities }
from '@temporalio/workflow';
import type * as A
from './activities';
type Acts = typeof A;
const UNKNOWN = 'outcome unknown';
// READ: safe to run twice; retried
const safe = proxyActivities<Acts>({
startToCloseTimeout: '5 minutes',
retry: { maximumAttempts: 5 },
});
// EFFECT: one attempt only
const once = proxyActivities<Acts>({
startToCloseTimeout: '10 minutes',
heartbeatTimeout: '30 seconds',
retry: { maximumAttempts: 1 },
});
export async function session(
id: string,
) {
for (;;) {
const step =
await safe.runModelCall(id);
if (!step.tool) return;
const run =
step.effect ? once : safe;
// CLAIM: started is on record
const result = await run
.runToolCall(id, step)
.catch(() => UNKNOWN);
// SEAL: the transcript decides
await safe
.sealStep(id, step, result);
}
}
The two proxies are the whole policy. An effect gets one attempt and a 30-second heartbeat: when its Worker dies the server stops hearing from it, declares the attempt timed out and, with no second attempt allowed, tells the Workflow instead. The history already holds the call as scheduled and started, which is the claim. The catch turns the failure into outcome unknown, sealStep writes that into the transcript, and the next model call runs a read of its own, git ls-remote or gh pr view, before it decides. The Activity must call heartbeat() while the tool runs, or the 30 seconds expire on a Worker that is alive and busy. A real session loop also calls continueAsNew every few hundred steps to keep the event history small.
A tool that fails must return, not throw
A rejected push reported by throwing looks like a dead Worker, and the catch above files it as outcome unknown. Tools return their failures as results. Once they do, a thrown ActivityFailure can only mean Temporal lost the attempt, and its cause says how, a TimeoutFailure when the heartbeat stopped.
A lease fences the worker that is not dead yet
A Worker that stopped answering may still be running. A partition leaves the old one mid-step while the engine hands the session to a new one, and for a while two processes believe they own the transcript. Pointed at shared storage through PI_SESSION_DIR, pi-temporal keeps the session file where every Worker reads the same record, and it gives the session a lease. A Worker that lost its lease cannot append to the session file, and the stale one's late result is refused rather than interleaved with the successor's. Snapshots make the swap physical: after each step the Worker writes a Git bundle into <session>.jsonl.tree/ beside the session file, and the next Worker restores it before it continues.
The proof is the chaos demo itself: Workers keep dying mid-step and the session finishes anyway. Run that kill test on your own harness before you trust it, and time the resume: a 30-second heartbeat plus a bundle restore is a frozen agent to the operator.
Watch a worker die mid call
Press Run turn below, then Kill worker whenever you like. One step runs on a Worker: the model call, a tool call that posts a comment on pull request 1832, then the seal. A second Worker takes over and reads the transcript before it does anything. Kill during the model call and the successor repeats it. With the claim on, kill during the comment and the successor finds a claim without a result, seals outcome unknown, checks the pull request and records what it finds. Switch to a plain retry and the comment can land twice. Kill after the seal and there is nothing left to redo.
Three places a run can live, priced by the wait
The engine decides what survives a swap; the hosting shape decides what you pay while a run waits and how it comes back.
- A Durable Object with SQLite. A conversation is one object, holding its sessions and their transcripts in the object's database, and eviction is answered by an alarm that restarts the job. A model request that streams for more than 15 minutes can be cut off, since an alarm invocation runs for 15 minutes at most. On the 2026-10-01 compatibility date or later, a pending operation keeps the object alive without a connected client for up to 15 minutes, and duration charges continue while it does.
- A Temporal Worker fleet. pi-temporal, Temporal's MIT-licensed experiment, keeps the position in the Workflow, and whichever Worker picks up the task rebuilds it from the event history. You run the Workers. A two-day approval wait costs one Workflow history in storage and nothing else. Budgets live in the executor, per turn and per session, with a hard deadline for the turn.
- A microVM per session. DigitalOcean's Managed Agents, in public preview since 22 September, runs each session in its own Firecracker microVM. It pauses the session when idle, measured as no outgoing model or tool calls, and stops CPU and memory charges while paused; the stored state stays billable.
Two of the three come with no gate on the tool call. Cloudflare's docs say so of the harness they host: “Pi Durable has no approval or permission step for tool calls yet.” pi-temporal has none either, though Temporal's post points to its experimental Agent Harness, whose tool approvals can pause for a human decision. DigitalOcean's runtime is the exception. It holds the run whenever an action resolves to ask under the session's permission policy, a shell command included, and an action no rule matches resolves to ask unless the policy sets another default. Recording the verdict so a crash can never replay a rejected action is the subject of the tool call policy guide.
Durability buys a fleet, and the fleet needs a referee
GitHub's post of 16 September on porting its Copilot runtime to Rust counts 832,378 lines of production Rust across 128 pull requests. A 25-hour parent session created 15 child sessions in seven waves over about three hours, each on its own branch with its own worktree and its own agent. Its lesson: parallel sessions over adjacent code need a designated coordinator or a human, and these had neither. GitHub's “agent merge” handled every porting pull request, but in most cases the engineer driving the port stopped short of the actual merge and spot-checked what the agent had done first.
Cockroach Labs' account of 9 October runs five roles, Triage Nurse, Fellow, Review Attending, Discharge Nurse and Chief of Medicine, under one rule: “No code before a plan, no plan without review.” The reviewer is told it is a different agent instance from the one that treated the case, and every review starts by disabling the change and confirming that the new tests fail. Five months produced 1,238 merged pull requests with 7 reverts, and the reason any of them merged can be rebuilt from its issue thread alone.
Takeaways
- Keep the transcript where the next machine can read it and snapshot the working tree after each step; the CLI inside is then a part you can swap.
- Decide per tool whether a repeat is safe and let the engine's retry policy carry it: one attempt for comments, messages, issues and deploys, retries for reads and for pushing the same commit again.
- A claim without a result is outcome unknown, and the successor checks the remote before it acts; QA Wolf's doubled bug report is the alternative.
- A lease keeps a partitioned Worker from appending a late result, and a kill test in rehearsal proves the arrangement.
- Price the hosting shape by what a waiting run costs, since a held Durable Object bills duration, a waiting Workflow bills history storage and a paused microVM bills stored state. Of the three, only DigitalOcean's runtime holds a tool call for approval, and its permission policy asks about any action no rule matches unless you set another default.
Would your coding agent survive its own host dying?
We host your agent harness as a durable session: the transcript in storage, one attempt for side effects, a kill test before go-live, and the runtime chosen for what it costs while a run waits.
Make the run durable