Inside ctx
One process owns one SQLite database. It has one writer and several readers, and keeps every change. That shape is what makes revisions dense, history complete and watches gap-free, and it is fast enough that agents can treat the store as part of every step.
One daemon
- One authority. The CLI, the MCP server, the Pi plugin and the pi-daemon module all send the same operations to
/v1/op. None of them reimplements the semantics; the plugins drive thectxbinary itself. - Reads never wait. In WAL mode, readers see the last committed state while a write is in progress. Reads are sub-millisecond SQLite calls, so they run in place on one of four reader connections rather than being handed to another thread.
- Writes queue to one thread. A single writer means revisions are dense and ordered, and compare-and-set needs no locks beyond the transaction itself.
Group commit
Many agents write at once, and one fsync per write would cap throughput. The writer drains every request waiting in its queue, up to 512, and commits them in one transaction:
Each request gets a savepoint
A failed compare-and-set or an invalid value rolls back only its own request. The rest of the group commits.
Each change is numbered and recorded
The entry is updated, the change appended to history with the next revision, and the full-text index updated, all in the same transaction, so search is never behind.
One commit, then replies and events
Each request's reply carries its revisions, and every change is broadcast to watch streams.
With 16 concurrent writers a group averages about 15 requests. The writer turns off SQLite's automatic checkpoint, which used to run inside commits and stall them; a separate thread copies the WAL into the database every 250 ms without blocking anyone, and truncates it once writes pause.
durability | Means |
|---|---|
normal (default) | Fast, and durable across process crashes. A power cut can lose the last commits, never corrupt the store. |
full | fsync on every commit: durable across power loss, at some cost in write latency. |
What is stored
| Table | Holds |
|---|---|
entries | Each key's current value (validated JSON), revision, created revision, version, writer and time. |
changes | Every set and delete ever made, by revision, with its previous revision: the history and the source of every watch replay. |
fts | An FTS5 index over keys and value text, updated in the write's transaction. |
vectors | With embeddings configured: one vector per entry revision, filled in the background. |
Values are checked with SQLite's json_valid as well as by the daemon. Nothing expires, so the database grows with history; 16,700 entries took about 4 MiB in the benchmark.
Watch streams
A watch task subscribes to the commit broadcast before it replays history, so a change committed between the replay and going live is never lost. It skips anything it has already sent, by revision.
- Lagging. The broadcast holds 8,192 recent changes. A watcher that falls further behind is caught up from the
changestable, in order, then continues live. - Heartbeats. Every 15 seconds, with the head revision, so clients can tell a quiet store from a dead connection.
- Resuming. The CLI's
ctx watchreconnects on its own from the last revision it printed, which also carries Pi and pi-daemon subscriptions across daemon restarts.
Semantic search
With daemon.embeddings set, an embedder thread sends the text of entries that lack a vector for their current revision to any OpenAI-compatible /embeddings endpoint, in batches, and keeps unit vectors in memory. A semantic query embeds the query and ranks entries by cosine similarity. Hybrid search fuses the keyword and semantic rankings by reciprocal rank (k = 60). Indexing never blocks writes, and a failing endpoint shows up in ctx status while keyword search carries on.
Performance
Measured with cargo run --release --example bench: a real daemon over loopback and a Unix socket, against a fresh database. The host was a shared 16-vCPU Xeon running other agents, with a load average of 13 to 24, so these are not best-case numbers.
| Operation | n | p50 ms | p95 ms | p99 ms |
|---|---|---|---|---|
| set (TCP, keep-alive) | 3000 | 0.84 | 1.11 | 2.20 |
| get (TCP, keep-alive) | 3000 | 0.62 | 0.79 | 0.87 |
| get (Unix socket, new connection) | 1000 | 0.51 | 0.71 | 0.80 |
| get subtree of 500 (TCP) | 500 | 3.02 | 3.63 | 3.92 |
| set, 16 concurrent writers | 16000 | 1.60 | 2.79 | 4.59 |
| watch delivery (write sent → event received) | 500 | 0.96 | 2.18 | 3.39 |
| search (FTS5, ~20k entries) | 500 | 0.80 | 1.27 | 1.95 |
Sixteen concurrent writers sustain 8,900 writes per second, against a budget of 5,000. What made the difference:
- Checkpoints off the commit path: store throughput rose from 2,500 to over 9,000 writes per second, and the worst write fell from 540 ms to under 35 ms.
- Group commit: about 15 requests share each transaction and fsync.
- Fewer thread hand-offs: writes are awaited through a oneshot channel and reads run in place, which cut about 0.2 ms from the median.
- Search in the same transaction costs about 15% of write throughput, within budget, and keeps search consistent with writes.
The test suite proves correctness rather than speed: dense, ordered revisions under concurrent writers; a watcher forced to lag 2,000 changes behind receives every one in order; an idle WAL is checkpointed and truncated; and watches resume across a daemon restart.