SQLite Checkpointing
SQLiteCheckpointer is the lightweight persistence backend: a single
local file, no server, append-only message log. It's the default
choice for laptops, single-process apps, desktop tools, and any
environment where one Python process owns the conversation.
Install the extra:
pip install "cubepi[sqlite]"
This pulls in aiosqlite.
Basic usageâ
import asyncio
from cubepi import Agent, Model
from cubepi.checkpointer import SQLiteCheckpointer
from cubepi.providers.anthropic import AnthropicProvider
async def main():
provider = AnthropicProvider(api_key="âĻ")
async with SQLiteCheckpointer("agent.db") as cp:
agent = Agent(
provider=provider,
model=Model(id="claude-sonnet-4-5-20250929", provider="anthropic"),
checkpointer=cp,
thread_id="user-42",
)
await agent.prompt("Remember: my favourite colour is teal.")
# Restart the script later â the file persists.
await agent.prompt("What did I say my favourite colour was?")
# â "You said teal."
asyncio.run(main())
Two things to internalise:
thread_idis your conversation identifier â usually a user id or a session id. Two agents on the samethread_idshare the same history.async with SQLiteCheckpointer(...)is required: the context manager opens the connection and runs the one-time table creation on__aenter__. Using it without the context manager will raiseAssertionError.
What gets persistedâ
SQLiteCheckpointer writes two tables on first use:
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
thread_id TEXT NOT NULL,
message_json TEXT NOT NULL,
created_at REAL NOT NULL DEFAULT (julianday('now'))
);
CREATE TABLE thread_extra (
thread_id TEXT PRIMARY KEY,
extra_json TEXT NOT NULL DEFAULT '{}'
);
- Every
UserMessage,AssistantMessage, andToolResultMessagebecomes one row inmessages. Pydanticmodel_dump()is used for the JSON payload. - The
extradict onAgentContextis persisted onagent_endintothread_extra. Middleware that wants thread-scoped state should write intocontext.extra.
This schema is append-only. CubePi never updates or deletes rows.
HITL pending tableâ
When the HITL module is in use, an additional table is
created automatically on __aenter__:
CREATE TABLE IF NOT EXISTS thread_pending_request (
thread_id TEXT PRIMARY KEY,
request_json TEXT NOT NULL,
created_at REAL NOT NULL DEFAULT (julianday('now'))
);
No manual migration is needed â CREATE TABLE IF NOT EXISTS is
idempotent.
When CubePi readsâ
On the first prompt() after instantiation, CubePi calls
load(thread_id). If the thread exists, history is restored into
agent.state.messages and extra is restored into the agent's
private _extra dict.
Subsequent prompt() calls don't re-read â the in-memory state is
authoritative.
This means: don't share one Agent instance across processes.
Process A's in-memory state will diverge from Process B's writes.
Multi-thread isolationâ
async with SQLiteCheckpointer("agent.db") as cp:
alice = Agent(provider=âĻ, model=âĻ, checkpointer=cp, thread_id="alice")
bob = Agent(provider=âĻ, model=âĻ, checkpointer=cp, thread_id="bob")
# Each call only loads/appends its own thread.
You can pool one checkpointer across many users; the thread_id
isolates them.
Concurrency modelâ
The checkpointer uses an asyncio.Lock around every read and write.
SQLite itself can be written from multiple processes, but CubePi's
expectation is that a single agent instance owns a thread. If you
have multiple processes writing the same agent.db:
- Reads are safe.
- Concurrent writes to different threads are safe.
- Concurrent writes to the same thread will interleave and the resulting history won't make sense â you'll see two assistant messages back-to-back, etc.
If you need multi-process writers on shared threads, jump to Postgres which uses an advisory lock per thread.
Where the file livesâ
Use an absolute path in production:
SQLiteCheckpointer("/var/lib/myapp/agent.db")
Relative paths resolve against os.getcwd() at the moment of
__aenter__. The directory must exist; create it ahead of time.
Backup and inspectionâ
The file is a normal SQLite database. You can:
# Inspect history for a thread
sqlite3 agent.db "SELECT message_json FROM messages WHERE thread_id='user-42' ORDER BY id"
# Back up
cp agent.db agent.db.bak
# Vacuum to reclaim space (optional â file grows linearly with history)
sqlite3 agent.db "VACUUM"
Common pitfallsâ
- Used without
async withâAssertionError: self._db is not None. Always wrap inasync with. - Two processes writing the same thread â Interleaved history. Use Postgres or coordinate at the application layer.
- WAL mode not enabled â CubePi uses the default journal mode for
portability. For a single-writer, many-reader app, enable WAL once
via
sqlite3 agent.db "PRAGMA journal_mode=WAL"for better read concurrency. - Forgetting to set
thread_idâ Without it, the agent has no persistence binding. The checkpointer is silently ignored. Always pass both.
See alsoâ
- Postgres Checkpointing â for multi-instance deployments.
- Custom Backends â implement the protocol for Redis, DynamoDB, etc.
- Recipes â Persistent Chat â end-to-end app with SQLite.
- Recipes â Resumable Long Tasks â agents that survive a process restart mid-tool.