DEV Community

Cover image for Three bugs my AI agents couldn't fix
fabperso
fabperso

Posted on

Three bugs my AI agents couldn't fix

I spent the last few weeks building wimux,
a terminal multiplexer for Windows written in Rust. tmux and zellij are
Unix-first — on Windows they only really live inside WSL, detached from the
native shell. I wanted persistent PowerShell sessions, so I wrote them.

Most of the code was written by AI agents. My commits say so explicitly
(Co-Authored-By: trailers), and I'd rather lead with that than have someone
discover it. What I want to write about isn't the code the agents produced —
it's the three bugs they couldn't fix, because those turned out to be the
interesting part.

A quick sketch of the thing, so the bugs make sense: a detached per-user daemon
owns the sessions, drives child processes through ConPTY (portable-pty),
and emulates the terminals server-side. Clients — a TUI, a Tauri v2 GUI running
xterm.js, and a scriptable CLI — attach over a Windows named pipe with a
postcard binary protocol. So: a Rust daemon, a WebView, and a PTY, three
layers that each have their own idea of what the screen looks like.


Bug 1: the corruption that wasn't where everyone looked

Symptom: split a pane while a full-screen TUI app was running, and the
display filled with orphaned characters — box-drawing glyphs stranded from a
narrower render.

Everything pointed at the client. xterm.js is the thing drawing pixels; the
GUI reparents DOM nodes when the layout changes; re-attaching to the session
made the corruption disappear. Obvious client bug.

We tried four fixes, in this order:

  1. Stop reparenting the DOM — position panes absolutely instead. Still broken.
  2. Have the daemon push a fresh snapshot after each resize. Still broken.
  3. Recreate the xterm instance on structural changes. Still broken.
  4. Debounce the reattach so it happens after the layout settles. Still broken.

Four plausible fixes, four failures. Each one was a reasonable theory, and each
one was aimed at the client.

The move that cracked it took thirty seconds: I captured the terminal grid
from the server, using the daemon's own view of the pane — the one that has
never heard of xterm.js.

wimux agent capture -t 0 -p 2
Enter fullscreen mode Exit fullscreen mode

The orphaned glyphs were there too.

That single output eliminated the entire client. The corruption existed before
a single pixel was drawn. The real cause was upstream of everything we'd been
touching: splitting a pane sends a burst of intermediate sizes to the PTY.
The app inside redraws at each one, its incremental redraws don't clear what
they no longer cover, and the leftovers stay in the grid — in the server's grid.

The fix was three lines: debounce resize events so the PTY only ever sees the
final, settled size.

The lesson isn't "debounce your resizes." It's that four consecutive fixes
failing is data. It means the assumption underneath all four is wrong — and no
amount of new fixes at the same layer will help. Go get an observation from a
different layer instead.


Bug 2: the keystroke that vanished

Symptom: click a pane, type — the first keypress doesn't appear, and the
machine beeps. Press again and it works.

This one resisted for days, partly because every reasonable theory was wrong.
Here's what I eliminated, and how:

  • The click→focus mechanics. I rebuilt the exact handler in a bare Chromium page — same xterm version, same mousedownterm.focus(). The first keypress went through, every time. Not it.
  • A layout re-render stealing focus. Reading the code: on an unchanged layout it's a no-op, and the refocus is guarded. Not it.
  • Another process stealing the foreground. I logged GetForegroundWindow and GetGUIThreadInfo while typing. Focus never moved. Not it.
  • Focus churn on every keystroke. An in-page overlay logging document.hasFocus() showed all keystrokes landing on the terminal's textarea. The blur/focus events I'd blamed were me alt-tabbing. Not it.
  • Terminal focus reporting (DECSET 1004) — a beautiful theory: the terminal emits ESC [ I on focus, PSReadLine chokes on it, beeps, and eats the next character. It explains every symptom. I enabled the mode and watched what xterm actually emitted: nothing. ConPTY had swallowed the sequence. Not it.

Five theories, five refutations, all backed by an observation rather than an
argument. What finally worked was building a reproduction harness: a script
that clicks a pane and sends a keystroke after a delay, sweeping the delay from
0 ms to 150 ms, dozens of times. A human can't hit a 50 ms window on purpose. A
script does it forty times in a row.

Result: zero losses. Clicking inside a pane was never the problem.

So I pointed the harness at the sidebar instead — clicking a session entry to
switch workspaces. Four out of four keystrokes lost, with a log that made
the mechanism unmistakable:

MD  pane=-  target=SPAN.name  active=TEXTAREA.xterm-helper-textarea
== BLUR ==                          the terminal loses focus
KEY "k"  active=BODY  target=BODY   the keystroke lands on <body>
== FOCUS ==                         focus returns ~310 ms later
Enter fullscreen mode Exit fullscreen mode

The sidebar entries are plain <div>s. They aren't focusable, so clicking one
drops document focus to <body>. Then two delays stack: a 200 ms timer that
disambiguates double-click-to-rename, plus a 300–450 ms server round-trip to
rebuild the panes. For half a second, keystrokes go nowhere — and Windows
beeps at a key sent to a non-editable element.

The fix follows from the diagnosis: in a terminal app, a keystroke with no
focused field belongs to the active pane. It gets delivered there, and if a
switch is in flight — the target pane doesn't exist yet — it's buffered and
replayed into the pane you switched to.

Verified the same way it was found: 0/4 keystrokes delivered before, 4/4 after,
each in the correct session's pane.


Bug 3: the lag that was an accounting problem

Symptom: with several panes open, typing lagged badly.

This one was quick, and it's the clearest example of a class of bug agents are
bad at: nothing is wrong anywhere. Every line of code is correct. The daemon
read PTY output and emitted one IPC message per chunk. Each message crossed
into the WebView and hit a single JavaScript thread. With several panes
producing output, that thread spent its life in message dispatch instead of
rendering.

The fix was to drain what's already available and merge consecutive chunks from
the same pane into one message, capped at 64 KiB.

No bug was fixed. An accounting decision was changed. That's a category agents
rarely reach for on their own, because there's no error to find.


The agents that reported success and wrote nothing

Worth a mention, because it cost me an hour of confusion.

I fanned a task out across several agents, each in its own isolated git
worktree. All of them reported success in careful, structured prose. Not one
had written a single file: in non-interactive mode they needed an explicit
permission flag to touch the filesystem, and without it they narrated the work
they would have done.

What saved me was that the review step didn't read their reports — it ran
git status in each worktree and counted changed files. It said 0, and the
0 was true.

Trust the artifact, never the report. This is the single most useful habit
I've taken from working this way.


What I'd tell you about building this way

Agents produced a working multiplexer faster than I could have alone. They also
produced four consecutive confident wrong fixes for the first bug, and would
have produced a fifth if I'd asked.

The thing they don't do on their own is stop and go get evidence. They
generate plausible next steps, and plausible is exactly the failure mode you
have to defend against. Every one of these three bugs was solved by the same
move: capture the state at a layer nobody was looking at.

  • Bug 1: read the grid from the server instead of the browser.
  • Bug 2: log the component boundaries to a file, then build a harness that hammers the timing window a human can't hit.
  • Bug 3: notice that nothing was broken, and count messages.

None of that is exotic. It's ordinary debugging discipline — the kind that's
easy to skip when something is handing you a confident answer every thirty
seconds. If you take one thing from this: when three fixes in a row fail,
stop fixing. Your shared assumption is the bug.


wimux is MIT-licensed and runs on Windows 10/11:

cargo install wimux wimux-server
Enter fullscreen mode Exit fullscreen mode

Repo, demo GIF and the full architecture write-up:
github.com/fabperso/wimux

Happy to answer questions — especially about ConPTY, which was by far the
messiest part.

Top comments (0)