Skip to Content
RuntimeCheckpoints

Checkpoints

A checkpoint is a saved copy of a run’s transcript. With checkpoints on, an interrupted run continues from where it stopped without re-running or re-billing the turns it already did, and you can fork a session from an earlier point. Use them for long runs where starting over is expensive.

With Tuning(checkpoint=True), the agent loop persists a transcript snapshot (messages, turn count, cumulative usage) after every architecture pass. A crashed run resumes from the latest snapshot with agent.resume(): prior turns are restored verbatim, never re-executed, never re-billed.

from loomflow import Agent, Tuning from loomflow.runtime import SqliteRuntime agent = Agent( "...", model="claude-opus-4-7", runtime=SqliteRuntime("./journal.db"), tuning=Tuning(checkpoint=True), ) # First run, killed by OOM / deploy / Ctrl-C partway through: await agent.run("long research task", session_id="research-2026-07-04") # After restart: restore the latest checkpoint and keep going. # The restored turns are accounted in the result's usage but the # model is never re-invoked for them. result = await agent.resume( "pick it up where you left off", session_id="research-2026-07-04", )

Checkpointing is off by default (checkpoint=False). A failed checkpoint write is logged as a warning and never kills the run: it’s a durability optimisation, not a correctness dependency.


What a checkpoint contains

class Checkpoint(BaseModel): session_id: str checkpoint_id: str # auto: new_id("ckpt"), time-sortable turn: int messages: list[Message] # the full transcript cumulative_usage: Usage # tokens + cost up to this point cursor: str | None = None # architecture-local re-entry marker created_at: datetime

Checkpoints are serialised as JSON via model_dump_json, never pickle, so payloads are inspectable with jq and safe to load even from a store an attacker could write to. Journal step entries are tagged JSON too, unless you opt into serializer="pickle"; see Journal format.

One checkpoint is written per architecture pass: the first pass, and each stop-hook (Ralph loop) iteration. Snapshots are taken after auto-compaction runs, so a compacted transcript is what gets stored.


agent.resume(...)

async def resume( self, prompt: str | None = None, *, session_id: str, from_checkpoint: str = "latest", user_id: str | None = None, **kwargs, # any other run() keyword: metadata, context, extra_tools, emit, output_schema, effort, ... ) -> RunResult: ...

When the runtime supports checkpoints and one exists for session_id:

  1. session.messages, turns and cumulative_usage are restored verbatim from the snapshot.
  2. Memory-rehydration seeding is skipped: the restored transcript is the state, so no duplicate system/memory blocks are injected.
  3. prompt is appended as a fresh USER turn. prompt=None injects an internal continuation nudge ("Continue the interrupted task.") so the model picks up without new user input.
  4. The loop continues. The returned RunResult rolls up restored and new usage. Prior turns are accounted, not re-billed: no model call is repeated for them.

Fallback. When the runtime has no checkpoint support, or no checkpoint exists for the session (for example, the run didn’t have Tuning(checkpoint=True)), resume() falls back to run(prompt, session_id=session_id) against the session’s last run. Memory rehydration rebuilds the conversation and, on a durable runtime, that run’s journaled steps replay instead of re-executing, even if it finished. See Replay and resume.

Checkpoints are stored per (session, user). Pass the run’s user_id to resume() and to agent.list_checkpoints(session_id, user_id=...); a different user (or none) won’t see them.


Fork from an older checkpoint

from_checkpoint selects the snapshot. "latest" (the default) continues the session in place. An explicit checkpoint id that is not the latest forks: a fresh session_id is derived (new_id("sess")), the checkpoint’s messages are copied in, and the run proceeds under the fork id. The original session’s history and checkpoints are untouched.

metas = await agent.list_checkpoints("research-2026-07-04") older = metas[-1] # newest-first ⇒ last = earliest result = await agent.resume( "try a different approach from here", session_id="research-2026-07-04", from_checkpoint=older.checkpoint_id, ) result.session_id # "sess...": a NEW id, the fork

Passing the id of the latest checkpoint continues in place, the same as "latest", with no fork.


agent.list_checkpoints(...)

metas = await agent.list_checkpoints(session_id, limit=50, user_id="alice") for m in metas: # newest first print(m.checkpoint_id, m.turn, m.created_at)

Returns list[CheckpointMeta] (id, turn and timestamp only, no message payload), so listing is cheap. Returns [] when the runtime has no checkpoint support. Pass the user_id the run used (omit it for runs without one).


Retention

Every checkpoint-capable runtime takes max_checkpoints_per_session (default 20): on each write, the session’s oldest checkpoints beyond the limit are dropped. Values < 1 disable pruning (unbounded).

SqliteRuntime("./journal.db", max_checkpoints_per_session=5) runtime = await PostgresRuntime.connect(dsn, max_checkpoints_per_session=50) InProcRuntime(max_checkpoints_per_session=20)

On the journaled runtimes, runtime.store.prune(before=..., session_id=...) deletes journal entries and checkpoints matching the filters; use it to purge completed sessions. Nothing else prunes automatically.

InProcRuntime checkpoints are process-local. The default runtime implements the full checkpoint API, so Tuning(checkpoint=True) + resume() works within one process (useful for tests and long-running services that survive logical failures). But the snapshots live in memory and are lost on exit. For crash durability, use SqliteRuntime or PostgresRuntime.


Journal vs checkpoint

Both live in the same store, and they solve different problems:

JournalCheckpoint
What it storesEach step’s result (model call, tool dispatch), keyed by run + name + input fingerprintThe whole transcript (messages + usage + cursor) at a pass boundary
What it’s forMemoisation: a resumed run skips already-completed stepsRestoration: a resumed run starts from the snapshot, no replay walk
GranularityPer step, within a passPer architecture pass
Wire formattagged JSON by default; pickle only with serializer="pickle" (trust the store)JSON (model_dump_json, inspectable)
Enabled byUsing a JournaledRuntime (Sqlite / Postgres)Tuning(checkpoint=True) on any checkpoint-capable runtime

They compose: on a durable runtime with checkpointing on, resume() restores the latest snapshot directly. If the run crashed, the journal still replays any step after the restore point that re-fires with identical inputs.

Checkpoints must have been written to be restored. resume() on a session whose runs never had Tuning(checkpoint=True) silently takes the run() fallback. That’s the designed behaviour, not an error. Check agent.list_checkpoints(session_id) if you need to know which path you’ll get.

For crash-recovery semantics of the journal itself (step keys, the determinism contract, at-least-once delivery), see Replay and resume. For pausing a live run and resuming it on human input, see Signals.

Last updated on