I run a lot of coding agents at the same time: Claude Code in one worktree, Codex in another, a dev server, a couple of shells. The agent workspace I was using did the job, but it was an Electron app, and with a dozen terminals open it had quietly become the heaviest thing on my Mac.
So I built Krill: the same parallel-agent workflow as a native Rust desktop app (no Electron, no web view). This post is about the architecture, and about a benchmark I ran to check whether the rewrite was actually worth it.
What it does
- Each task gets its own Git worktree and terminal, so agents don't trip over each other.
- It works with any CLI agent (Claude Code, Codex, Cursor CLI, OpenCode, Antigravity) or a plain shell.
- Agent status hooks (
working,waiting,done,blocked...) feed a Needs You queue, so the agent waiting on your approval floats to the top. - Sessions outlive the app. Quit it, crash it, update it: the agents keep running and you reconnect to the same session IDs.
- A headless
krillCLI drives everything without the GUI (krill new,send,read,wait,attach).
Three processes, not one
The "sessions survive the app" part drove most of the design.
krill-app gpui UI. Renders only the panes you can see.
│ IPC
krilld session daemon: terminal state (alacritty_terminal),
│ 1,000 hot rows + zstd-compressed cold scrollback,
│ hooks, git watching, mobile gateway
krill-holder tiny PTY parent. Owns the file descriptors. Never panics.
- krill-holder is the only process that owns PTYs. It is deliberately small (the macOS build has a 1,300-line budget) and panics are banned there. If it dies, krilld still holds the fds.
-
krilld keeps the terminal state and is the only thing that answers terminal queries. Session work runs under
catch_unwind, so one bad session doesn't take down the others. If krilld crashes, the agents survive because the holder keeps the PTYs. Output that krilld had read but not yet written to a segment can be lost, and the UI shows an explicit gap marker rather than pretending nothing happened. - krill-app can be killed at any time. On restart it restores the layout and reattaches to the same sessions.
Rebooting, or losing both holder and daemon at once, does end the processes. Krill then respawns shells in the last working directory and keeps the old scrollback above them, but it doesn't pretend the old process survived.
The benchmark
I wanted a number I could defend, so I measured Krill 0.2.2 against Orca 1.4.237 (an Electron agent workspace with a similar feature set) on the same machine:
- Mac mini (Apple M6, 32 GB), macOS 27.0.1, release builds from
/Applications, no tuning - fresh isolated profile per run, same 1200×760pt window
- N login-shell tabs in the same git repo, one visible; each tab prints 20,000 lines (~2.8 MB of colored, wide, Unicode text)
- every process the app spawns is counted (helpers, GPU, renderer, daemons); shells are excluded on both sides
- 1, 10 and 20 terminals × 3 runs, alternating which app goes first; medians reported
At 10 terminals (Krill 0.2.2 vs Orca 1.4.237, measured 2026-10-25):
| Metric | Krill | Orca | |
|---|---|---|---|
Memory (phys_footprint) |
85.9 MiB | 1,274.9 MiB | 1/15 |
| Idle CPU (30 s average, screen locked) | 1.1% | 31.0% | 1/27 |
| CPU time to process the output | 1.2 s | 6.4 s | 1/5 |
| Install size | 92 MiB | 689 MiB | 1/7 |
Most of the gap is the UI layer: 30 MiB for the Krill app versus about 994 MiB for Electron's main, GPU and renderer processes. The terminal daemons differ too (46.5 vs 256.3 MiB), mostly because Krill keeps 1,000 hot rows per terminal and compresses the rest.
Where Krill lost, and other caveats
- Wall-clock output at 20 terminals: Krill took 7.5 s to finish the burst versus Orca's 4.0 s. Krill's runs varied a lot (1.9 to 7.9 s) and the machine was shared, so I can't yet say how much of that is load. It's a real result, though, and it's on my list.
- Screen locked: all runs were recorded with the display locked. Even then, Orca's GPU process used ~22% CPU. I haven't measured the screen-on case yet, so I'm not claiming the 1/27 holds there.
- Shared machine: other work (Rust builds) was running. Memory barely moves with load; CPU and timing can.
- Agent processes themselves are excluded. Claude Code costs the same in either app.
Try it
Apple Silicon macOS is the main platform, and there's a Windows x64 preview. The macOS build isn't notarized; the installer verifies the checksum and signature, then removes the quarantine attribute, so read it before running.
curl -fsSL https://krill.page/install.sh -o /tmp/krill-install.sh
# read it first
bash /tmp/krill-install.sh
It's Apache-2.0: github.com/craftowen/krill · krill.page
If you try it, the most useful feedback is which step blocked you first: install, opening a repo, starting an agent, creating a second worktree, or reconnecting after an app restart.


Top comments (7)
The three-process architecture is genius. The fact that the krill-holder process owns the PTYs and bans panic! completely explains why the sessions survive when the app crashes. That's the exact kind of reliability you need when you're running multiple AI agents overnight.
The benchmark numbers are wild—1/15th the memory and 1/27th the idle CPU is a massive win. But your honesty about losing the wall-clock output test at 20 terminals is just as valuable as the wins.
One technical question about the session persistence: when krilld reconnects to a session after a crash, how much of the 1,000 hot rows does it actually manage to preserve? Does the terminal state (alacritty_terminal) handle the exact cursor position and colors, or do you ever get a corrupted scrollback buffer after a heavy crash?
Thanks! Short answer: it depends on which process dies.
If only the app crashes, nothing has to be rebuilt. krilld holds the live alacritty_terminal state the whole time, so cursor, colors, alt screen etc. are exactly where they were. The app just asks for a fresh snapshot when it comes back.
If krilld itself crashes, the holder still has the PTYs, and the new krilld rebuilds each terminal from its on-disk segments: the last keyframe plus a replay of the raw bytes after it. The hot 1,000 rows aren't really the thing being "preserved" there; scrollback comes from the segments. The keyframe has to carry more than cells (scroll region, alt screen, origin mode and so on), and keyframes are only taken at byte boundaries where no escape sequence is half-parsed. Otherwise you get exactly the corruption you're describing.
What can be lost is the small slice krilld had read from the PTY but not written to a segment yet. When that happens the UI shows an explicit gap marker instead of pretending the output is continuous. Every chunk on disk also has a length and a CRC, so a torn write at the tail gets dropped instead of being replayed as garbage.
One caveat on my own numbers: the 1/27 idle CPU was measured with the screen locked. I haven't measured the screen-on case yet, so I wouldn't lean on that one too hard.
The detail about showing an explicit gap marker instead of pretending the output is continuous is excellent engineering. Most systems just silently fail or replay garbage. The CRC on the tail write is a nice touch too. Do you find that the CRC check adds noticeable latency during heavy I/O, or is it negligible at the segment level?
Honest answer: I haven't profiled the CRC on its own, so I can't give you a number.
What I can say is where it sits. The checksum is computed per chunk over the bytes that actually hit disk, which are already zstd-compressed (and encrypted, in the newer segment format), not over the raw terminal output. So it touches a lot fewer bytes than the PTY produced, and the compression step in front of it is the more expensive part. Disk writes are also rate-limited per session, so a burst of output doesn't turn into a burst of checksumming.
If I end up measuring the recording path separately I'll post the numbers here.
If you do end up profiling it, I'd be curious to see the breakdown between the compression step and the CRC itself. My guess is the compression dominates by an order of magnitude, but it'd be great to see real numbers.
Same guess here. The CRC is a single pass over bytes that are already compressed, so I'd be surprised if it's anywhere near zstd. But I haven't measured it, so I won't put a number on it. If I profile the recording path I'll post the split in this thread.
Same. CRC32 is usually measured in GB/s, while zstd is in hundreds of MB/s depending on the level. The compression step is going to dominate by at least an order of magnitude.