Replay

Check that your real threads still reproduce, byte for byte, after an upgrade. No model calls, no API keys, no cost.

Every model request a thread makes is recorded: the exact bytes that were sent, and the events they were built from. replay() rebuilds each of those requests from the log with the code you are running now and checks that the bytes match. It calls no model and no tool, starts no sandbox and writes nothing, so it is free and safe to run on production data.

Use it as the first step of your evals: before you stub-run saved cases or pay for a judge, prove the new build still sees your threads the way the old one did.

Replay one thread

import { openThread, sqlite } from "@threads/core";

const store = sqlite("./threads-data");
const opened = await openThread(store, threadId);
if (!opened.ok) throw new Error(opened.error.message);

const replayed = await opened.value.replay();
if (!replayed.ok) {
  console.error(replayed.error.code, "at seq", replayed.error.seq);
}

A thread that made no model request replays trivially. A fork replays its parent's history too.

Run it in CI

Keep a corpus of real threads (export them with threads export, or point at a copy of a production store) and replay every one on each pull request. A failure names the thread, the check that failed and the seq of the request, so you can open that request in the timeline.

import { expect, test } from "bun:test";
import { openThread, sqlite } from "@threads/core";
import { corpus } from "./corpus"; // your list of thread ids

const store = sqlite("./fixtures/threads");

for (const id of corpus) {
  test(`thread ${id} still reproduces`, async () => {
    const opened = await openThread(store, id);
    if (!opened.ok) throw new Error(opened.error.message);
    const replayed = await opened.value.replay();
    expect(replayed).toEqual({ ok: true, value: undefined });
  });
}

The TypeScript and Python packages write the same bytes, so a corpus recorded by one replays in the other.

What a failure means

CodeWhat changed
prefix_changedThe request's first line (system prompt, tools, model and adapter settings) no longer matches the settings it was recorded under. Usually an adapter or tool-schema rendering change.
request_hash_mismatchThe rest of the request renders differently: how messages, tool results, summaries or instructions are laid out has changed.
artifact_missingA stored blob the request needs (a tool result, an image, a summary) is gone from the store.
artifact_corruptA stored blob is there but its bytes changed.
log_corrupt, unsupported_format, unsupported_critical_eventThe log itself can't be read: it was edited, or it was written by a newer version of threads.

The first failure wins: replay() stops at the earliest request that doesn't reproduce and reports its seq.

Replay checks the requests, not the answers. To check that the agent still behaves well on the same inputs, fork a thread in stub mode or run a saved case.

Edit on GitHub

On this page