the workspace daemon & its CLI · alpha
Padi: the memory
under kolu.
padi is the per-host daemon that remembers the memory under kolu — what a Kolu
workspace is: terminals, sessions, restore state, repo context, the code tree,
git status, diffs, and agent awareness. It sits above kaval, which
owns the live PTYs. The split is simple: kaval keeps shells alive; padi keeps the
workspace legible. The whole layer stack is in Architecture;
padi is a @kolu/surface daemon, with the
drain-capable live recycle that keeps a client
connected across an upgrade.
Where it sits
The browser talks to kolu-server, the server binds padi, and padi supervises
its own kaval. Restart the web shell and padi still has the workspace. Restart
padi and kaval still has the PTYs. The layers recover on different clocks because
they own different state.
Steady-state supervision
“padi supervises its own kaval” is continuous, not a boot-time step. Every ten seconds padi asks its kaval three read-only questions over the socket, and a kaval that cannot answer them in five seconds is not busy — it is stuck. Three such answers in a row (about half a minute) and padi runs, by itself, exactly what the Restart kaval button runs: save the session, stop the stuck daemon, start a fresh one, and offer your terminals back for restore. One slow moment changes nothing; only a real stretch of silence does.
If three restarts in a row do not produce a daemon that answers, padi stops restarting and leaves the card and the button — a hot restart loop is not a repair. The kaval-down card is for the genuinely unrepairable now, not for every case.
The line between padi and its kaval can drop while the daemon behind it is perfectly healthy — still running, still holding every terminal, still able to answer. From the outside that looks exactly like a stuck kaval, and it is the opposite: nothing is broken, and there is only a connection to re-make. padi re-makes it by itself, about a second after it drops, and keeps trying — backing off to once every half-minute — for as long as it takes. It only ever re-makes a line to a daemon it has just checked is still answering, so the ordinary outcome is that it takes that daemon back over: your terminals and the agents inside them keep running throughout, there is nothing to restore, and kolu says so once before the card clears itself.
Two narrower cases end somewhere else, and kolu tells you which one you got rather than showing you the same line for all three. If the daemon exits in the moment between that check and the re-connect, the re-connect starts a fresh one; and if a re-connected daemon turns out to hold terminals kolu cannot account for, kolu replaces it deliberately rather than leave terminals it cannot see. In both, the message is the restart one — “kaval was unresponsive — kolu restarted it; your session is ready to restore” — and your session comes back through the restore card.
A daemon that is genuinely gone is a different fact and gets the different answer. Re-making a line to a daemon that is not there would mean starting a new one, which is a restart — so padi leaves that to the ten-second check above, and to its three-strike limit, which is what keeps a kaval that dies as fast as it starts from being restarted forever.
kaval watches itself from the other side of the same wire: it asks its own address a trivial question on the same cadence, and three unanswered self-checks in a row make it exit non-zero rather than sit there accepting connections it cannot serve. That covers the case padi cannot — a kaval nobody is currently watching — and turns “stuck forever”, which nothing handles, into “stopped”, which the supervisor already handles.
While a host’s kaval is down, that host’s dot is amber, not green.
What padi knows
kaval only knows terminals. padi is where terminal observations become a workspace: repo-aware, agent-aware, resumable, and readable from more than one face.
terminal registry
which terminals exist, where they belong, and how to re-open them after the web layer moves.
workspace memory
the session record, activity feed, restore target, and durable state-root for one host.
repo context
repo, branch, pull request, checks, cwd, foreground command, file browsing, git status, and diffs.
agent state
whether a terminal’s agent is working, waiting, or asking for you, folded into one live surface.
The thin shell face — padi-tui
padi-tui reads the same workspace surface kolu uses. Where kaval-tui shows
what’s running in each PTY, padi-tui shows what each terminal is in: repo, PR,
checks, foreground command, live byte activity, and agent state. That is the
state behind the multi-agent dock.
padi-tui status
padi-tui watch
padi-tui wait a3f10000 --until awaiting,waiting
padi-tui create --toplevel -- claude
ID REPO·BRANCH PR AGENT FOREGROUND IDLE
a3f10000 kolu·feat/dial-ssh #1412 ✓ claude · working node 2s
b7c20000 kolu·master — codex · waiting codex 1m
c9d40000 kolu·fix/fold #1408 ✗ — nvim 5m
Usually no flag. padi keys its socket by a digest of its state-root, so
inside a kolu terminal $PADI_SOCKET makes padi-tui flag-less — an agent
driving its siblings never guesses a path. padi also puts the tools themselves on
that terminal’s $PATH — padi-tui, kaval-tui, and the kolu whose mcp
face agents connect to — taken from padi’s own build. On a remote host they
arrive in the closure kolu provisions, so an agent working there has the whole
loop available with nothing installed on the box, and can never end up holding a
tool from a different build than the daemon it is talking to. Elsewhere it autodiscovers the running
padi — and when several are running (your everyday padi plus a dev or test one),
it picks the one keyed to the state root your environment names rather than
asking you to choose; --socket <path> or --state-root <dir> point it at a
different local daemon, and --host <ssh> reaches a padi on another machine — the twin of
kaval-tui --host: it provisions the
daemon with Nix, runs padi --stdio over ssh, and runs every verb unchanged
against the remote (padi-tui status --host nix@prod). Read verbs are safe and
create lands a real terminal on the host (which survives the link); a tui is a
dial, so it never drains or converges the remote padi. --host is mutually
exclusive with --socket / --state-root. Pointing kolu’s browser at a
remote padi is a separate path that already ships — see Add another
machine for adding a machine to the canvas.
wait <id> --until awaiting,waiting blocks until a terminal’s agent finishes its
turn — a done-signal read from real padi state, not guessed from terminal
silence. For a robust driver, wait in two phases: --until working for pickup,
then --until awaiting,waiting for turn-end, so a stale previous-turn state
cannot satisfy the wait early. --timeout <ms> fails loud (exit 2), a terminal
that exits first exits 3, and --json prints { id, agent }. The deeper design
notes live in the padi atlas note.
| Command | What it does |
|---|---|
padi-tui status · --json | Print a one-shot workspace snapshot: terminals, repos, agent state, foreground process, and recency. |
padi-tui watch · --json | Follow the workspace live, including state changes and activity indicators. |
padi-tui wait <id> --until awaiting,waiting · --timeout <ms> · --json | Block until an agent turn ends using padi’s real agent-state fold, not a guess from terminal silence. |
padi-tui create (--toplevel | --parent <id>) -- claude · --worktree <branch> · --repo <path> · --json | Create a kolu terminal from the shell, appearing live on the canvas. Placement is required and has no default: --toplevel for a tile of its own, or --parent <id> for a split under any existing terminal (including a split of a split; the canvas flattens descendants into the root tile’s tab strip, the Dock keeps the true tree). Add --worktree for a worktree’d agent. |
Why it is not inside kolu-server
The web server is a face: HTTP, assets, websocket transport. The workspace is a daemon: durable folder, terminal registry, session restore, agent awareness. Keeping those apart means a deploy can replace the web shell without making the workspace disappear.