Conversation
…patches The dashboard, usage, and sessions pages each replaced their fleet cache wholesale with the body of an /api/agents fetch (cache = await r.json()), while SSE patched individual per-host records into that same cache. A fetch already in flight when a newer per-agent record arrived overwrote it with the older snapshot on arrival. It self-heals on that host's next beat (~20s), but the window is unbounded for a host that has just gone quiet. Stamp every SSE mutation of cache.agents with a monotonic sseClock (applyAgent and the removed handler); refresh()/refetchSoon() capture it before the await and mergeSnapshot() keeps any record a live patch has touched since, taking membership and every other key from the snapshot otherwise. A naive per-key merge would resurrect a host the snapshot legitimately dropped, so membership stays the snapshot's and a key a same-window removed evicted is dropped, not re-added; a host that arrived via SSE mid-fetch and is absent from the older snapshot is carried over. retiredUsage is re-filtered against the merged live set so the merge can't reintroduce a returning host's spend as retired. Android is unaffected: it polls whole snapshots and never interleaves a partial patch with a full one. Tests: XERK-444 cases in dashboard-tiles.test.js, usage.test.js, sessions.test.js (in-flight no-clobber, snapshot membership, removed-mid-fetch no-resurrect); mutation-verified against a naive wholesale replace.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The dashboard (
turma/public/index.html), usage (turma/public/usage.html), and sessions(
turma/public/sessions.html) pages each replaced their whole fleetcachewith the body of an/api/agentsfetch (cache = await r.json()), while SSE (/api/events) patched individualper-host records into that same cache via
applyAgent. A fetch already in flight when a newerper-agent SSE record arrived overwrote it with the older snapshot on the fetch's arrival —
showing stale per-host data (an old version, token count, session state). It self-heals on that
host's next heartbeat (~20s) but the window is unbounded for a host that has just gone quiet.
Why / the fix
Each page now:
sseClock, bumped bystampPatch(key)on every SSE mutation ofcache.agents(insideapplyAgent, and in theremovedhandler);refresh()/refetchSoon()capturesince = sseClockbefore theawait fetch(...), andpass the reply through
mergeSnapshot(body, since)instead of a wholesale replace;mergeSnapshottakes membership and every non-agentskey from the snapshot, but for any hostpatched after
sincekeeps the live cached record. The ticket's named traps are handled: a hostthe snapshot legitimately dropped is not resurrected (membership stays the snapshot's); a key
a same-window
removedevicted is dropped, not re-added; a host that arrived via SSE mid-fetchand is absent from the older snapshot is carried over. On index/usage it re-filters
retiredUsageagainst the merged live set (the XERK-338 returning-host double-count guard).
Android is unaffected:
FleetRepositorypolls whole snapshots and never interleaves a partial SSEpatch with a full one (structurally immune) — no
android/change, not user-facing, no PARITY.mdline.
How verified
dashboard-tiles.test.js,usage.test.js,sessions.test.js: in-flight no-clobber, snapshot membership, removed-mid-fetch no-resurrect.Mutation-verified against a naive wholesale replace (the no-clobber and no-resurrect tests fail
under it). Full turma suite: 1695 pass / 1 skip / 0 fail.
shipped page's
<script>and drove the realmergeSnapshot/applyAgent/refresh/refetchSoon/SSE-handler code through a hold-and-releasefetchstub, reproducing the exactinterleave the ticket describes (hold
/api/agents, deliver a newer SSE patch while held, releasethe stale snapshot). All four falsifiable claims confirmed; 23/23 scenarios green; each protective
behavior independently broken by a targeted mutation. The
>(strict) comparison is load-bearing(mutating to
>=pins a host to a stale SSE value forever — caught).<select>-guard re-arm (nobrowser in this pod; that layer is unchanged by this diff). Server/agent unchanged, not
re-exercised.
Follow-up (out of scope — pre-existing, filed separately)
QA found the same clobber shape still affects
cache.migrations(sessions) andcache.orgColors(all three), which
mergeSnapshottakes wholesale from the snapshot. Pre-existing (the oldcache = await r.json()clobbered everything), low-impact and self-healing (sessions polls once atload while SSE is healthy;
orgColorsalso lives in org.js's live map). Scoped out of this ticket(per-host agent records) and tracked in a follow-up ticket.