Checkpoints and Resuming
A checkpoint is a saved snapshot of interpreter state - variables, call stack, which choices have already been taken. Two different things write checkpoints, they use different names for the same save, and resuming silently ignores a name it cannot find. Read this page before you build a save system.
Where checkpoints live
Checkpoints are keyed by universe + story + user - not by session. A
checkpoint written by one session is visible to the next session the same user
opens on the same story, which is exactly what makes resuming possible. It
also means a dev session can never resume from an earlier one: every
POST /lore/dev mints a fresh story id, so each dev run starts with an empty
checkpoint namespace.
They are stored in Redis without a TTL, so they outlive the session, the process, and a server restart.
The two writers
In-script. A @checkpoint "midpoint" directive snapshots real interpreter
state at the point it executes. This is the one you want for save points.
Over REST. POST /sessions/{id}/checkpoints stores whatever JSON you hand
it under a name you choose. It does not snapshot the interpreter - it is a
side-channel for your own data.
Both land in the same namespace, and checkpoints lists them together:
snug --output json lore checkpoints AHagnTEmSHed{
"checkpoints": [
{ "name": "midpoint.chk" },
{ "name": "restsave" }
],
"total": 2
}midpoint.chk came from the @checkpoint "midpoint" directive. restsave
came from snug lore checkpoint <id> -n restsave -d '{"marker":"rest"}'. Note
the suffix: the interpreter appends .chk to every name it saves, and the
REST endpoint does not.
The naming trap
That suffix splits the two surfaces apart, in opposite directions.
Resuming takes the bare name. The interpreter appends .chk itself, so
you pass what you wrote in the script, not what checkpoints printed:
snug lore create -u BSvXeGcHSGZV -s cLefayKVjdMp --auto-start -c midpointFor a story that speaks "Chapter one.", checkpoints, then speaks
"Chapter two.": resuming with -c midpoint skips straight to "Chapter two."
Reading takes the listed name. load-checkpoint reads the raw key, so it
wants the .chk form that checkpoints showed you:
snug --output json lore load-checkpoint AHagnTEmSHed -n midpoint.chk{
"name": "midpoint.chk",
"data": {
"version": 1,
"state": {
"variables": {},
"callStack": [],
"chooseState": {},
"actors": {},
"config": { "commandWaitDefault": true }
}
},
"created_at": "2026-08-29T12:47:33.998540Z"
}The state object is the interpreter's own snapshot format, trimmed here - it
also carries nextNodeId, stack, skipStack, and the sprite, audio, and
loop bookkeeping needed to resume mid-story.
Asking for -n midpoint instead returns 404 checkpoint_not_found.
So the same save point is midpoint to create -c and midpoint.chk to
load-checkpoint. Pick whichever the operation wants.
Resuming never fails loudly
A checkpoint name that does not resolve is not an error. The interpreter resets to a fresh start and the session runs from the top as if you had passed nothing. Both of these silently replay from "Chapter one.":
snug lore create -u ... -s ... --auto-start -c midpoint.chk # double-suffixed, miss
snug lore create -u ... -s ... --auto-start -c restsave # REST-written, missThe second is the important one: a checkpoint saved over REST under a plain
name can never be resumed from, because resume looks for restsave.chk and
REST wrote restsave. Use the REST endpoint for your own bookkeeping, and
@checkpoint in the story for anything a player will load.
Since there is no error to catch, verify a resume worked by checking the first events a session emits, not by checking a status code.
Copying a save
The suffix rule cuts both ways, which gives you a supported way to clone or
transplant a save: read a real checkpoint, write it back under a different
name ending in .chk, and it resumes like any other. Verified end to end:
snug --output json lore load-checkpoint $SID -n midpoint.chk \
| jq -c '.data' > save.json
snug lore checkpoint $SID -n 'copy.chk' -d "$(cat save.json)"
snug lore create -u BSvXeGcHSGZV -s cLefayKVjdMp --auto-start -c copyThat session resumed at "Chapter two.", the same as -c midpoint. This works
only because the payload is a genuine interpreter snapshot - an arbitrary JSON
object under a .chk name will not restore anything useful.
What does not work
The client events create_last_checkpoint and delete_last_checkpoint are
accepted by the transport and routed onward, but nothing consumes them - only
start and stop are acted on. Sending them is a no-op; no checkpoint
appears. Checkpoints are created by the @checkpoint directive or the REST
endpoint, and by nothing else.
Also note that created_at in a load-checkpoint response is the time you
made the request, not the time the checkpoint was written - it changes on
every read. Do not use it to order or age saves.
Reference
- Lore API - the checkpoint endpoints
- Running stories - sessions, the step loop, and limits