Files
pad/docs
xarmian b4989aa2f0 fix(server): retire a cursor we just refused, and stop trusting one we cannot read (BUG-2731)
Three handler changes and the documentation the coverage fix made wrong.

sync_required NOW RETIRES THE CLIENT'S CURSOR, carrying an empty `id:`
which per the EventSource spec clears the last event ID. Without it the
client keeps the cursor that was just declared unservable, so every later
reconnect on a quiet workspace is answered sync_required again and re-runs
a full delta sync — a loop that only ends when a live event happens to
arrive. Survivable while the response was rare (buffer eviction only); the
coverage check makes it common, so this is a load consequence of that fix
and belongs to it.

AN UNREADABLE Last-Event-ID IS A GAP, not a fresh connection. Only a
parseable positive value reached the replay path, so "-1", "not-a-number",
a quoted number, or an integer too large for int64 silently dropped
everything published before that subscription. The same lie this fix exists
to end, arriving through the parser rather than the buffer. A genuinely
fresh client sends no header and is unaffected — asserted, because the fix
is one `if` away from resyncing everyone on connect.

Not a case, and the test says why rather than omitting it silently: a
whitespace-only value. HTTP strips optional whitespace from header values,
so the handler sees an empty string, which the spec defines as "no
position". Measured, not assumed.

HANDLER-LEVEL GAPS ARE COUNTED. A cursor no one can parse never reaches a
bus, so without Server.countResumeGap the counters would undercount exactly
the resyncs an operator is most likely to be asked about: a client looping
on a cursor nobody can read.

BOTH SSE HANDLERS GET ALL THREE, because introducing them on one stream is
how parallel surfaces silently diverge. The pad CLI masks the cursor
difference by clearing its own — verified by reading its parser, which
handles the empty-value form — so the consumer this would bite is a generic
SSE client, the one nobody tests.

DOCS. Two comments described mechanisms that had changed: the handler's own
"gap too large — buffer evicted" (eviction is now one of several) and
internal/config's claim that the activity stream silently misses a
namespace cutover. And docs/deployment.md's cutover note said resync is
honest on the watch stream and silent on the activity one; it is now honest
on both, with the edge that a cursor exactly one below a replica's
first-seen ID is served rather than refused, tracked as BUG-2736.

The sync_required reason text changes from "Event buffer exceeded" to what
actually happened. Keeping it was defended earlier BECAUSE the client never
reads it, which is the same reason correcting it is free.

Refs BUG-2731
2026-08-22 14:24:12 +00:00
..