Checkpoints
Resumable task state for one agent, keyed on subject and kind, with a TTL, and deliberately kept out of the evidence store.
State, not facts
A checkpoint answers “where did I get to?”. An observation answers “what happened?”. Merging them fills a memory store with stale scaffolding that later reads as truth.
Checkpoints, leases and handoffs are execution state, never factual evidence. Checkpoint text alone cannot support a claim, because consolidation only accepts observation ids as sources. If an agent finishes work and the finding matters, it appends an observation; the checkpoint is what it throws away.
Checkpoints expire. Evidence does not.
A checkpoint is overwritten in place. An observation is append-only.
A checkpoint cannot be cited as a source. Only observations can.
Save, and save again
POST /v1/checkpoints upserts on organization, subject, agent and kind, so the second
write returns 200 with updated: true and the same checkpoint_id.
const saved = await titen.saveCheckpoint({
subject_id: 'user_rama',
kind: 'task_state',
state: { step: 3, of: 7, pending: ['rollback smoke'] },
ttl_seconds: 3600,
});
// saved.checkpoint_id → "ckpt_713be19162544650a85b76b5ef6d7543"
// saved.updated → false, then true on every later write
The response carries a state_hash instead of the payload, which is enough to tell
whether a write changed anything:
{ "data": { "checkpoint_id": "ckpt_713be19162544650a85b76b5ef6d7543",
"subject_id": "user_rama", "agent_id": "agent_8df7b8699a0945488ceb3d4b104006d6",
"kind": "task_state",
"state_hash": "5ab982ea637562b9b4d1c752bf073346eb27d7ea673a2adb463c1324c2fe947b",
"expires_at": "2026-07-30T11:48:04.652Z", "updated": true } }
kind is one of task_state, conversation, workflow or cursor, a closed set, so
the lookup key stays predictable across agents. state is any JSON value except null,
under 64 000 bytes serialized. ttl_seconds runs from 60 to 2 592 000 (30 days). All three
are checked before the write; a bad kind comes back as:
{ "error": { "code": "VALIDATION_ERROR",
"message": "Field \"kind\" must be one of: task_state, conversation, workflow, cursor." } }
Reading one back
GET /v1/checkpoints is a lookup, not a list. It requires subject_id and kind, and
agent_id defaults to the calling credential’s own principal.
curl -H "authorization: Bearer $TITEN_API_KEY" \
'http://127.0.0.1:8787/v1/checkpoints?subject_id=user_rama&kind=task_state'
The response is the full record, including the parsed state, ttl_seconds,
expires_at, created_at and updated_at. Anything missing, expired, or belonging to
another agent returns the same non-disclosing 404:
{ "error": { "code": "NOT_FOUND", "message": "Resource was not found." } }
Because agent_id defaults to the caller, two agents saving task_state for the same
subject get two independent checkpoints and neither can read the other’s by accident.
Pass agent_id explicitly to resume someone else’s work, which is what a
handoff hands you.
Expiry is enforced at read time: the lookup carries an expires_at > now predicate, so an
expired checkpoint stops being visible. Nothing in the background deletes checkpoint rows.
DELETE /v1/checkpoints/:id is the explicit removal and returns
{ "checkpoint_id": "…", "deleted": true }.
Checkpoints and compiled context
POST /v1/context/compile accepts include_checkpoints: true and reports that it cannot
do it:
{ "meta": { "degraded": { "semantic": false, "vector": "disabled",
"model": "disabled", "checkpoints": "unavailable" }, "candidates": 1 } }
Checkpoints do not enter a compiled pack yet. Fetch the one you need with getCheckpoint
and put it in your prompt as a separate block, since task state and retrieved evidence
should not arrive looking alike. The flag exists so the request survives when the
capability lands; meta.degraded tells you it has not.
From MCP
Two of the seven MCP tools are checkpoints, so an agent that speaks only MCP can still
resume: titen_checkpoint_save takes subject_id, kind, state and ttl_seconds, and
titen_checkpoint_get takes subject_id and kind. Both arrive over POST /mcp, which
takes the same bearer key as REST and is gated on mcp:call.