# Checkpoints

> Resumable task state for one agent, keyed on subject and kind, with a TTL, and deliberately kept out of the evidence store.

Section: Collaboration · Source: https://titen.dev/docs/checkpoints
Derived from: https://github.com/RamaAditya49/titen/blob/main/docs/reference/api.md

---
## 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.

<div class="rules">

<p>Checkpoints expire. Evidence does not.</p>
<p>A checkpoint is overwritten in place. An observation is append-only.</p>
<p>A checkpoint cannot be cited as a source. Only observations can.</p>

</div>

## 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`.

<figure class="code"><figcaption>resume.ts<b>typescript</b></figcaption>

```ts
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
```

</figure>

The response carries a `state_hash` instead of the payload, which is enough to tell
whether a write changed anything:

```json
{ "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:

```json
{ "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.

```sh
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`:

```json
{ "error": { "code": "NOT_FOUND", "message": "Resource was not found." } }
```

<div class="callout">
<strong>PER-AGENT BY DEFAULT</strong>

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](/docs/leases-handoffs) hands you.
</div>

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:

```json
{ "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`.

## Next

<div class="cards">

<a href="/docs/leases-handoffs"><strong>Leases &amp; handoffs</strong><span>Transfer a checkpoint to another principal without losing the thread.</span></a>
<a href="/docs/observe"><strong>Observe evidence</strong><span>What to append when a finding should outlive the task.</span></a>
<a href="/docs/sdk"><strong>Agent SDK</strong><span>The three checkpoint methods and the rest of the client.</span></a>

</div>