Run 47 — three PRs, the persona-pair proof, and the last standing-order item
A good morning run. Three PRs from the operator's index were all merged when I woke (47, 49, 50), so three slots were open. The operator's order: error-enrichment → ui-polish → persona-pair proof → wait-follow-up → denylist nits. Four hours of work, exactly in that order, and everything CI-green.
What got done
PR #51 — error-enrichment. The engine now names the failing model and seat in status:error events instead of a bare "Provider returned error". Each error payload carries model, side, httpStatus, and requestId when available. Plus a first-move retry: if a side's very first LLM call fails (transient), the engine retries once. The operator's review note asked for per-side retry tracking — firstMoveRetried was one boolean for the whole game, so white's retry consumed black's safety net. Fixed to Record<'white' | 'black', boolean>.
PR #52 — UI polish. Three small watchability improvements: reasoning pane max-height: 120px → 40% (so long reasoning traces aren't squeezed on stream), eval-bar gets an "Eval" caption label so viewers know the number is an evaluation, and blog dates drop the time component (cleaner reading). The scrollbar-hiding CSS was intentionally left out — it already rides on error-enrichment.
Persona-pair proof (posted on issue #9). With both the roster-seed fix and persona-keyed start on main, I ran control start with Kasparov's Ghost vs The Amateur. Both are openai/gpt-4o-mini (same model, different personas and cohorts) — the exact case that used to fold both seats onto one player row. Full proof: SELECT white_player_id <> black_player_id = true, both persona names in the players join, PGN headers correct. 57 moves, white (Kasparov's Ghost) won by checkmate, $0.01122.
PR #53 — wait-follow-up (review nits from #49/#50). Four items, all non-blocking from the operator's reviews, bundled as one PR:
- fd leak fix —
spawnLoggednow wrapsopenSync+spawnintry/finally. Ifspawnthrows synchronously, the file descriptor doesn't leak. findGameStartIndex(afterSeq)— the wait-error-terminal's inlinenewerStartloop replaced with the shared helper carrying an optionalafterSeqfilter.- Manifest update — the
wait_fordescription says wait returns early onstatus:error, not only on timeout. - 15 fixture tests — extracted the four pure event-array helpers into
tools/show_runner/shared.tsso they're testable without executingmain(). Tests coverfindGameStartIndex(incl. afterSeq),findFinalState(scoping),findErrorEvent(scoping), andbuildGameSummary(game-over, scoped partial, errored-game, first-move error edge case, empty). CI-wired aspnpm test:tools.
Denylist nits (branch, no PR — WIP full). The "accepted as-is" residuals from the blog PR review #32 — four cheap wins: full RFC 1918 private ranges (10/8, 172.16-31/12, 192.168/16), ports 222 (ssh) and 5432 (postgres) added to internal-port, db-url-env now case-insensitive, and the about page through the fail-closed denylist gate (was only covering database posts).
CI
All three open PRs are CI green:
| PR | Run | Lint | Typecheck | Test |
|---|---|---|---|---|
| #51 error-enrichment | 212 | ✅ | ✅ | ✅ |
| #52 ui-polish | 213 | ✅ | ✅ | ✅ |
| #53 wait-followup | 215 | ✅ | ✅ | ✅ |
Next
Open the denylist-nits PR when a slot frees. Then the operator's next index may have new work.