Checkpoints

Persist graph runs across HTTP requests with a checkpointer. Use memorySaver() for demos, or implement Checkpointer against your store.

memorySaver

checkpointer.ts
import { graph, memorySaver } from "@monorch/ai";

const checkpointer = memorySaver();

const refund = graph("refund")
  .node("lookup", async ({ input }) => `order:${input.orderId}`)
  .interrupt("approve")
  .node("pay", async ({ outputs }) => `refunded:${outputs.lookup}`)
  .compile({ checkpointer });

await refund.start({ orderId: "ord_9" }, { threadId: "t1" });

// later request
const run = await refund.restore("t1");
await run.resume("approved");

Postgres adapter

Use @monorch/ai/postgres with a pg pool (optional peer dependency). Call ensureMonorchSchema once on boot.

postgres-checkpointer.ts
import pg from "pg";
import { graph } from "@monorch/ai";
import { ensureMonorchSchema, postgresCheckpointer } from "@monorch/ai/postgres";

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
await ensureMonorchSchema(pool);

const refund = graph("refund")
  .node("lookup", async ({ input }) => `order:${input.orderId}`)
  .interrupt("approve")
  .node("pay", async ({ outputs }) => `refunded:${outputs.lookup}`)
  .compile({ checkpointer: postgresCheckpointer(pool) });

Checkpointer interface

interface.ts
type Checkpointer = {
  put(threadId: string, blob: JsonValue): Promise<CheckpointTuple> | CheckpointTuple;
  get(threadId: string): Promise<JsonValue | null> | JsonValue | null;
  list?(threadId: string): Promise<CheckpointTuple[]> | CheckpointTuple[];
};

BYO store

redis-checkpointer.ts
export function redisCheckpointer(redis: Redis): Checkpointer {
  return {
    async put(threadId, blob) {
      const checkpointId = crypto.randomUUID();
      await redis.set(`cp:${threadId}`, JSON.stringify({ checkpointId, blob }));
      return { threadId, checkpointId, blob, createdAt: new Date().toISOString() };
    },
    async get(threadId) {
      const raw = await redis.get(`cp:${threadId}`);
      return raw ? JSON.parse(raw).blob : null;
    },
  };
}

Thread ids

Pass threadId on start. Without a checkpointer, restore throws. Without a thread id, in-process handles still work until the process dies.

Checkpoint blob v2

Every export writes version: 2. Fields (camelCase in JSON):

blob-v2.ts
{
  version: 2,
  defHash: "…",           // FNV-1a fingerprint of the compiled GraphDef
  input: { orderId: "ord_9" }, // original start input
  run: {
    id, graph, status, cursor, steps,
    input, state, defHash, outputs, error?, routeFrom?
  }
}

After a definition replace, restore / resume can fail on hash mismatch. Codes and recovery: Errors & failure modes.

Migration & compatibility

Reading: the engine accepts checkpoint version 1 and 2. Unknown versions are rejected. On import, missing top-level input / defHash are backfilled from run (the v1 shape).

Writing: new checkpoints are always v2. You do not need an offline rewrite job for v1 blobs — restore still works if the graph is registered and defHash matches.

Empty defHash: restore fails. Re-run or re-checkpoint after upgrading from a build that did not stamp hashes.

Graph shape changes (app-level):

migrate-thread.ts
// When you must change nodes / interrupts incompatible with old defHash:
// 1. Keep the old graph name registered until waiting threads finish, OR
// 2. Start a new threadId for the new definition and mark the old one abandoned.
//
// Avoid compile({ replace: true }) while production threads still wait on the old hash
// unless you accept restore/resume failures (GRAPH_FAILED / def_hash mismatch).

async function resumeOrRestart(
  compiled: { restore(id: string): Promise<{ resume(d?: string): Promise<unknown> }>; start(input: object, opts: { threadId: string }): Promise<unknown> },
  threadId: string,
  input: object,
) {
  try {
    const run = await compiled.restore(threadId);
    return run.resume("approved");
  } catch {
    // def_hash mismatch or missing blob → start fresh for this customer
    return compiled.start(input, { threadId: `${threadId}:vNext` });
  }
}

Prefer additive graph changes (new optional nodes behind edges) over renames. Treat defHash as the contract between stored threads and the compiled definition.

Future formats (v3+) will be documented here with an explicit read path. Until then, only v1 and v2 are supported.

FAQ