# Checkpointing (/docs/checkpointing)



A checkpointer snapshots every channel in the session and lets you
restore to any prior point. The substrate uses checkpoints for
automatic rollback on error; consumers use them for time-travel
debugging, eval, and replay.

Checkpoint is one of three concepts in the
[state-and-persistence cluster](/docs/storage#the-state-and-persistence-cluster) —
alongside the [storage adapter](/docs/storage) and the
[sync version vector](/docs/sync) — that together carry session
state across restarts, rewinds, and concurrent writers. This page
covers the rewind axis: per-channel snapshots written at message
and event boundaries, restorable in place via
`runtime.checkpoints.rollback` or branchable via
`TimeTravelAPI.fork`.

```typescript
import {
  MemorySaver,
  IndexedDBSaver,
  SupabaseSaver,
  createSupabaseSaver,
  type SupabaseSaverConfig,
} from "@pleach/core/checkpointing";
```

Prefer `createSupabaseSaver(config)` over `new SupabaseSaver(config)` —
the factory wires schema and RLS context correctly. The plain
constructor is fine for tests but skips that plumbing.

<SourceMeta subpath="@pleach/core/checkpointing" source="{ label: &#x22;src/checkpointer/&#x22;, href: &#x22;https://github.com/pleachhq/core/tree/main/src/checkpointer&#x22; }" />

## Picking a checkpointer [#picking-a-checkpointer]

| Checkpointer     | Environment | Pairs naturally with               |
| ---------------- | ----------- | ---------------------------------- |
| `MemorySaver`    | Any         | `MemoryAdapter` (tests, dev)       |
| `IndexedDBSaver` | Browser     | `IndexedDBAdapter` (offline-first) |
| `SupabaseSaver`  | Server      | `SupabaseAdapter` (production)     |

You can mix-and-match across storage and checkpointer types, but
the natural pairings above are what `HARNESS_MOCK_MODE=true` wires
and what production setups land on.

<Callout type="info" title="Provider-agnostic Postgres">
  On a non-Supabase Postgres (node-postgres, Neon, RDS, pglite), pair the
  provider-agnostic [`PgStorageAdapter`](/docs/storage#pgstorageadapter-provider-agnostic-postgres)
  and [`createPgEventLogWriter`](/docs/storage#durable-event-log-writer) — both
  take an injected `PgClientLike`. Cross-restart durability requires wiring a
  durable storage adapter **and** event-log writer; without them the runtime
  defaults to in-memory and loses state on restart. A shipped Postgres
  `Checkpointer` is not bundled yet — implement the `Checkpointer` interface
  against your `PgClientLike` if you need durable *checkpoints* on the same store
  (the [durable local-dev store](/docs/storage#durable-local-dev) is the
  executable reference).
</Callout>

## Durability levels [#durability-levels]

Every checkpoint carries a `_durability` stamp set by the
checkpointer **after** the underlying write acknowledges. The
stamp lets the runtime know which side of a partition the
checkpoint actually survived.

| Checkpointer     | `_durability.level` | When the stamp fires                               | Survives                                                                     |
| ---------------- | ------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------- |
| `MemorySaver`    | `"memory"`          | Synchronously, immediately on `put()`              | Process restart? No.                                                         |
| `IndexedDBSaver` | `"disk"`            | After the IDB transaction's `complete` event fires | Browser refresh? Yes. Tab close + reopen? Yes. Profile wipe? No.             |
| `SupabaseSaver`  | `"replicated"`      | After the server-side `insert` returns 200         | Process restart? Yes. Region failover? Depends on your Postgres replication. |

This is pleach's shape for the same trade LangGraph documents as
`durability: "exit" | "async" | "sync"`. LangGraph offers the dial
**per write** against the same checkpointer; pleach pushes the
choice to the checkpointer constructor and stamps the outcome on
every checkpoint. The reason: a per-write dial means a single
session can have some checkpoints durable and others not, which
makes "did this turn survive the crash" a runtime decision.
Pleach's stamp-after-ack discipline means inspecting any
checkpoint tells you the durability level deterministically.

If you need LangGraph's per-write dial behavior — async writes for
hot loops, sync writes at the turn boundary — wrap a checkpointer
that batches `put()` calls and flushes on a signal. The
contract intentionally doesn't ship this out of the box; the
operator-visible discipline is "one checkpointer, one durability
level per session."

## Wiring a checkpointer [#wiring-a-checkpointer]

```typescript
import { SessionRuntime } from "@pleach/core";
import { createSupabaseAdapter } from "@pleach/core/sessions";
import { createSupabaseSaver } from "@pleach/core/checkpointing";

const runtime = new SessionRuntime({
  storage:      createSupabaseAdapter(supabase),
  checkpointer: createSupabaseSaver(supabase),
  userId:       "user_123",
});
```

`SupabaseSaverConfig` exposes `tableName` and a `maxCheckpoints` cap;
the saver auto-prunes on every `put` to keep the per-session
checkpoint count under that ceiling. Pass `maxCheckpoints: 0` to
disable auto-prune and manage retention yourself.

Once wired, the runtime writes a checkpoint at message and event
boundaries — each tells you what triggered it through
`CheckpointMetadata.source` (`"message"`, `"tool_execution"`,
`"job_complete"`, `"subagent"`, `"sync"`, or `"manual"`). You don't
have to call anything; the checkpoints exist by the time the turn
lands. Each write fires a `checkpoint.created` stream event carrying
the full `Checkpoint` envelope — the same event the React DevTools
surface listens for to populate its checkpoint picker. The `source`
on the envelope is what tells a UI "this checkpoint was written
after a tool execution" versus "this one was written on the inbound
message."

## Listing checkpoints for a session [#listing-checkpoints-for-a-session]

`runtime.checkpoints.list` is an async generator yielding
`CheckpointSummary` records, newest first.

```typescript
for await (const cp of runtime.checkpoints.list(sessionId)) {
  console.log(cp.id, cp.createdAt, cp.source);
  // source: "manual" | "message" | "tool_execution" | "job_complete" | "subagent" | "sync"
}
```

Pass a `limit` as the second argument to cap iteration without
draining the underlying generator.

## Rolling back to a checkpoint [#rolling-back-to-a-checkpoint]

```typescript
const restored = await runtime.checkpoints.rollback(sessionId, checkpointId);
```

The runtime rebuilds session state — messages, in-flight tool calls,
pending jobs, planning context — to exactly what it
was at the checkpoint, bumps its version vector, and writes a NEW
checkpoint with `source: "manual"`, `writes: ["rollback"]`, and
`parentCheckpointId` set to the rollback target. The next
`executeMessage` continues from the restored state.

> **Beta — channel-level re-execution replay.** Rollback restores the
> persisted session *state* (durable checkpoints are battery-proven).
> Reconstructing every in-memory channel to *re-run* the turn
> deterministically from the checkpoint is a completeness slice still
> in beta — the runtime wires a minimal channel projection today
> (`createChannels: () => ({})`). Treat the continuation as
> state-restore, not deterministic channel-replay.

### Rollback vs fork [#rollback-vs-fork]

The two shapes look similar but answer different questions.

| Operation                 | Same `sessionId`?                 | Reach for it when                                                                                                                                                            |
| ------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rollback(sessionId, cp)` | Yes — session is mutated in place | A tool ran with bad args (e.g. `search_corpus` called with the wrong `query`) and you want to retry from the pre-call channel state without losing the user's prior messages |
| `fork(sessionId, cp, …)`  | No — new `sessionId` is minted    | You want to keep the original transcript intact and explore an alternate continuation (eval branch, "what if the planner had picked `classify` instead of `summarize`")      |

Rollback is destructive on the live session; fork is non-destructive
and gives you two siblings sharing a common parent checkpoint.

The flat methods `runtime.rollbackToCheckpoint(...)` and
`runtime.listCheckpoints(...)` remain callable but are deprecated —
prefer the `runtime.checkpoints` facet.

## Time-travel from React DevTools [#time-travel-from-react-devtools]

In a React app with `useHarnessDevTools()` wired, time-travel is a
one-liner from the browser console:

```javascript
__HARNESS_DEVTOOLS__.checkpoints();
// → [{ id: "cp_018f...", source: "tool_execution", createdAt: ... }, ...]

__HARNESS_DEVTOOLS__.rollback("cp_018f...");
// session state reverts; UI re-renders against the restored state
```

See [DevTools](#devtools) below for the full surface.

## Branching: fork a session from a checkpoint [#branching-fork-a-session-from-a-checkpoint]

`TimeTravelAPI` is the substrate-shipped fork surface. Construct it
once from any `Checkpointer` and call `fork` to spawn a new session
id pointing at the same state envelope.

```typescript
import { TimeTravelAPI } from "@pleach/core/time-travel";

const api = new TimeTravelAPI(checkpointer);

const forked = await api.fork(sourceSessionId, checkpointId, crypto.randomUUID());
```

The forked checkpoint records `source: "fork"` and a `forkedFrom`
metadata block carrying the source session id + parent checkpoint id
so the lineage stays queryable from the audit ledger.

## The `Checkpointer` interface [#the-checkpointer-interface]

All three savers implement the same contract. Write your own
(Redis, S3, filesystem) by implementing it:

```typescript
import type {
  Checkpointer,
  Checkpoint,
  CheckpointMetadata,
  CheckpointListOptions,
  PendingWrite,
} from "@pleach/core";

interface Checkpointer {
  put(
    threadId: string,
    state: SessionState,
    metadata: CheckpointMetadata,
  ): Promise<Checkpoint>;
  get(threadId: string, checkpointId?: string): Promise<Checkpoint | null>;
  list(
    threadId: string,
    options?: CheckpointListOptions,
  ): AsyncIterable<Checkpoint>;
  delete(threadId: string, checkpointId: string): Promise<void>;
  prune(threadId: string, keepLast: number): Promise<number>;
  putWrites?(
    threadId: string,
    writes: PendingWrite[],
    taskId: string,
  ): Promise<void>;
}
```

`threadId` here is the session id — the parameter name carries over
from the LangGraph-derived checkpointer contract. `list` is an
`AsyncIterable`, not an array; iterate with `for await`. `prune`
returns the number of checkpoints deleted and keeps the most recent
`keepLast` (it does NOT take an "after" cursor). A typical retention
job runs `prune(sessionId, 50)` per session at the end of each turn
— `SupabaseSaver` already does the equivalent via `maxCheckpoints`,
so you only call `prune` directly when wiring a custom `Checkpointer`
against a store the runtime can't auto-prune for you.

A `Checkpoint` is a typed envelope carrying the session state
plus per-channel snapshots; the shape is part of the
language-agnostic contract that keeps the Go and TypeScript
implementations in sync.

## DevTools [#devtools]

Wire the DevTools hook once near your provider:

```tsx
import { useHarnessDevTools } from "@pleach/core/react";

function App() {
  useHarnessDevTools();
  return <HarnessProvider runtime={runtime}>...</HarnessProvider>;
}
```

Then from the browser console:

```javascript
__HARNESS_DEVTOOLS__.session;               // current SessionState
__HARNESS_DEVTOOLS__.checkpoints();         // list checkpoints
__HARNESS_DEVTOOLS__.rollback("cp_...");    // restore to a checkpoint
__HARNESS_DEVTOOLS__.tools();               // list registered tools
__HARNESS_DEVTOOLS__.syncStatus();          // version vectors
__HARNESS_DEVTOOLS__.forceSync();           // push a sync now
```

Gate the hook call behind `process.env.NODE_ENV !== "production"`
to keep the DevTools surface out of production bundles. A typed
`HarnessDevToolsAPI` is exported for typing
`window.__HARNESS_DEVTOOLS__` in consuming apps.

## Where to go next [#where-to-go-next]

<Cards>
  <Card title="Storage" href="/docs/storage" description="Pair the checkpointer with the right storage adapter." />

  <Card title="SessionRuntime" href="/docs/session-runtime" description="The constructor field for wiring a checkpointer." />

  <Card title="Stream events" href="/docs/stream-events" description="`checkpoint.created` and related events fired during execution." />
</Cards>
