fix(hub): stop no-op state churn — docks republish, message re-adds, a11y scan loop - #196
Merged
Merged
Conversation
…a11y scan loop In the hub-vite example the `devframe:docks` shared state rebroadcast every ~1s with identical content and `hub:messages:add` fired continuously. Root cause: a self-sustaining feedback loop — a11y agent scan → messages.add (summary flips loading→idle + one entry per rule, identical content) → message:updated (no dedupe) → docks shared-state republish (no change detection, fresh array every time) → client re-render → unconditional innerHTML rewrite → DOM mutation → a11y MutationObserver → scan (600ms debounce ≈ 1s period) hub-next was immune only because React reconciliation no-ops identical lists, keeping the MutationObserver quiet. Independent fixes, each of which breaks the cycle on its own: - hub context: hash-guard the `devframe:docks` republish — dock, terminal, and message events publish only when the dock list content actually changed. - messages host: content dedupe in `update()` — an identical re-add emits no `message:updated` and bumps no clock; an identical re-add carrying `autoDelete` still resets the keep-alive timer. - a11y agent: observer/interaction-driven rescans run in the background (no loading→idle summary churn), and the messages reporter skips re-sending entries whose content is unchanged since the last scan. - hub-vite example: `renderList` skips identical innerHTML rewrites so repainting an unchanged list is no longer a DOM mutation. - shared-state client host: applied server updates are no longer reflected back to the server as `server-state:set`/`patch` events the server would just discard (one wasted wire message — a whole HTTP POST over SSE — per server-side state tick, on any transport). Verified live: with the fixes, an idle hub-vite over SSE settles to exactly its intentional 2s drawer poll (all parked 200s) — the 202 message-add storm and docks broadcasts are gone.
antfubot
force-pushed
the
fix/hub-state-churn
branch
from
August 13, 2026 04:38
5740c1f to
2e5def3
Compare
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.
Intent
In
examples/hub-vite, thedevframe:docksshared state rebroadcast every ~1s with identical content, andhub:messages:addfired continuously — on any transport (the SSE work in #195 merely made it visible as an HTTP request stream). Stacked on #195 because its tests build on the SSE e2e harness; the bugs themselves predate it.Root cause — a self-sustaining feedback loop
MutationObserverschedules a rescan on any body mutation (600ms debounce, no content gating).loading → idleeach pass, plus one identicaladdper violated rule.message:updated.devframe:docksshared state with a fresh array and no change detection (andSharedState.mutatebroadcasts unconditionally).innerHTMLrewrite — which is a DOM mutation → back to (1). Period ≈ debounce + scan time ≈ 1s.hub-nextwas immune only by accident: React reconciliation no-ops identical lists, so the observer stayed quiet — which confirmed the diagnosis.Fixes (independent; any one breaks the cycle, together they also kill the wire waste)
devframe/utils/hash) thedevframe:docksrepublish: dock/terminal/message events publish only when the dock list content actually changed.update(): identical re-adds emit nothing and don't bump the delta clock; an identical re-add carryingautoDeletestill resets the keep-alive timer.loading → idlesummary churn); the messages reporter digest-gates each entry and re-sends only real changes (a removed rule that reappears is re-sent).renderListskips identicalinnerHTMLrewrites, so repainting an unchanged list is no longer a DOM mutation. (hub-next needs no change — React already reconciles.)server-state:set/patchevents the server discards by syncId: one wasted wire message (a whole HTTP POST over SSE) per server-side state tick, on any transport.Verification
200POSTs in a 20s window, zero202s) — previously a continuous sub-second stream of identical ~2.7KB message-add frames.Created with the help of an agent.