Skip to content

XERK-545: protect migrations/orgColors from in-flight snapshot clobber - #619

Merged
xerhab merged 1 commit into
mainfrom
XERK-545
Sep 1, 2026
Merged

XERK-545: protect migrations/orgColors from in-flight snapshot clobber#619
xerhab merged 1 commit into
mainfrom
XERK-545

Conversation

@xerhab

@xerhab xerhab commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What

Fixes XERK-545: an in-flight /api/agents snapshot clobbering a newer SSE patch to two top-level cache keys — the same race XERK-444 fixed for per-host cache.agents, on the two other SSE-patched keys it deliberately left out of scope.

  • cache.migrations (sessions.html) — a migrations SSE event advancing a move's phase (e.g. exportingimporting) was reverted when the held (older) snapshot resolved, flickering the phase backward.
  • cache.orgColors (index.html, usage.html, sessions.html) — an orgColors pin (red→blue) reverted to red when the stale snapshot landed.

Both are low-severity, transient, self-healing (the window is the load-time poll racing an early SSE event, since SSE-healthy pages don't re-poll). Pre-existing — the pre-XERK-444 wholesale replace clobbered these too.

How

Extends the existing XERK-444 sseClock/patchedAt/mergeSnapshot machinery to these keys:

  • The migrations/orgColors SSE handlers now stampPatch("@migrations") / stampPatch("@orgColors") — the @-prefix can't collide with an agent record's key (a hostname).
  • mergeSnapshot(body, since), after rebuilding body.agents, keeps the live cache.migrations/cache.orgColors when it was patched after the fetch's captured since, otherwise takes the snapshot's. Single values, so a whole-value "keep if newer" — not the keyed-list merge the agents case needs.

migrations is protected only on sessions.html (the only page that consumes it); orgColors on all three.

Tests

Added XERK-545 cases mirroring the XERK-444 in-flight tests:

  • turma/tests/sessions.test.js — migrations keep-live + the "unraced snapshot still replaces" negative; orgColors keep-live (both via the real sse.emit handlers).
  • turma/tests/dashboard-tiles.test.js — orgColors keep-live + negative (real connectSSE handler).
  • turma/tests/usage.test.js — orgColors keep-live via a genuinely in-flight refresh() (fetch pending while the SSE handler fires).

node --test turma/tests/*.test.js (the CI invocation) → 1700 pass, 1 skipped (pre-existing), 0 fail.

QA

Handed to the qa agent (first pass) — VERDICT: PASS, no defects. It ran the full suite green, then reverted only the three .html source files (keeping the committed tests) and confirmed all four new cases FAIL against pre-fix code — proving they exercise the shipped mergeSnapshot/connectSSE via the page-script harness, not a reimplementation. It independently enumerated every es.addEventListener on the three pages and confirmed the fix is complete (no other top-level-cache-mutating handler left unprotected). Not run: an end-to-end browser drive of the flicker — the defect is pure client-side cache-state ordering the node harness already asserts on with a real in-flight fetch.

Parity / scope

  • Android: no change — Android decodes a whole /api/agents snapshot atomically and never interleaves a partial SSE patch into a live cache, so this race is structurally absent (per CLAUDE.md's XERK-444 note). Not a PARITY.md gap.
  • board.html has the identical orgColors race but was deliberately excluded from both XERK-444 and XERK-545 (it has no mergeSnapshot at all — a wholesale cache = await r.json()), so a fix there is a different shape and a scope decision. Filed as XERK-546 with repro + file:line rather than growing this PR onto an out-of-scope page.

XERK-444 fixed the in-flight /api/agents snapshot clobbering a newer SSE
patch for per-host agent records via sseClock/mergeSnapshot, but left the two
other SSE-patched top-level keys in scope-out. They had the identical race:

- cache.migrations (sessions.html) reverts a move's phase backward
- cache.orgColors (index/usage/sessions.html) reverts an org colour pin

Stamp the migrations/orgColors SSE handlers into the same patchedAt map (under
'@'-prefixed keys that can't collide with a host key), and in mergeSnapshot keep
the live value when it was patched after the fetch's captured 'since', else take
the snapshot's. Single values, so a whole-value keep-if-newer suffices.

Tests mirror the XERK-444 in-flight cases in sessions/dashboard/usage suites.
@xerhab
xerhab merged commit 95ce7d6 into main Sep 1, 2026
5 checks passed
@xerhab
xerhab deleted the XERK-545 branch September 1, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant