agentafk
Guides

Persist structured state across sessions

Save JSON checkpoints with exact keys, version checks, and optional expiry.

The state store keeps machine-readable JSON documents across process restarts and surface changes. Use it for progress checkpoints, counters, lookup tables, and coordination records; use memory for natural-language knowledge retrieved by relevance.

Save and resume a checkpoint

Choose a namespace and key

Ask the agent to use a project-specific namespace, such as dashboard-migration, and a stable key such as checkpoint. Namespaces are shared across sessions, not automatically scoped to the current directory.

Save confirmed progress

The agent calls state_put with an input like this after the underlying work succeeds:

{
  "namespace": "dashboard-migration",
  "key": "checkpoint",
  "value": { "phase": "transform", "rows_done": 50000, "last_id": "rec_0xAB4F" }
}

A first write returns a version and created: true. Subsequent writes overwrite the document and advance its version.

Read before resuming

{ "namespace": "dashboard-migration", "key": "checkpoint" }

Pass this to state_get. It returns the value, version, key, and update timestamp, or null if absent or expired. Confirm the external work still matches the checkpoint before skipping steps.

Available operations

ToolPurposeResult
state_getRead one namespace/keyDocument or null
state_putWrite without a version conditionversion, created
state_casWrite only when expected_version matchesmatched, and newVersion on success
state_deleteRemove a documentdeleted
state_queryList a namespace, optionally by prefixArray of documents sorted by key

Namespaces and keys must match [A-Za-z0-9_.-]+ and be at most 128 characters. Values can be JSON objects, arrays, strings, numbers, booleans, or null. A null value is different from an absent document: the former is returned inside a document row.

state_put and state_cas accept optional ttl_ms and JSON metadata. Metadata is stored for auditing but is not returned in normal document reads.

Read results

{
  "key": "checkpoint",
  "value": { "phase": "transform", "rows_done": 50000 },
  "version": 3,
  "updated_at": 1792287000000
}

The update time is milliseconds since the Unix epoch. Treat the returned version as an opaque concurrency token. Do not assume a delete, expiry, or recreation continues the old document's version history.

Avoid lost updates with compare and swap

Two sessions can read the same counter and then overwrite each other's increment if both use state_put. Use state_cas for updates to a shared existing record.

Read the current version

Call state_get for metrics / processed-files. Suppose it returns value 142, version 7.

Write conditionally

{
  "namespace": "metrics",
  "key": "processed-files",
  "expected_version": 7,
  "value": 143
}

A successful state_cas returns {"matched":true,"newVersion":8}.

Recompute on a conflict

If it returns {"matched":false}, read again and recompute from the new value. The record may have changed, expired, or been deleted. CAS does not create a missing document.

A read followed by an unconditional write is not a lock. In particular, state_get returning null does not prove that another session is not simultaneously starting the same operation. The model-facing tools do not provide an atomic create-if-absent lock operation. Coordinate initial creation and use version checks for existing records.

The store uses WAL-mode SQLite with a 5-second busy timeout. CAS runs in an immediate transaction, so callers racing on the same version cannot both win that update.

Expire temporary state

{
  "namespace": "rate-limits",
  "key": "github-search",
  "value": { "remaining": 9 },
  "ttl_ms": 3600000
}

Expired documents disappear from state_get and state_query results; expired rows are garbage-collected when the store opens. An update with a new ttl_ms resets expiry from the write time. Omitting ttl_ms on an update removes the previous expiry rather than preserving it.

TTL is useful for cached rate-limit observations or temporary hints. It is not a timer that runs work when a document expires.

state_query accepts namespace, optional key_prefix, and limit (default 20, maximum 100):

{
  "namespace": "dashboard-migration",
  "key_prefix": "phase-",
  "limit": 50
}

Results are sorted by key. The tool has no cursor or offset parameter, so this is bounded listing, not a complete pagination API. Use deliberately partitioned prefixes when you expect many documents. Timestamp-shaped keys can form an audit trail, provided they use only allowed key characters, for example 2026-10-17T21.00.00Z rather than a timestamp containing colons.

Storage and subagents

The database lives at ~/.afk/state/kv/kv.db, or kv/kv.db under AFK_STATE_DIR. It is created on first use with owner-only directory permissions and best-effort owner-only database permissions on platforms that support them. REPL, chat, Telegram, daemon, and scheduled sessions share documents when they use that same state tree.

Top-level sessions can receive all five tools. Ordinary subagents receive read-only state_get and state_query; the coordinator should persist their final checkpoints. Tool permissions can further restrict access.

For coordination within one task, use the shared workspace instead:

ToolUse
workspace_publishShare a finding, evidence, hypothesis, decision, artifact, or status with siblings
workspace_queryLook up findings without rereading the same files
workspace_subscribeReceive matching workspace updates on child surfaces where subscription support is wired

workspace_subscribe is not part of the ordinary top-level tool surface. Workspace sharing is session coordination, not a substitute for a durable state checkpoint. See subagents for usage.

Choose state or memory

NeedBetter fit
Exact lookup of a JSON checkpointState store
Concurrent update to an existing counterState store with CAS
A fact recalled by topicMemory archive
Durable user preferencesMemory
Findings needed by sibling agents nowWorkspace
A persistent repository objectiveGoal tracking

On this page