pi-daemon1.3

Log 03 · Architecture

One daemon. Many sessions. No agent code in it.

A small server process authenticates, keeps the catalog and routes. Supervised worker processes, one per profile, run the agents. Every session is its own SQLite store, open only while something needs it.

01Processes

A server that cannot be taken down by agent work

The processes Left, the server process: one listener whose /healthz and /readyz need no bearer and whose other paths do; the AHP projection with its subscriptions and in-memory replay window; the ACP and Chord routes and the /web login; the session catalog in catalog.sqlite; and the worker pool, which spawns, routes to and restarts workers. Right, one worker process per profile (a second is shown behind): a Harness for each open session store, a registry holding the profile's built-in tools and modules, the profile agentDir's models and logins, an execution environment that runs tools in each session's cwd and environment, MCP clients per session, and terminals. They talk over Node IPC. Below, the watchdog is its own small process that probes the listener. listener/healthz · /readyz open; rest: bearer /ahpprojectionreplay window /acpeditorconnections /chordcalls · watches /weblogin cookie catalog.sqlite worker poolspawn · route · restart Harness poolone per open store registrybuilt-ins + modules modelsauth.json · models.json execution envcwd + the session's env MCP clientsper open session terminalslive processes pi-daemon-watchdogits own process · expects the listener’s 401 SERVER PROCESS WORKER · ONE PER PROFILE IPC the profile’s session stores: open, live and closed
Fig. 3.1The server runs no model calls, tools, MCP servers or extensions. Everything that can misbehave lives behind a worker’s hatched edge.
  • Not one processAn extension, an MCP server or a runaway tool can crash its worker. It cannot crash the listener, the catalog or another profile’s sessions.
  • Not a process per sessionA Node process costs roughly 60 MB before it does anything. A worker holds the Harnesses of many sessions and opens their stores only when they are used.
  • A trust domain eachA worker is one profile’s trust domain. Isolation between mutually untrusted work means another uid, a container or a remote execution environment.

02Routing

From a socket to a session

How a request reaches a session Culture upgrades to /ahp with the bearer, which the listener checks before any route runs. It subscribes to a session. The AHP projection looks the session up in the catalog to find its profile, cwd and variables, and calls the profile's worker over IPC. The worker acquires the session's store, opening it if it is closed, and the Harness answers. The state goes back to Culture as a snapshot. upgrade /ahp + bearer1 wrong bearer: 401, no route runs subscribe ahp-session:/s12 s1 → profile · cwd · env3 call over IPC4 acquire: open if closed5 Harness6 session state7 snapshot8 Culturea clientCulturea client listenerserverlistenerserver /ahpprojection/ahpprojection catalogcatalog.sqlitecatalogcatalog.sqlite workerthe profile'sworkerthe profile's session.sqlitethe session'ssession.sqlitethe session's
Fig. 3.2The catalog says which profile, and so which worker, owns a session; the worker owns its store.

The catalog, state/catalog.sqlite, belongs to the server. It holds each session’s id, name, profile, title, cwd, nonsecret variables, creation and activity times, read and archive marks, default chat and drafts, and whether it holds live work. It never holds conversation content: that is only ever in the session’s own store, which only its profile’s worker opens.

03Stores

Open while needed, closed otherwise

A session store's life in its worker A store is closed on disk until its first use opens it. While a call holds it, it is open; when the last call releases it and it has no live work, it is idle, and after five idle minutes it closes. Live work — a running task or an unsettled submission — keeps it open until the work settles. A storage error closes only that store; the next use reopens it. A store a crash closed while it was marked live is reopened at startup and its work resumed. closedon disk only marked liveclosed by a crash openheld by a call workinglive work keeps it open idleno call, no live work first use opens it storage error: only this one last call released used again 5 minutes idle work starts work settles startup resumes it
Fig. 3.3Each store is a file at state/sessions/<id>/session.sqlite. Back one up, or move it to another host, by copying it.

A worker reports whether each open store holds live work — a pending, running, waiting or completing task, or a queued or placed submission — and the catalog records it. The report of new work is made before the commit that starts it, and the report that it ended after the commit that finishes it, so a crash can never leave started work marked idle. At startup that mark is the whole recovery plan: exactly the marked sessions are reopened and resumed, and nothing else is touched.

04Startup

Ready means resumed

Startup and shutdown Startup, left to right: create the owner-private state and agent directories, seed auth.json once and create or check the bearer; open the catalog and resolve the profiles; spawn every profile's worker and wait until all are ready (a profile that cannot start fails serve); each worker resumes its sessions marked live, four at a time; then start the listener, and /readyz answers 200. Shutdown on SIGTERM or SIGINT, with repeated signals absorbed: the protocol routes close; the listener stops (/readyz answers 503) and gives requests in flight five seconds; workers shut down with a five-second grace; the catalog closes. Live work is resumed on the next start. SERVESIGTERMpathsstate · agentDir · bearercatalog+ profilesworkersspawn, all readyresume4 at a time per workerlisten/readyz 200signalrepeats absorbedroutesAHP · ACP · Chord closelistener/readyz 503 · 5 s drainworkers5 s gracecatalogcloseda worker that never becomes ready fails serve; it is a broken profile, not a crash
Fig. 3.4The listener opens only after every worker is up and its live sessions are moving again.

05Crashes

A crashed worker restarts; nobody else notices

When a worker crashes Left, the restart delay after each consecutive crash: 100 ms, then doubling to 200, 400, 800, 1,600 and 3,200 ms, capped at 5 seconds. A worker that ran for 10 seconds before crashing counts as stable, so its next crash starts again at 100 ms. A worker that never became ready is a broken profile, not a crash: it is not restarted. Right, what a crash does: calls in flight fail with worker_exited, except prompts and submissions, which are resent with the same request id up to three times; watches end, so AHP chat subscribers are disconnected and reconnect for fresh snapshots; terminals end; the other profiles' workers are untouched; on restart the worker resumes its sessions marked live. RESTART DELAY AFTER CONSECUTIVE CRASHEScap 5 s100 ms#1200 ms#2400 ms#3800 ms#41.6 s#53.2 s#65 s#75 s#8ran 10 s before crashing: stable, back to 100 msnever became ready: a broken profile, not restartedWHAT A WORKER CRASH DOEScalls in flightfail with worker_exitedprompts and submitsresent with the same request id, up to 3 timesAHP chat streamssubscribers reconnect for fresh snapshotsterminalsend; they are processes, not Durable statelive sessionsresumed by the restarted workerother profilesuntouched: separate processes
Fig. 3.5Backoff keeps a crash loop from becoming a restart storm; a stable worker earns a fast restart again.

Process supervision above the daemon is the job of systemd, launchd or supervisord, and of the semantic watchdog, which restarts the daemon only when its listener stops answering.

06Multiplexing

Cost follows what is open, not what exists

Many sessions, few processes Left, one worker holding 72 sessions: most stores are closed on disk, nine are open and three hold live work. Above them, the profile's turn gate: with concurrency 16 at most sixteen turns run at once, three are running. Memory follows the open stores, not the total. Right, the comparison: a process per session would be 72 Node processes at roughly 60 MB each; pi-daemon runs one server and one worker per profile. ONE WORKER · 72 SESSIONSturn gate · concurrency 163 turns runningopen stores: memoryclosed stores: a fileTHE ALTERNATIVEa process per session72 × ≈60 MB Node processespi-daemon1 server + 1 worker per profile
Fig. 3.6A profile’s concurrency caps its turns at once (default 16), so one application’s fleet cannot starve your personal sessions.

A store stays open for five minutes after its last use and its last live work, then closes. A session that nobody has touched for a week costs one closed file and one catalog row.

07Bounds

Every queue has a size

WhereBoundValue
listenerconnections1,024
listenerheader and request timeouts30 s · 60 s
AHPconnections · frame size256 · 4 MiB
AHPunsent backlog before a client is dropped to reconnect16 MiB
AHPrequests in flight per connection64
AHProot actions kept for reconnect4,096
AHPchats streaming live · turns starting per chat256 · 64
AHPturns in a snapshot or page, unless asked for fewer50
Chordcalls in flight · subscriptions per connection64 · 64
catalogpage size, default and most100 · 1,000
workercalls in flight · watches · terminals4,096 · 1,024 · 64
workerprompt resends after it dies3
workerrestart delay, first and most · stable after100 ms · 5 s · 10 s
storeidle time before it closes5 min
startupsessions resumed at once per worker4
profileturns at once (concurrency)16
MCPwait for servers on open · tools offered before tool_search10 s · 32
ask_userquestion · choices · answer4,000 · 16 · 16,000 chars
resourceswatches · terminal scrollback in a snapshot64 · 256 Ki chars
changesetsfiles per chat · baseline size256 · 1 MiB
drafts · titlessize64 KiB · 4,096 chars