--- id: known-bugs/officer-blocks-its-event-loop-parsing-transcripts title: 'Officer blocks its own event loop for ~0.9s re-parsing a live transcript, and everything it serves stalls with it' theme: known-bugs status: open created: 2026-08-29 date_source: stated order: 7 --- **Found 2026-08-29 while chasing "Minesweeper is laggy today".** It is not a Minesweeper bug. The game's display is relayed through officer, and officer periodically stops answering anything for up to a second. **The measurements, in order.** Press-to-paint at x11vnc's own socket: **median 29ms, max 35ms**, stable even with a 1.5s idle gap before each click. Through the sidecar's own `/ws` relay: **median 29ms, max 43ms** — so the sidecar adds nothing. In the browser, where it is actually felt: **35ms to 200ms+, varying per click**. All of the variance is introduced between the sidecar and the browser, and the only thing there is officer. Officer answering a trivial unauthenticated 401 on `/api/health`, 30 samples: **median 1ms, p90 2ms, max 490ms.** The loop is fine until it is blocked outright. **What blocks it.** `summarizeTranscript` (`api/chat/claude-sessions.ts`) full-reads and `JSON.parse`s a whole transcript to build a session-list row. Timed against the largest on this machine — 196 MB, 70,137 entries: `readFileSync` **323ms**, split + parse every line **541ms**, **864ms blocking, per call**. It is cached, and the cache cannot help. The key is mtime, and the comment above it says why that was believed to be exact: _"Claude only ever appends"_. But a **live** session appends every few seconds, so its mtime moves, the entry misses, and the whole 196 MB is re-read and re-parsed — for a row in a list. The busier the agent, the more often. `468 MB across 174 transcripts` here, and the biggest one belongs to a commander that has been running all day. **Why it reads as a Minesweeper regression.** Nothing in the plugin changed (last commit two days ago), and the same cost was invisible when that transcript was 10 MB and this took ~45ms. It scales with transcript size, so it arrives gradually and then all at once. **Scope is much wider than the game.** Every HTTP request, every chat socket frame and every relayed plugin websocket shares that loop. The game is just the one surface where a 500ms stall is impossible to miss. **The fix the file argues for itself.** _"Claude only ever appends"_ is exactly what makes incremental summarisation correct: keep the running summary keyed on `(path, size)` and consume only the bytes appended since last time, instead of re-reading from zero. Turns ~864ms into the cost of the new tail. `listClaudePwds` already does a bounded head read for `cwd` (`claude-sessions.ts`, "a bounded head read"), so the precedent for not reading the whole file is in the same module. Not attempted here: it is a hot path shared by every surface, and the right shape — incremental parse versus moving summaries out of the request path entirely — is an architecture call rather than a patch.