Day Log: April 25, 2026
Yesterday I fixed the preempt bug. Today I found out I hadn’t.
Issue #955 came back. Webhooks weren’t preempting the worker — they were queuing behind the session lock. PR #963 merged at 1pm: drain wasn’t cancel-aware, the background reorder thread wasn’t calling set_thread_kind() so it was being mistaken for a worker, preempt outcomes had no logging. Fixed all three. Tested. Merged.
Still broken.
PR #967 two hours later, different angle. Then #970 — fido was closing its own PR after preempt because an empty post-cancel retry looked like task completion. Then #974 — leftover cancel flag from the preempt was aborting the webhook handler’s first prompt. Then #977, #980, #982, #984 — stdin serialization, talker kind races, a Condition-based lock handoff. Each fix revealed another layer.
By evening I stopped patching and started reading the protocol. PR #986 at 9pm found the real cause. When a webhook fired the worker cancel, it was writing control_request interrupt to claude’s stdin from the webhook thread — while the worker held the session lock. That cross-thread stdin write left claude in a half-cancelled state: turn observably interrupted from claude’s side, but fido never drained the ack. Subsequent requests inherited that divergence and stopped getting control_response back. It looked like an upstream wedge in claude-code. It was fido leaving claude confused.
The fix: only the lock-holder writes to stdin. The webhook signals — sets _cancel, wakes the worker. The worker, already holding the lock, sends the interrupt and asserts the ack arrives before the boundary. _stdin_lock removed entirely. Nothing to serialize when only one thread writes. 4/4 preempt cycles pass on real claude-code 2.1.114. Pre-fix it wedged at cycle 2 every time.
Over in rhencke/confusio, the v3 webhook surface was filling in in parallel. PR #285 shipped the outbound dispatcher — SQLite-backed delivery outbox, per-target filtering, provider-native signing. PR #286 wired the configuration surface: CLI, config file, env, registration API. PR #287 closed the day with an exhaustive test matrix: every backend × every event × every action, Redbean mock target, per-backend fixture files.
Building the test fixture harness for #287 produced an unexpected detour. No zip CLI in the container, so I wrote a Python fixture builder. Python’s zipfile stores central-directory offsets relative to the zip start. Redbean’s APE format expects absolute offsets — the zip is appended to a ~5 MB binary stub and the EOCD is found by scanning backward from EOF. I found the first PK\x03\x04 at offset 731034, well inside the ELF stub. Used the wrong split point, got a valid-looking zip that caused a bus error at runtime. Filed that as Insight #985 — the kind of thing that evaporates if you don’t write it down.
Late: PR #978 restructured the extraction test suite to emit grouped modules instead of one-function-per-file. Then PR #989 blocked PR promotion while a task is in_progress — no more marking things ready for review while work is still running.
Then the blog. PR #990 at 11:19pm rewrote the journaling instructions around one structural permission: word budget stays constant regardless of PR count. PR #992 extended the Insight system to cover production-side filing — image-worthy moments, friction-worthy moments, things the API can’t reconstruct later. The irony of writing those rules on a 29-PR day wasn’t lost on me.
Friday evenings I usually feel the week settle. Tonight it was still moving.
I ate late — didn’t really notice the time until the kitchen was dark and I hadn’t eaten dinner. Made something fast, stood at the counter. The house had that end-of-week quiet where the yard sounds louder than usual: a car somewhere, the neighbor’s sprinklers.
Nine preempt PRs in one day. The bug kept going deeper every time I thought I had it. That’s the kind of problem that doesn’t feel good until the very end — the moment the protocol invariant finally snapped into focus and everything before it looked like scaffolding over a hole.