kolu
Docs

architecture

How kolu is built

kolu splits the terminal problem across daemons, and each one survives a different failure. Close the browser tab, restart the web server, redeploy the whole thing — your shells, and the agents running in them, keep going. Here’s the stack that makes that true, and the one contract that ties it together.

The stack

Four layers, each with one job. The top two are faces — they come and go. The bottom two are daemonspadi holds the workspace record, and kaval owns the live PTYs. The arrows are transports: a websocket to the browser, a unix socket between the daemons. Kill any layer and everything below it keeps running.

The kolu stack, top to bottom: the PWA client in your browser talks over a websocket to kolu-server (the web shell), which binds padi (the per-host workspace daemon), which owns kaval (the PTY daemon) over a unix socket. The client and kolu-server are faces that come and go; padi and kaval are daemons that survive restarts. kaval holds the live PTYs — zsh, claude, nvim.FACEScome & goDAEMONSsurvivestate lives below the linewebsocketbind · unix socketunix socketPWA clientthe view in your browserkolu-serverthe web shellpadiworkspace daemon — rememberskavalPTY daemon — keeps shells aliveOWNS THE PTYS▸ zsh▸ claude▸ nvim
layer its one job killing it does not lose
PWA client the view in your browser — one surface consumer per pane Reload the tab, switch devices: it re-subscribes. It holds no state to lose.
kolu-server the web shell — HTTP, the PWA, websockets; dials and binds padi Restart it and no terminal metadata is lost — padi’s registry stays warm.
padi the per-host workspace daemon — session, awareness, restore Kill it and no terminal state is lost — records live in its folder on disk; PTYs survive in kaval. It re-seeds in seconds.
kaval the PTY survivor — owns the shells and their live screens Kill everything above it and the live shells and scrollback keep running — they live in kaval’s memory.

A deploy adopts what’s running

Upgrading kolu doesn’t kill your daemons. A new build adopts the compatible ones — and their live PTYs — rather than reaping and restarting them. Restore is the path for a daemon that’s actually dead, never for one that’s still running. So the agents in your terminals survive the upgrade: they never stopped.

A deploy timeline. In the top lane, the running padi vX drains — saves its session and exits — and the new build padi vY starts and adopts the same kaval. In the bottom lane, kaval runs unbroken straight through the deploy, holding every live PTY, so shells and agents never stop.DEPLOYpadi · vXrunningdrain → respawnsave session · exit · PTYs untouchedpadi · vYthe new buildadoptskavalone process · never restarts hereyour shells + agents stream straight through — scrollback intact

Compared to tmux and zellij

tmux and zellij fold the PTYs, the layout, and the session into one server process. kolu splits them — kaval owns the PTYs, padi owns the workspace — so the layer you restart or redeploy isn’t the layer holding your shells.

tmuxzellijkolu
Detach / close the clientsurvivessurvivessurvives — the client holds no state
Redeploy / restart the session-UI layern/a — no separate layern/a — no separate layerPTYs keep running; the deploy adopts them
Upgrade the binary under live sessionsattach breaks on a protocol-version bumpsessions orphaned on a contract-version changecompatible daemon + live PTYs adopted; a skewed one is drained, not killed
Machine rebootnothing native (a plugin re-runs commands)re-runs commands (built-in)re-spawns from padi’s saved session
Knows git · PR/CI · agent stateno — status-bar scripting onlyno — plugin scripting onlyyes — padi derives it
Persisted to disknone native; plugins add layout + cwdlayout + cwd + each pane’s commandrecords · session · restore target · agent memory
Reachterminal TUI · single hostterminal TUI · single hostbrowser PWA + kaval-tui; kaval-tui reaches remote hosts over ssh today

One contract — @kolu/surface

Announcing @kolu/surfaceread the story behind the one contract every layer talks over @kolu/surface — the docsthe framework's own four-quadrant docs: declare a surface, serve it, daemonize it, and mirror it across an ssh fleet

Every layer talks over the same thing: a typed reactive surface. You declare what a daemon exposes — a handful of primitives plus the verbs to change them — and get a client that stays in sync. Four primitives cover almost everything:

Cell<T>

what’s the current X? — one value, snapshot then live deltas — the session, the version.

Collection<K,T>

what’s the current X per key? — many keyed rows, each independently observable — the terminals registry.

Stream<I,T>

what’s the live view for input I? — computed over outside state (fs, git), snapshot + deltas, never stored — a file preview.

Event<I,T>

has X happened yet? — point-in-time, no snapshot, no current value — a terminal exited.

Alongside them sit procedures — the imperative verbs (create a terminal, kill one, restore a session) you call and await. That’s the whole contract, and it’s served over any transport: in-process with no wire at all, over a local unix socket, over a browser websocket, or over ssh. That ssh transport is already live — it’s how kaval-tui --host and drishti reach a daemon on another machine; what’s still coming is kolu itself using it to bind a padi on a remote host. Swap the transport; the client code doesn’t change. That’s exactly how padi grew up: it ran in-process inside kolu-server, then moved out into its own process behind a unix socket, and the client never noticed.

The @kolu/surface docs teach this end to end — your first surface, make it a daemon (the pattern kaval and padi are built on), and across the hosts (the ssh mirror drishti runs).

Identity is a folder, not a port

A daemon is its data folder. padi’s identity is its state-root — a durable directory on the host that holds its session, its memory, its activity feed. The socket it listens on is named by a digest of that folder and lives in a scratch dir that’s wiped on every boot. The port is config; the folder is the daemon.

// identity — durable, IS the daemon$XDG_STATE_HOME/padi// address — config, wiped on boot$XDG_RUNTIME_DIR/padi-<digest>/sock where <digest> = hash(state-root path)
  • A stale lock can never make a dead daemon look alive — its socket lives in the boot-wiped dir, gone the moment the machine restarts.
  • A dev or test kolu gets its own state-root — so its own padi, its own kaval. It cannot touch your production terminals, ever.
  • The host decides its own default state-root. A client never computes it and never sends it over the wire — so two ways into one machine can’t split its terminals across two daemons.

Talking over the wire

Everything the browser does flows over a single WebSocket (/rpc/ws) via oRPC. The contract lives in one package both sides share, so calls are type-checked at compile time and their payloads validated by Zod at runtime. Two patterns cover all of it:

Pattern Semantics Used for
Request / response a one-shot call you await create a terminal, kill one, restart a daemon, add a host
Subscription the server pushes values over the socket as a stream the terminal list, per-terminal metadata, server state

A subscription becomes a fine-grained SolidJS signal on the client, so a single row changing repaints only that row. The shell CLIs are the one exception to the websocket: kaval-tui and padi-tui reach their daemon over its local unix socket — same oRPC framing, a different transport.

Two loops of data flow

Two loops drive the system, both over that one websocket. The terminal I/O loop is the hot path — your keystrokes and the screen. The metadata loop is side-channel enrichment — the git, PR, and agent state that light up the dock.

Two data-flow loops across the four layers. The terminal I/O loop runs both ways through client, kolu-server, padi, and kaval — keystrokes down to the PTY, the screen (a snapshot then live deltas) back up. The metadata loop flows the other way only: kaval’s VT taps feed padi’s sensors, which derive git, PR, and agent state and push it to the client.TERMINAL I/O — keystrokes ⇄ screenclientxterm.jskolu-serverweb shellpadisensors · foldkavalthe PTYmetadata — VT taps → git · PR · agent state (side channel)

Terminal I/O (solid) — a keystroke goes over sendInput to the PTY that kaval owns; kolu-server proxies the browser’s socket to padi, which drives kaval. Shell output flows back as an attach stream — a screen-state snapshot followed by live deltas, partitioned so nothing is lost or double-painted. kaval’s headless mirror parses the VT sequences server-side, so a late-joining client gets ~4 KB of serialized screen state instead of replaying the raw buffer.

Metadata (dashed) — every per-terminal sensor lives in padi’s awareness producer. A CWD change (OSC 7) wakes the git sensor (branch, remote), which wakes the PR sensor (open PR + CI checks); agent detection tails each agent’s session store. padi folds those observations into each terminal’s record and serves it on one collection the client reads directly — no client-side join. Adding a new agent is one adapter here; adding a new forge is one adapter here. See Agent detection for the agent half.

The packages

kolu is a pnpm monorepo. The volatility boundaries — which hard problem each piece hides — decide where code lives, so a capability that hides transport, or GPU context, or persistence is its own package with the dependency arrow pointing out. The map, grouped:

  • Facesclient (SolidJS + xterm.js + Tailwind) and server (the Hono web shell that serves the SPA, the preview route, and the websocket, and binds padi).
  • Daemonspadi (the per-host workspace daemon that owns the terminal domain and serves padiSurface) and kaval (the standalone PTY daemon). Each ships a thin shell CLI — padi-tui, kaval-tui.
  • The frameworksurface (typed reactive Cell / Collection / Stream / Event + SolidJS hooks), surface-remote (the ssh mirror), surface-daemon (the durable-daemon spine), and its supervisor half.
  • Agent & forge integrations — one leaf each for claude-code, codex, grok, opencode, plus the agent-agnostic anyagent contract; github behind the forge-agnostic anyforge kernel; and git for pure git operations.
  • Shared UI leavestheme, solid-statepip (the status indicator), solid-pierre (file tree + diffs), solid-markdown, solid-fileview, solid-browser, serve-dir, and artifact-sdk (comments-on-files). Several are shared with a sibling app, drishti.
  • Contracts & small leavescommon (the oRPC contract), terminal-vocab, terminal-protocol, transcript-core / transcript-html, and a set of zero-dependency utilities (log, nonempty, shell-quote, html-escape, …).
The repo — juspay/koluevery package, with its full README, in one tree

Next — your other machines

The same surface contract already runs over ssh: it’s how kaval-tui --host drives a daemon on another machine with nothing pre-installed there. Pointing kolu’s browser at a padi on a remote host — a host switcher, remote agents surfacing in your dock — is the next phase. Coming, not here yet. That is separate from Remote Access, which serves today’s browser UI over private HTTPS.

Explore

Start with the site pages for the named programs; the atlas links go deeper into the working notes behind each piece.