ServicesLore Story Engine

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 midpoint

For 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, miss

The 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 copy

That 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

On this page