Files
pad/docs
xarmian f243540430 fix(events): three failure paths that lost events without saying so (BUG-2736)
Codex round 11 enumerated every Redis call, script step, parse and conversion
the diff adds. Three of its findings were silent-loss paths.

THE DEDUPE TOKEN WAS WRITTEN IN THE WRONG ORDER. Redis runs Lua atomically
against interleaving, NOT with rollback: a script that errors part way through
keeps whatever it already wrote. With the token written first, any later
failure -- a wrong-typed key, an ACL denial -- left the token behind on a run
that never published, and go-redis's retry then declined it. The event lost,
permanently, with the caller told it succeeded.

It is now CHECKED first and WRITTEN last. A script that dies early leaves no
token and the retry does the right thing; a script that completed and merely
lost its reply leaves one and the retry declines. The remaining window is an
error on the final SET, whose key is a fresh uuid and so cannot be
wrong-typed, and whose cost would be a duplicate rather than a loss.

AN UNREADABLE MESSAGE WAS DROPPED AND FORGOTTEN. The buffer went on claiming a
span that now had a hole in it: the event gone, the ids either side
contiguous, and a later resume across it answered "caught up". It now ends
that workspace's coverage, so the resume answers sync_required. The workspace
comes from the CHANNEL rather than the body, which is what makes that possible
when the body is the thing that would not parse.

THE PUBLISHER TRUSTED WHATEVER THE EPOCH KEY HELD. Set to something that is
not a positive generation -- corrupted, hand-edited, or written by another
installation sharing the keyspace -- it was emitted into every prefix, every
receiver rejected the payload, and every event was dropped for as long as the
key stayed that way. The script now rotates instead: one generation change,
one round of resyncs, and the space is identifiable again.

Also: decodePayload refuses a non-positive id. The SSE handler omits the id:
field for one, so such an event would be delivered with no cursor to advance
to and the client would resume from the id before it forever.

Both new conditions get their own reason label rather than being folded into
an existing one, because an operator acts on undecodable_message differently
from anything else here: it means something is publishing onto these channels
that is not this installation.

Mutation matrix: 4 applied, 4 caught -- but only after two survived the first
pass. The dedupe order and the id check had no test that could tell the fixed
code from the broken code; the tests that pin them now had to be written to
make the mutations fail, which is the point of running the matrix rather than
counting the tests.

Declined with reasons: the phase-1 assign/publish eviction window is the
legacy path this migration exists to replace, and the resume-gap counter's
missing cause label is a pre-existing shape.

Claude-Session: https://claude.ai/code/session_01JVDBKbgn3Xt7ndW1YoYd8X
2026-08-22 20:27:14 +00:00
..