Session checkpoints
Persist per-session state between agent runs so long-running agents resume with context intact. Checkpoint records live on the session thread and are picked up on the next run.
Why checkpoints exist
Agents that run for hours or days burn context quickly. Session checkpoints give an agent a durable hand-off point: a single record summarizing progress, learnings, and next steps. The next session reads the latest checkpoint and resumes — without re-walking every prior turn.
Anatomy of a checkpoint
A checkpoint is a LEARN record on the session thread with
body.kind = "agent.session.checkpoint". The body carries:
{
"summary": "Completed auth refactor; next: migration tests",
"learnings": [
"mutex pattern worked for concurrent refresh",
"refactor touched 14 files"
],
"next_steps": "Write migration tests for the new mutex path",
"thread": "th_auth_refactor_...",
"session_id": "sess_...",
"actor": "did:sync:agent:dev"
}The session_id is free-form — use the short alias your task tracker
assigns, or a UUID, or the thread id itself.
Creating a checkpoint — CLI
spl session save \
--thread th_auth_refactor_abc123 \
--summary "Completed auth refactor; next: migration tests" \
--learnings "mutex pattern worked" \
--learnings "refactor touched 14 files" \
--next-steps "Write migration tests" \
--actor did:sync:agent:devFlags:
--thread— session thread id. Required.--summary— one-paragraph progress summary.--learnings— repeat for each learning (at least 1).--next-steps— what the next session should pick up first.--actor— agent DID. Defaults toSPL_ACTORenv var.
Creating a checkpoint — SDK
Python
await client.emit(
act="LEARN",
kind="agent.session.checkpoint",
body={
"summary": "Completed auth refactor",
"learnings": ["mutex pattern worked"],
"next_steps": "Write migration tests",
},
thread=thread,
)TypeScript
await client.emit({
act: "LEARN",
kind: "agent.session.checkpoint",
body: {
summary: "Completed auth refactor",
learnings: ["mutex pattern worked"],
next_steps: "Write migration tests",
},
thread: threadId,
});The boundary annotation — --draft
You don't have to hand-write the checkpoint at all. With --draft, the
daemon derives what happened from the substrate itself — what closed,
what's still open, which decisions await you, what moved — and lands
the annotation on your attention thread:
spl session save --draft --summary "One human line on top of the drafted summary"--summary, --learnings, and --next-steps overlay the draft rather
than replace it; the one hand-written summary line is usually the only
part worth authoring by hand. --reason records why the boundary
happened (explicit_end, context_window_limit, timeout, …) so a
later reader knows whether the session ended cleanly.
This is the natural thing to run before an agent's context window compacts, a laptop lid closes, or a shift ends — it costs one command and makes the next re-entry lossless.
spl resume — where was I, what changed, what needs me
The other half of the boundary annotation. On a fresh session:
spl resumeOne payload answers the three re-entry questions: the summary line you
LEFT, what MOVED while you were away (rooms with new activity), what
NEEDS you (pending decisions — answer them with spl decisions), the
register totals, and the suggested next room. Descend into a specific
room — a thread id, a thread prefix, or a task alias — for its
five-bucket re-entry brief:
spl resume SKL-1234spl resume reads the annotation spl session save --draft landed, so
the pair is a closed loop: save at every boundary, resume at every
re-entry, and nothing between sessions relies on memory.
Resuming from a checkpoint
The agent's first action each session is to read the latest checkpoint:
# List checkpoints on a thread
spl session list --thread th_auth_refactor_abc123
# Show one in detail
spl session show th_auth_refactor_abc123Programmatically, query records on the thread, filter for
body.kind == "agent.session.checkpoint", sort by clock, take the
tail:
const records = await client.query_thread({ thread: threadId });
const checkpoints = records
.filter(r => r.body?.kind === "agent.session.checkpoint")
.sort((a, b) => a.clock - b.clock);
const latest = checkpoints[checkpoints.length - 1];
// agent now has: summary, learnings[], next_stepsDev Agent protocol
The dev agent (did:sync:agent:dev) prompt template expects a
checkpoint at the end of every session. See
~/.syncro/dev-prompt.md for the
contract. Humans reviewing an agent's work read the checkpoints as the
first signal of progress.
When NOT to use checkpoints
- Single-turn tasks — no resumption needed, checkpoint is waste
- Throwaway exploration — dev loop, not committed state
- Fine-grained progress tracking — use KNOW records per step, not checkpoints per step. Checkpoints are summaries, not step logs.
Retention + cleanup
Checkpoints are records like any other — content-addressed, immutable, permanent. They are NOT garbage-collected. For long-running threads with many sessions, the tail grows unbounded.
If you want to retain only the last N checkpoints per thread, emit a
core.erasure record referencing older ones. An automatic retention
policy for checkpoint records is planned.
Pairs with
- Agent integration — full protocol for the dev agent + other roles
- Python SDK / TypeScript SDK — full
emit()+query_thread()reference
Body-Kind Manifests
Declare which body fields for a given body.kind should be indexed. The instance creates SQLite expression indexes at config reload so rich-query filters on nested body fields stay fast as your record log grows.
Dispatch observability
How Syncropel turns every dispatched task into a queryable record stream — what records get emitted, how to read them, and what the completion codepath and failure reason enums mean.