Checkpoints
Persist graph runs across HTTP requests with a checkpointer. Use memorySaver() for demos, or implement Checkpointer against your store.
memorySaver
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.
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
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
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):
{
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):
// 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.