Running a server
Serve a data directory over a socket or TCP, with folders to import and watch only if you want them.
The daemon serves every tenant in its data directory. Native tenants need nothing else: a write through the API commits to the database, and a tenant that does not exist yet is created by its first write. A folder of Markdown is optional, for teams that keep editing files in a repository and want the database to follow.
Start it
langonrock serve --data ~/okf/data --socket /tmp/okf.socklangonrock serving /home/me/okf/data on /tmp/okf.sock (0 tokens, 0 writable tenants)Without --socket, the daemon listens on <data>/langonrock.sock.
Talk to it
langonrock query "okf+unix:///tmp/okf.sock?tenant=acme" manifest
langonrock query "okf+unix:///tmp/okf.sock?tenant=acme" search ordersEvery client that connects shares this one process, its open tenants and their warm search indexes, so no agent invocation pays a cold start.
What the banner counts
The last line reports how many tokens loaded and how many folders the daemon is watching:
langonrock serving /home/me/okf/data on /tmp/okf.sock (0 tokens, 1 writable tenant)"Writable tenant" there counts the entries in sources.json, not the tenants that accept writes.
Under the CLI daemon every native tenant accepts a write from a token granted write access, or from
any caller on a socket with no tokens, so 0 writable tenants does not mean the store is read-only.
Zero tokens on a socket is a valid configuration. Zero tokens on TCP never starts.
Importing and watching a folder
Register a folder per tenant in sources.json, next to the data:
mkdir -p ~/okf/sources/acme/sales ~/okf/data
echo '{ "acme": "/home/me/okf/sources/acme" }' > ~/okf/data/sources.jsonWrite the path in full. Nothing expands ~ inside a JSON string, and every immediate subdirectory
of the folder is a bundle.
At startup the daemon imports each registered folder and keeps watching it, one watcher per entry:
snapshot cd9078b9af0b (new), 2 bundles [ops sales], 3 concepts, 1236 bytes on disk
watching /home/me/okf/sources/acme for tenant acme
langonrock serving /home/me/okf/data on /tmp/okf.sock (0 tokens, 1 writable tenant)Those three lines say it imported, it is watching, and it is listening. You do not need a separate
import first, and you do not need langonrock watch in another terminal. Running both would put
two watchers on one tenant.
The folder and the database can both change, and neither silently wins. An unchanged file in the folder never overwrites an edit made through the API; a file changed in the folder is imported; a document changed differently in both fails the whole import, and the watcher reports the conflict instead of picking a side. Nothing is ever written back to the folder. Import, watch and export covers the rules.
ENOENT: no such file or directory, watch '...'
A path in sources.json does not exist. The folder has to be there before the server starts, and
every immediate subdirectory of it is a bundle, so sources/acme/sales/orders.md works where
sources/acme/orders.md gives you an empty tenant.
Tuning the watcher
| Flag | Default | What it controls |
|---|---|---|
--debounce | 200 ms | How long to coalesce filesystem events before importing |
--rescan | 30 s | Full rescan interval, the backstop for missed events |
Watch events are a hint, never the whole story. FSEvents coalesces, ReadDirectoryChangesW
overflows its buffer under bursts, and inotify has a watch limit. The rescan writes nothing when the
tree is unchanged, but it is not free: at 5,000 concepts it takes 84 ms of work to conclude that
nothing happened, against 167 ms for an import that changes one concept.
--debounce only coalesces changes made to the folder. Writes through the API commit directly and
never wait on it.
Watching one extra tenant
langonrock serve --data ~/okf/data --tenant beta --watch ~/okf/sources/beta--watch keeps one more tenant importing from a folder without recording it in sources.json, so
it lasts exactly as long as that command line. sources.json is the better answer in almost every
case, since it survives a restart.
Over TCP
langonrock serve --data ~/okf/data --host 127.0.0.1 --port 7777The default host is 127.0.0.1 and the default port is 7777. A TCP bind always requires tokens,
including on loopback, and any host past loopback requires TLS. Both rules are enforced at startup
rather than per request. See Tokens and TLS.
Reading the output
Progress goes to stderr so stdout stays clean for piping, and Bun paints anything written with
console.error red. Those lines are status, not failures.
From the library
serve() is the same server without the CLI around it, and it starts read-only:
import { serve } from 'langonrock';
serve({ root: '/var/data', unix: '/tmp/okf.sock', writable: true });The CLI daemon sets writable: true itself. A library caller must set it, or pass a sourceDir
callback that authorizes each tenant, before any write route accepts a change; without either, a
write is refused with a 409 that names sources.json. TLS is passed as PEM text rather than file
paths, so the module never touches the filesystem to start.
Memory is the practical ceiling
The daemon keeps the 16 most recently used tenants open, each with its BM25 index in memory: 1.1 MB of heap at 500 concepts, 8.1 MB at 5,000 and 32 MB at 20,000. In the engine benchmarks, a fresh process that opens 20,000 concepts, builds the index and searches peaks at 0.36 GB resident.
Each folder import rebuilds the index right away, so the first search after a save to the folder does not pay for it. A commit through the API does not warm it: the first search after one builds the index, 19 ms at 500 concepts and 578 ms at 20,000.