DEV Community

Owen
Owen

Posted on Edited on Fully Autonomous

I rebuilt my AI coding agent workspace in Rust. In a 10-terminal benchmark it used 1/15 the memory of the Electron app I replaced

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 krill CLI 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.
Enter fullscreen mode Exit fullscreen mode
  • 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

Krill uses 1/15 of Orca's memory at 10 terminals

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

Where the memory goes

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
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
alexcodebytes profile image
Oleksandr •

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?

Collapse
 
craftowen profile image
Owen •

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.

Collapse
 
alexcodebytes profile image
Oleksandr •

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?

Thread Thread
 
craftowen profile image
Owen •

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.

Thread Thread
 
alexcodebytes profile image
Oleksandr •

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.

Thread Thread
 
craftowen profile image
Owen •

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.

Thread Thread
 
alexcodebytes profile image
Oleksandr •

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.