← the Atlas

herdr vs. kolu — what to adopt

Comparisons·budding·

A shipped Rust agent-multiplexer (herdr) makes the same first-party-owns-the-PTYs bet kolu's remote-terminals plan chose — so it's a reference implementation of R2.4, not a competitor. One handoff-discipline borrow, several validations; of the two gaps it surfaced, native resume now ships (juspay/kolu#1495) and resize arbitration remains open. Claims fact-checked against both codebases.

A study of ogulcancelik/herdr (cloned @ HEAD) read through kolu’s remote-terminals plan (#951 and the pty-daemon / kaval-tui / chrome-bar docs). Sibling to the Ghostex vs. kolu remote-terminals analysis. Every load-bearing claim below was fact-checked against both codebases — herdr citations all verified; three kolu-side claims were corrected (noted inline). 13-agent workflow + adversarial critique

Both projects are AGPL-3.0-or-later, so herdr’s open-source code is license-compatible with kolu — porting is permitted under the AGPL’s terms, not blocked. The reason most recommendations are techniques and design rather than verbatim code is the stack gap (herdr is Rust; kolu is TypeScript/SolidJS), not licensing.

herdr is a ~107K-LOC Rust TUI agent multiplexer: one binary, a long-lived background server that owns every PTY (one ghostty VT emulator + one OS-thread PTY actor per pane), thin clients that attach/detach over a unix socket. Workspaces → tabs → panes. An agent-awareness sidebar rolls each workspace up to its most urgent state (blocked / working / done / idle). A second socket exposes a JSON API so agents themselves can create panes, read output, and wait for state. Named sessions, remote-over-SSH, 14+ agent integrations, and an experimental zero-downtime live handoff.

The architectural contrast

herdr — Rust TUI multiplexer (first-party server owns the PTYs)kolu — SolidJS web ADE (R2: kaval is the survivor)background server (server/headless.rs)PTY actor per pane (pty/actor.rs)live handoff — SCM_RIGHTS fd-pass (server/handoff.rs)agent detect + socket API (detect/, api/)native resume — claude --resume (agent_resume.rs)foreground_client_id — shared geometry (server/headless.rs)kaval (node-pty + @xterm/headless mirror)kolu-server — provider DAG, runs fresh@kolu/surface (oRPC links: ws / stdio / direct)kaval-tui (raw client) — shipped: list / snapshot / attach (spawn/kill = Phase 3, planned)R2.4 recovery (capture -> drain -> respawn) — planned validates: thin survivor owns PTYs (A2)validates: snapshot-on-attach (A3) borrow DISCIPLINE; reject fd-pass (A1 / A7)Done=unseen rollup + optional hooks (U1 / U2)adopted: native session resume (G1, juspay/kolu#1495)GAP: resize arbitration (G2)
Module correspondence. SOLID edges = herdr validates a decision kolu already made, or a direct borrow. DASHED edges = a gap or an explicit non-goal. herdr owns one long-lived server; kolu's R2 inverts the survivor to kaval (the package renamed from @kolu/pty-host in R2.2) while the provider DAG runs fresh in kolu-server.
Concern herdr kolu (built + planned)
Who owns PTY lifetime First-party long-lived server owns every master fd; clients are stateless front-ends. Same bet. R2 makes kaval the thin survivor; the volatile provider DAG runs fresh in kolu-server.
Survive restart Server outlives clients; full restart restores from a snapshot; resume_agents_on_restore respawns agents. kaval-tui = client detach/reattach; R2.4 = daemon survives systemctl restart via cgroup-escape + reattach-by-id. The #1034 hazard lives here.
Recovery on owner restart Transactional: old owner stays alive and re-binds sockets until the new one acks; one bool gates who may signal children; injected-failure tested. R2.4’s composed captureSession → drainTerminals → respawn → finalize with waitForPidGone — designed to never repeat the “kill-then-pray” loss.
Late / lazy attach A live screen snapshot, never a byte replay: reset baseline → re-render the live emulator into one full frame. Same: ptyHost.ts subscribes then serializes a snapshot | delta union (~4KB).
Renderer Server diffs a cell-grid → ANSI. No web terminal. Raw VT → xterm.js in the browser; the headless mirror is for snapshot + taps only.

Architecture — what to adopt

UX — what to adopt

Gaps herdr surfaced

The two highest-leverage herdr ideas the plans never covered. One — native session resume — was clean and real, and kolu has since shipped it (G1, juspay/kolu#1495); the other is a pre-existing condition the plans still don’t arbitrate and remains open (G2).

What to do next

  1. R2.4, now (low risk): adopt the transactional handoff discipline (A1), the single-owner kill invariant (A2), the snapshot invariant test (A3), the two-axis honest-state + inline-recovery-hint (A4). Add the SCM_RIGHTS non-goal note (A7).
  2. Close G2 (multi-client resize arbitration) — Phase 2 shipped with documented last-resize-wins; an arbiter (and the size-change tap attach.ts already names) is still open.
  3. kaval-tui: A5’s socket path and G3’s attach TTY-guard shipped (#1084, #1255); carry the non-tty contract forward to Phase 3’s kill/spawn.
  4. UX: ship the attention rollup with Done = unseen (U1), keeping unread-bytes distinct from turn-finished; fold the navigator into the palette (U3).
  5. G1 (native --resume) shipped (#1495): the live agent.sessionId is persisted as a { kind, id } ref and replayed by id on wake/restore — the clearest adoptable, now done.
  6. U2’s blocked signal: #905 shipped (screen-scrape awaiting_user) — remaining: map awaiting_user into the U1 rollup as Blocked. Later: R9 reattach-hint UX (A6).

Net: herdr is the reference implementation for the survivor kolu already chose to build — most of its architecture validates R2 rather than redirecting it, with one battle-tested checklist to harden R2.4 (A1) and one explicit non-goal to write down (A7). The durable surprises were two gaps it surfaced: native session resume (now shipped by exact id, juspay/kolu#1495) and multi-client resize arbitration (still latent in an already-endorsed feature).