●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Make Agent Failures Reproducible: Deterministic Replay and Event Sourcing
An autonomous agent that fails at 2 a.m. can't be reproduced by simply running it again. Record every nondeterminism boundary as an append-only event log and replay the failed run deterministically — with working code and operational lessons.
I've been building apps on my own since 2014, and across more than 50 million cumulative downloads, the failures that hurt the most were never the loud crashes. They were the quiet ones: a backend automation that failed exactly once in the middle of the night, and by morning the logs no longer told me why.
Put an autonomous agent into production with the Claude Agent SDK and this problem takes on a sharper shape. Agents depend heavily on external state. The model's output differs slightly every time, the APIs your tools call return different data as hours pass, Date.now() naturally advances, and retry branches shift with jitter and latency. In other words, "just run the failed execution again" will never lead you back to the same failure.
Both of my grandfathers were miyadaiku — traditional shrine carpenters — and I grew up watching the things they assembled last for decades. Anything built to last shares one trait: it can be inspected after the fact. That's the property I care about most when designing agents, and this article is about exactly that — designing for after-the-fact reproduction, through deterministic replay and event sourcing, from both the implementation and operations sides.
Why "run it again" can't reproduce the failure
What blocks reproduction is the set of nondeterminism boundaries scattered through an agent run. The logic itself may be deterministic, but the moments where it touches the outside world receive different values every time. From what I've observed in production, those boundaries collapse into four kinds.
First, model output: even at temperature: 0, exact reproduction isn't guaranteed, and a single different character in a tool-call argument changes the branch taken. Second, tool I/O: external APIs, databases, and files change over time. Third, the clock: any branch based on Date.now() or the date walks down a different path on each run. Fourth, randomness and retries: jittered backoff and sampling wobble between runs.
This is where many teams install an observability stack — metrics, traces, dashboards — and call it done. But that shows you statistics about what happened; it doesn't let you run one specific execution again. Postmortems need the latter. Flip the approach: record every value that passes through those four boundaries into an append-only log, and on re-execution read those values back from the log. The run becomes deterministically reproducible. That's what it means to apply event sourcing to an agent.
Defining the event log schema
Start by issuing a unique runId for each execution, then append an event with a monotonic seq every time that run crosses a boundary. The crucial part is symmetry: in "record" mode the event stores the real value, and in "replay" mode it reads that value back.
// events.ts — append-only event type and a minimal storeexport type BoundaryKind = "model" | "tool" | "clock" | "random";export interface AgentEvent { runId: string; seq: number; // strictly increasing within a run kind: BoundaryKind; key: string; // logical key of the call (e.g. "tool:fetch_orders") input: unknown; // input at record time (used for matching) output: unknown; // the real value the boundary returned tokensIn?: number; // model events only tokensOut?: number; costUsd?: number; ts: string; // ISO8601 — record time, not the clock's true value}export interface EventStore { append(e: AgentEvent): Promise<void>; load(runId: string): Promise<AgentEvent[]>;}
In production, keeping the EventStore strictly append-only is the lifeline. Rewriting it midstream destroys reproducibility. I run two implementations: JSON Lines (one event per line) in object storage for production, and a plain array in development. Always carry kind and key in the schema — they're the keys the replay step uses to verify "same order, same call."
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Identify the nondeterminism boundaries (model output, tool I/O, clock, randomness) and record them as an append-only event log, with concrete code
✦A replay harness that reproduces a single failed run deterministically and entirely offline
✦An operational workflow that attributes cost and tokens per run from the same log
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Instead of calling a nondeterministic boundary directly, always route it through this single boundary() function. In record mode it runs the real work and writes the result to the log; in replay mode it never calls the real work and returns the value from the log.
// boundary.tsimport { AgentEvent, BoundaryKind, EventStore } from "./events";export type Mode = "record" | "replay";export class RunContext { private seq = 0; private cursor = 0; private recorded: AgentEvent[] = []; constructor( public readonly runId: string, public readonly mode: Mode, private readonly store: EventStore, private replayLog: AgentEvent[] = [], ) {} static async forReplay(runId: string, store: EventStore) { const log = await store.load(runId); return new RunContext(runId, "replay", store, log); } // every nondeterministic boundary passes through this one point async boundary<T>( kind: BoundaryKind, key: string, input: unknown, real: () => Promise<{ value: T; tokensIn?: number; tokensOut?: number; costUsd?: number }>, ): Promise<T> { const seq = this.seq++; if (this.mode === "replay") { const e = this.replayLog[this.cursor++]; if (!e || e.kind !== kind || e.key !== key || e.seq !== seq) { // branch diverged from record time = the replay premise is broken throw new ReplayDivergenceError(this.runId, seq, kind, key, e); } return e.output as T; } const r = await real(); const event: AgentEvent = { runId: this.runId, seq, kind, key, input, output: r.value, tokensIn: r.tokensIn, tokensOut: r.tokensOut, costUsd: r.costUsd, ts: new Date().toISOString(), }; await this.store.append(event); this.recorded.push(event); return r.value; }}export class ReplayDivergenceError extends Error { constructor(runId: string, seq: number, kind: string, key: string, got: AgentEvent | undefined) { super(`Replay diverged at run=${runId} seq=${seq} expected ${kind}:${key} got ${got?.kind}:${got?.key}`); this.name = "ReplayDivergenceError"; }}
Deliberately throwing ReplayDivergenceError is the part of this design that earns its keep. If replay encounters a different order or a different call than was recorded, it tells you, at that exact moment, whether the code logic changed or the recording was incomplete. That's far better for a postmortem than silently returning a plausible value: it points at the divergence by seq, so you reach the root cause dramatically faster.
Routing the model call and tools through the boundary
From here it's just a matter of wrapping the model call and tool execution in boundary(). At record time the Claude request actually hits the Messages API; at replay time it returns the recorded response as-is.
// model.tsimport Anthropic from "@anthropic-ai/sdk";import { RunContext } from "./boundary";const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY }); // e.g. "YOUR_API_KEY"// approximate cost (Sonnet 4.6 example; refresh the unit prices from the pricing page)const PRICE_IN = 3.0 / 1_000_000; // USD / input tokenconst PRICE_OUT = 15.0 / 1_000_000; // USD / output tokenexport async function callClaude(ctx: RunContext, messages: Anthropic.MessageParam[]) { return ctx.boundary("model", "claude:messages", { messages }, async () => { const res = await client.messages.create({ model: "claude-sonnet-4-6", max_tokens: 2048, temperature: 0, messages, }); const tin = res.usage.input_tokens; const tout = res.usage.output_tokens; return { value: res, tokensIn: tin, tokensOut: tout, costUsd: tin * PRICE_IN + tout * PRICE_OUT, }; });}
Tools follow the same pattern. Treat the clock and randomness as boundaries too, and enforce one discipline: agent code must never call Date.now() or Math.random() directly.
Hold that discipline and the agent's main loop — call the model, run a tool, call again — works unchanged for both recording and replay. Your production code and your postmortem code become one and the same, which structurally eliminates the nastiest mismatch of all: writing a separate debug path that quietly behaves differently from production.
Replaying a single failed run
This is the operational goal. When runId=run_2026_0531_0203 fails at 2 a.m., the next morning I reproduce that one execution offline on my own machine.
// replay.tsimport { RunContext } from "./boundary";import { jsonlStore } from "./store";import { runAgent } from "./agent"; // the exact same loop used in productionasync function replay(runId: string) { const ctx = await RunContext.forReplay(runId, jsonlStore); try { const result = await runAgent(ctx); // touches no network and no LLM console.log("replay complete:", result); } catch (e) { if (e instanceof Error && e.name === "ReplayDivergenceError") { console.error("divergence detected — code change or missing recording:", e.message); } else { console.error("reproduced the failure at the same point as record time:", e); } }}replay(process.argv[2]);
Because replay touches nothing external, you can run it as many times as you like, setting breakpoints wherever you want. You can separate whether the failure came from model output that wobbled even at temperature: 0, from a specific gap in the order data, or from a clock-based branch that only fires at night — all without changing a single line of code. In my own experience, intermittent failures that used to take two or three days to isolate now generally get pinned down the same morning once replay is in place.
Attributing cost per run from the same log
This log isn't only for postmortems — it doubles as your cost attribution source. Since model events carry tokensIn / tokensOut / costUsd, aggregating a run tells you which job spent how many tokens for how much money.
You don't need a separate dashboard — the execution log itself becomes the primary source. In the backend automation for my wallpaper apps, I run this aggregation at the start of each month and consistently find one or two runs burning three times the expected tokens. Most turn out to be a retry that looped or a context stuffed with unnecessary history, and in both cases opening the model input at the offending seq in replay makes the cause obvious at a glance.
Things that actually tripped me up in production
A few notes you only learn by operating this, not from the official samples.
First, handling personal data. Recording the input / output of a boundary verbatim means customer data ends up in tool outputs. Run a per-key masking function just before append() and redact PII that isn't needed for reproduction. What reproduction needs is the value that decided a branch, which is often not the raw personal data itself.
Second, schema evolution. Adding fields to AgentEvent can break replay of older runIds. Carry a schemaVersion on each event and keep an adapter on the replay side that can read older versions. Just as a shrine carpenter leaves joints that anticipate future repair, building in consideration for whoever inspects this later — usually your future self — ends up being the cheapest path.
Finally, retention. Keeping every run forever bloats storage, so my default is "successful runs for 7 days, failed runs for 90 days." Failures live longer because the subject of a postmortem is always the failing side. Embedding the date in the runId also makes automatic deletion via a lifecycle policy straightforward to express.
A minimal rollout order, and what I recommend
If you're retrofitting this onto an agent that's already running, I recommend this order. Don't try to do it all at once — close one kind of boundary at a time.
Introduce RunContext and boundary(), and route only the model call first (smallest blast radius, biggest payoff).
Grep for direct Date.now() and Math.random() calls and replace them with clock / random boundaries.
Wrap external-I/O tools in a tool boundary one at a time. For non-idempotent side effects (payments, sends), execute only at record time and return the logged value on replay.
Make the EventStore append-only JSON Lines and carry schemaVersion from day one.
Always include the failing runId in alerts, and provide a one-command path to reproduce it via replay.ts.
As a guiding recommendation, pin temperature to 0 for both record and replay. It won't guarantee exact reproduction, but keeping the setting identical between record and replay reduces false divergence detections. For retention, default to "short for success, long for failure," and when in doubt keep the failing side — that's my conclusion on the best cost/benefit for postmortems. Conversely, I'd advise against uniformly long retention for every run, since it rarely justifies the storage cost.
Deterministic replay isn't a flashy feature. But being able to take a failure that happened once, unseen, in the dead of night, faithfully reproduce it the next morning, and actually reach its cause — that quiet confidence is a solid foundation for software meant to run for a long time. As a next step, find one place in your current automation where you call Date.now() directly and replace it with boundary("clock", ...). That single change is your first doorway into a reproducible design.
If you're wrestling with the reproducibility of your own nightly jobs, I hope this helps. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.