내 AI 에이전트들이 해결하지 못한 세 가지 버그
요약
Rust로 작성된 Windows용 터미널 멀티플렉서 wimux 개발 과정에서 AI 에이전트가 해결하지 못한 버그 사례를 다룹니다. 창 분할 시 발생하는 데이터 손상 문제가 클라이언트가 아닌 상류(upstream)의 PTY 크기 조정 폭발 문제였음을 밝혀내는 디버깅 과정을 설명합니다.
핵심 포인트
- AI 에이전트가 작성한 코드라도 복잡한 시스템 버그는 해결하지 못할 수 있음
- 문제의 원인이 클라이언트(xterm.js)에 있다는 가설이 틀릴 수 있음을 경고
- 디버깅 시 상류(upstream) 데이터의 무결성을 확인하는 것이 결정적임
- PTY 크기 조정 시 발생하는 급격한 중간 크기 변화가 데이터 손상의 원인
지난 몇 주 동안 저는 Rust로 작성된 Windows용 터미널 멀티플렉서(terminal multiplexer)인 wimux를 만드는 데 시간을 보냈습니다. tmux와 zellij는 Unix 우선적입니다. Windows에서는 WSL 내부에서만 실제로 작동하며 네이티브 셸(native shell)과는 분리되어 있습니다. 저는 지속적인 PowerShell 세션을 원했고, 그래서 직접 작성했습니다.
대부분의 코드는 AI 에이전트들이 작성했습니다. 제 커밋(commit)에도 명시적으로 (Co-Authored-By: 트레일러) 기록되어 있으며, 누군가 나중에 발견하게 하기보다는 먼저 밝히는 편이 낫다고 생각했습니다. 제가 쓰고 싶은 내용은 에이전트들이 생성한 코드가 아닙니다. 에이전트들이 해결하지 못한 세 가지 버그에 대해 쓰고 싶습니다. 왜냐하면 그 부분이 흥미로운 부분으로 드러났기 때문입니다.
버그를 이해하기 쉽게 구조를 간략히 설명하자면 다음과 같습니다: 분리된 사용자별 데몬(daemon)이 세션을 소유하고, ConPTY (portable-pty)를 통해 자식 프로세스를 구동하며, 서버 측에서 터미널을 에뮬레이션(emulate)합니다. 클라이언트 — TUI, xterm.js를 실행하는 Tauri v2 GUI, 그리고 스크립트 가능한 CLI — 는 postcard 바이너리 프로토콜을 사용하여 Windows 네임드 파이프(named pipe)를 통해 연결됩니다. 즉, Rust 데몬, WebView, 그리고 PTY라는 세 개의 레이어가 있으며, 각 레이어는 화면이 어떻게 보이는지에 대해 저마다의 개념을 가지고 있습니다.
버그 1: 모두가 찾던 곳에 있지 않았던 데이터 손상
증상: 전체 화면 TUI 앱이 실행 중인 상태에서 창(pane)을 분할하면, 화면이 고립된 문자들 — 더 좁은 렌더링에서 남겨진 박스 그리기 글리프(box-drawing glyphs) — 로 가득 찹니다.
모든 정황은 클라이언트를 가리켰습니다. xterm.js가 픽셀을 그리는 주체이고, GUI는 레이아웃이 변경될 때 DOM 노드의 부모를 재설정(reparent)하며, 세션에 다시 연결(re-attach)하면 손상 현상이 사라졌기 때문입니다. 명백한 클라이언트 버그처럼 보였습니다.
우리는 다음 순서로 네 가지 수정 시도를 했습니다:
- DOM의 부모를 재설정하지 않기 — 대신 창의 위치를 절대 좌표(absolute)로 지정하기. 여전히 작동하지 않음.
- 데몬이 각 크기 조정(resize) 후에 새로운 스냅샷을 푸시하도록 하기. 여전히 작동하지 않음.
- 구조적 변경 시 xterm 인스턴스를 재생성하기. 여전히 작동하지 않음.
- 레이아웃이 안정된 후 실행되도록 재연결(reattach)에 디바운스(Debounce) 적용하기. 여전히 작동하지 않음.
네 가지 그럴듯한 해결책, 네 번의 실패. 각각은 합리적인 이론이었고, 모두 클라이언트 (client)를 대상으로 했습니다.
문제를 해결한 결정적인 움직임은 30초밖에 걸리지 않았습니다. 저는 xterm.js에 대해 전혀 들어본 적 없는, 데몬 (daemon) 자체의 창 (pane) 뷰를 사용하여 서버로부터 터미널 그리드 (terminal grid)를 캡처했습니다.
wimux agent capture -t 0 -p 2
고립된 글리프 (glyphs)들도 그곳에 있었습니다.
그 단 한 번의 출력 결과가 클라이언트 전체를 배제했습니다. 손상은 단 하나의 픽셀이 그려지기 전부터 존재했습니다. 진짜 원인은 우리가 건드렸던 모든 것보다 상류 (upstream)에 있었습니다. 창 (pane)을 분할하면 PTY로 **중간 크기들의 폭발적인 변화 (burst of intermediate sizes)**가 전송됩니다. 내부 앱은 각 단계마다 다시 그리기 (redraw)를 수행하는데, 이 점진적인 다시 그리기가 더 이상 덮지 않는 영역을 지우지 못하며, 그 잔여물들이 그리드, 즉 서버의 그리드에 남게 되는 것이었습니다.
해결책은 세 줄이었습니다. 크기 조정 (resize) 이벤트를 디바운스 (debounce) 하여 PTY가 오직 최종적으로 안정된 크기만을 보도록 만드는 것이었습니다.
**교훈은
- 클릭→포커스 (click→focus) 메커니즘. 동일한 xterm 버전과 동일한
mousedown→term.focus()를 사용하여 순수 Chromium 페이지에서 핸들러를 그대로 재구축했습니다. 첫 번째 키 입력은 매번 성공했습니다. 이것이 아니었습니다. - 레이아웃 재렌더링 (re-render)에 의한 포커스 탈취. 코드를 분석한 결과: 레이아웃이 변경되지 않으면 아무 작업도 수행하지 않으며(no-op), 재포커스(refocus)는 방어 로직에 의해 보호되고 있었습니다. 이것이 아니었습니다.
- 다른 프로세스의 포커스 탈취. 타이핑하는 동안
GetForegroundWindow와GetGUIThreadInfo를 로깅했습니다. 포커스는 전혀 이동하지 않았습니다. 이것이 아니었습니다. - 매 키 입력마다 발생하는 포커스 변동 (Focus churn). 페이지 내 오버레이에서
document.hasFocus()를 로깅한 결과, 모든 키 입력이 터미널의 텍스트 영역(textarea)에 전달되고 있었습니다. 제가 원인으로 지목했던 blur/focus 이벤트는 제가 Alt-Tab을 누를 때 발생한 것이었습니다. 이것이 아니었습니다. - 터미널 포커스 보고 (DECSET 1004) — 아주 매력적인 가설이었습니다. 터미널이 포커스 시
ESC [ I를 방출하고, PSReadLine이 이를 처리하지 못해 비프음(beep)을 내며 다음 문자를 삼켜버린다는 가설입니다. 이는 모든 증상을 설명해 줍니다. 해당 모드를 활성화하고 xterm이 실제로 무엇을 방출하는지 관찰했지만, 아무것도 나오지 않았습니다. ConPTY가 해당 시퀀스를 삼켜버린 것이었습니다. 이것이 아니었습니다.
다섯 가지 가설과 다섯 가지 반박, 이 모든 것은 논쟁이 아닌 관찰에 근거했습니다. 결국 해결책이 된 것은 **재현 하네스 (reproduction harness)**를 구축하는 것이었습니다. 이는 창(pane)을 클릭한 후 일정 지연 시간을 두고 키 입력을 보내는 스크립트로, 지연 시간을 0ms에서 150ms까지 수십 번에 걸쳐 훑으며 테스트했습니다. 사람은 의도적으로 50ms의 타이밍을 맞출 수 없지만, 스크립트는 이를 연속으로 40번 수행할 수 있습니다.
결과: 손실 0건. 창 _내부_를 클릭하는 것은 결코 문제가 아니었습니다.
그래서 저는 하네스의 대상을 사이드바로 돌렸습니다. 세션 항목을 클릭하여 워크스페이스를 전환하도록 말이죠. 4번의 키 입력 중 4번 모두 손실되었으며, 로그를 통해 메커니즘이 명확히 드러났습니다:
MD pane=- target=SPAN.name active=TEXTAREA.xterm-helper-textarea
== BLUR == 터미널이 포커스를 잃음
KEY "k" active=BODY target=BODY 키 입력이 <body>에 전달됨
...
사이드바 항목들은 단순한 <div>입니다. 이들은 포커스(focus)를 가질 수 없으므로, 항목을 클릭하면 문서의 포커스가 <body>로 떨어집니다. 그러면 두 가지 지연(delay)이 중첩됩니다. 이름을 바꾸기 위한 더블 클릭을 구분하는 200ms 타이머와, 창(panes)을 재구축하기 위한 300~450ms의 서버 왕복 시간(round-trip)입니다. 이 0.5초 동안 키 입력은 어디로도 전달되지 않으며, Windows는 편집 불가능한 요소로 전송된 키에 대해 비프음을 울립니다.
해결책은 진단 결과에서 도출되었습니다. 터미널 앱에서 포커스된 필드가 없는 키 입력은 활성 창(active pane)에 속합니다. 키 입력은 해당 창으로 전달되며, 만약 전환(switch)이 진행 중이라면(즉, 대상 창이 아직 존재하지 않는다면) 입력은 버퍼링되었다가 전환된 창으로 다시 재생(replay)됩니다.
발견했을 때와 동일한 방식으로 검증했습니다: 이전에는 0/4개의 키 입력이 전달되었으나, 수정 후에는 4/4개가 모두 각 세션의 올바른 창으로 전달되었습니다.
버그 3: 회계(accounting) 문제였던 지연 현상
증상: 여러 개의 창이 열려 있을 때 타이핑 지연이 심하게 발생함.
이 문제는 빠르게 해결되었으며, 에이전트들이 어려워하는 버그 유형의 가장 명확한 사례입니다. 어디에도 잘못된 것이 없습니다. 모든 코드 라인은 정확합니다. 데몬(daemon)은 PTY 출력을 읽고 청크(chunk)당 하나의 IPC 메시지를 방출했습니다. 각 메시지는 WebView로 넘어가 단일 JavaScript 스레드에 도달했습니다. 여러 창에서 출력이 생성되면서, 해당 스레드는 렌더링 대신 메시지 디스패치(dispatch)에 모든 시간을 소비하고 있었습니다.
해결책은 이미 사용 가능한 데이터를 모두 비우고(drain), 동일한 창에서 오는 연속된 청크들을 하나의 메시지로 병합하되 최대 64 KiB로 제한하는 것이었습니다.
버그를 수정한 것이 아닙니다. 회계(accounting) 방식의 결정을 변경한 것입니다. 이는 에이전트들이 스스로 도달하기 어려운 범주인데, 찾아낼 '오류'가 없기 때문입니다.
성공을 보고하고 아무것도 쓰지 않은 에이전트들
언급할 가치가 있습니다. 저를 한 시간 동안 혼란에 빠뜨렸기 때문입니다.
저는 여러 에이전트에게 작업을 분산시켰고, 각 에이전트는 자신만의 격리된 git 워크트리 (worktree)에서 작업했습니다. 그들 모두는 정중하고 구조화된 문장으로 성공을 보고했습니다. 하지만 단 하나도 파일을 작성하지 않았습니다. 비대화형 모드 (non-interactive mode)에서는 파일 시스템에 접근하기 위해 명시적인 권한 플래그 (permission flag)가 필요한데, 그 플래그가 없었기에 그들은 수행했을 작업에 대해 이야기만 늘어놓았던 것입니다.
저를 구해준 것은 리뷰 단계가 그들의 보고서를 읽지 않았다는 점입니다. 대신 각 워크트리에서 git status를 실행하여 변경된 파일의 수를 세었습니다. 결과는 0이었고, 그 0은 사실이었습니다.
보고서를 믿지 말고, 결과물 (artifact)을 믿으세요. 이것은 이런 방식으로 작업하며 얻은 가장 유용한 습관입니다.
이런 방식으로 구축하는 것에 대해 여러분께 드리고 싶은 말
에이전트들은 제가 혼자 하는 것보다 더 빠르게 작동하는 멀티플렉서 (multiplexer)를 만들어냈습니다. 하지만 그들은 첫 번째 버그에 대해 확신에 찬 잘못된 수정 사항을 네 번 연속으로 내놓았고, 제가 요청했다면 다섯 번째 수정안도 내놓았을 것입니다.
그들이 스스로 하지 못하는 것은 바로 멈춰서 증거를 찾아 나서는 것입니다. 그들은 그럴듯한 다음 단계를 생성하는데, 그 '그럴듯함'이야말로 여러분이 방어해야 할 바로 그 실패 모드 (failure mode)입니다. 이 세 가지 버그는 모두 동일한 방식으로 해결되었습니다: 아무도 들여다보지 않던 계층의 상태를 포착하는 것이었습니다.
- 버그 1: 브라우저 대신 서버에서 그리드 (grid)를 읽기.
- 버그 2: 컴포넌트 경계 (component boundaries)를 파일에 로그로 남긴 후, 인간은 맞출 수 없는 타이밍 윈도우 (timing window)를 몰아붙이는 하네스 (harness)를 구축하기.
- 버그 3: 아무것도 고장 나지 않았음을 인지하고 메시지 개수 세기.
이 중 어느 것도 특이한 것이 아닙니다. 이는 평범한 디버깅 규율 (debugging discipline)입니다. 30초마다 확신에 찬 답변을 건네주는 무언가가 있을 때 건너뛰기 쉬운 그런 규율 말입니다. 여기서 한 가지만 기억하신다면: 세 번의 수정이 연속으로 실패한다면, 수정을 멈추세요. 여러분이 공유하고 있는 가정이 바로 버그입니다.
wimux는 MIT 라이선스이며 Windows 10/11에서 실행됩니다:
cargo install wimux wimux-server
리포지토리 (Repo), 데모 GIF 및 전체 아키텍처 (architecture) 설명글:
github.com/fabperso/wimux
질문은 언제든 환영합니다. 특히 가장 골치 아팠던 부분인 ConPTY에 관한 질문이라면 더욱 좋습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기