●ctx0.1.0

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

Inside the ctx daemonClients (the CLI, the MCP server, the Pi plugin and pi-daemon module, or any HTTP program) reach one HTTP server over TCP or a Unix socket with a bearer token. Reads run in place on a pool of four WAL reader connections, so they never wait for writes. Writes queue to one writer thread, which commits up to 512 concurrent requests in one transaction, each in its own savepoint, updating the full-text index in the same transaction. Each commit is broadcast to watch tasks, which replay history from the database before going live. Everything lives in one SQLite database in WAL mode with tables for entries, changes, full-text search and vectors. Two background threads: a checkpointer copies the WAL every 250 ms without blocking anyone, and an embedder turns new entries into vectors when embeddings are configured. ctx CLI · MCP server · Pi plugin · pi-daemon module · any HTTP program HTTP/1.1 POST /v1/op GET /v1/watch GET /v1/health TCP or Unix socket · bearer reads 4 WAL reader connections run in place, never wait writes one writer thread ≤ 512 requests per commit a savepoint each, FTS inline watches subscribe, replay, live ≤ 4096 at once SQLite WAL mode · durability normal or full entries changes fts5 vectors catch-up checkpointer PASSIVE every 250 ms; truncates when quiet embedder vectors for new entries, when configured
The daemon.Every client speaks the same typed operations over HTTP; reads, writes and watches each take their own path to the database.

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:

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

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

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

durabilityMeans
normal (default)Fast, and durable across process crashes. A power cut can lose the last commits, never corrupt the store.
fullfsync on every commit: durable across power loss, at some cost in write latency.

What is stored

TableHolds
entriesEach key's current value (validated JSON), revision, created revision, version, writer and time.
changesEvery set and delete ever made, by revision, with its previous revision: the history and the source of every watch replay.
ftsAn FTS5 index over keys and value text, updated in the write's transaction.
vectorsWith 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.

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.

Measured latency per operationHorizontal bars of median latency, with a line out to the 99th percentile and a tick at the 95th, on a scale from 0 to 5 milliseconds. Get over TCP: median 0.62, p99 0.87. Get over a new Unix-socket connection: 0.51 and 0.80. Set: 0.84 and 2.20. Set with 16 concurrent writers: 1.60 and 4.59. Watch delivery: 0.96 and 3.39. Search over about 20,000 entries: 0.80 and 1.95. Reading a 500-entry subtree: 3.02 and 3.92. Budget lines mark a 1 ms median and a 5 ms p99 for single operations. 0 ms 1 ms 2 ms 3 ms 4 ms 5 ms p50 budget p99 budget get · TCP keep-alive 0.62 / 0.87 get · Unix socket, new connection 0.51 / 0.80 set · TCP keep-alive 0.84 / 2.20 set · 16 concurrent writers 1.60 / 4.59 watch delivery 0.96 / 3.39 search · FTS5, ~20k entries 0.80 / 1.95 get subtree of 500 3.02 / 3.92 bar: p50 · tick: p95 · dot: p99 (labels p50 / p99, ms)
Latency.Every single-operation budget (p50 under 1 ms, p99 under 5 ms) is met; the 500-entry subtree read is one request returning 500 entries.
Operationnp50 msp95 msp99 ms
set (TCP, keep-alive)30000.841.112.20
get (TCP, keep-alive)30000.620.790.87
get (Unix socket, new connection)10000.510.710.80
get subtree of 500 (TCP)5003.023.633.92
set, 16 concurrent writers160001.602.794.59
watch delivery (write sent → event received)5000.962.183.39
search (FTS5, ~20k entries)5000.801.271.95

Sixteen concurrent writers sustain 8,900 writes per second, against a budget of 5,000. What made the difference:

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.