Checkpoints
Resumable task state for one agent, keyed on subject and kind, with a TTL that makes it expire — deliberately kept out of the evidence store.
State, not facts
A checkpoint answers “where did I get to?”. An observation answers “what happened?”. Those are different records with different lifetimes, and merging them is how a memory store fills up with stale scaffolding that later reads as truth.
The security invariant is explicit: 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
There is no separate create-or-update decision. 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 rather than echoing the payload back, 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 without anything having to sweep it. 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 today it answers
honestly 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 — which is the honest shape
anyway, since task state and retrieved evidence should not arrive looking alike. The flag
exists so the request survives when the capability lands; meta.degraded is the contract
that 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 a single scope, mcp:call.