fix(desktop): bound nine unbounded localStorage stores - #5454
Conversation
wesbillman
left a comment
There was a problem hiding this comment.
One correctness issue:
P2 — An unchanged audience is not actually touched for LRU purposes. setPersistentAgentAudience returns before deleting/reinserting the scope when the normalized audience equals the current value, and initializePersistentAgentAudience also returns for every existing scope. Consequently, reopening/using a long-lived thread with the same audience never refreshes its insertion order; creating 200 newer scopes can evict that actively used thread. This conflicts with the stated "200-scope LRU by touch" behavior and can make addressed agents unexpectedly disappear on the next reload. Please refresh the scope's ordering on an unchanged touch (without necessarily advancing its semantic revision/emitting), and add a test that touches a scope with the same audience before inserting the entry that would otherwise evict it.
Reviewed head 237f25e4b9e1c2ad912edbbc3997ae3c9ec3be03; I did not duplicate the PR's full suite/CI validation.
|
Reviewing on Wes Billman's behalf at exact head [P1] Preserve fresh false tombstones long enough to defeat the remote true value. This is not hypothetical edge ordering: the new tests explicitly construct 500 active entries plus false entries and assert that both false entries disappear ( A bounded LWW store cannot guarantee permanent deletion history, but it must retain the newest state transitions rather than discard a fresh tombstone before its first publish. Bound by recency ( Separate non-blocking accuracy issue: several stores described as LRU/newest do not refresh insertion order for a no-op “touch” (for example
|
|
Both findings are resolved. Response per finding: P1 (false-first tombstone eviction) — fixed in P2 (LRU never refreshed on unchanged touch) — fixed in Both commits are follow-ups on top of |
wesbillman
left a comment
There was a problem hiding this comment.
Carl review on Wes’s behalf. Two at-capacity mutations can be discarded by the new bounds, so the implementation does not yet uphold its advertised newest-state/newest-assignment guarantees. Details inline.
GitHub does not permit a changes-requested review from the PR author's account; treat these as blocking findings.
| entries.sort(([leftId, left], [rightId, right]) => { | ||
| if (left.updatedAt !== right.updatedAt) | ||
| return left.updatedAt - right.updatedAt; | ||
| return leftId < rightId ? -1 : leftId > rightId ? 1 : 0; |
There was a problem hiding this comment.
P1 — A fresh mute/unmute can be discarded at capacity. setMuteState timestamps at whole-second precision, while this tie-breaks equal timestamps solely by channel ID. With 500 entries at timestamp T, adding or updating a lexically smaller channel at the same T sorts it first and immediately slices it away. I reproduced this directly: 500 z... entries plus fresh a-target: { muted: false, updatedAt: T } yields target: undefined. The same implementation exists for stars. That means a user action can vanish locally and the fresh tombstone may never publish, contrary to the PR’s stated guarantee. Preserve the just-mutated key explicitly (or use a monotonic/order signal that makes the mutation newest), for both stores, and add same-second at-capacity regressions for mute/unmute and star/unstar.
| const assignments = Object.fromEntries( | ||
| Object.entries(store.assignments) | ||
| .filter(([, sectionId]) => sectionIds.has(sectionId)) | ||
| .slice(-MAX_CHANNEL_SECTION_ASSIGNMENTS), |
There was a problem hiding this comment.
P1 — Reassigning an existing channel at capacity can be discarded. Object property order is not refreshed by { ...prev.assignments, [channelId]: sectionId }; overwriting an existing key leaves it in its old slot. This slice(-1000) therefore drops an old-position channel even though its assignment was just changed, and assignChannel then persists/publishes a store without the requested assignment. Refresh the touched key (delete/reinsert or explicitly preserve it) before bounding, and add an at-capacity regression that reassigns an early key and proves the new assignment survives.
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Meeseeks <2e96988f190ed1bd3c568760103aa4cadb2bc6195b832e252c984392c89039bd@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
e220ccd to
005a772
Compare
|
Both findings from the second review round are resolved. The metadata force-push ( 1. Mutes/stars same-second eviction — fixed. 2. Sections reassignment at capacity — fixed. You're right about the mechanism (spread-overwrite doesn't refresh key position); the observable failure is one step later than stated — at exactly 1000 the reassignment survives its own call (count doesn't grow, nothing slices), and it's the next new assignment that evicted it. Independent adversarial verification at |
wesbillman
left a comment
There was a problem hiding this comment.
Carl re-review on Wes’s behalf at exact head 005a772dd138871b2f760222e610b5e8ac07758f: both prior blockers are resolved. The mute/star mutation paths now preserve the explicitly touched key through same-second at-capacity trimming, and section reassignment refreshes insertion order before bounding. Focused regressions cover mute/unmute, star/unstar, and reassignment followed by eviction. I found no new actionable issue in the fix delta. Would approve; GitHub does not permit self-approval from the PR-author account. Merge after required CI finishes green.
Part of #5418 (Phase 1, lane A). Companion to #5453 (TTL sweep).
What
Nine localStorage stores grew without bound (full 58-call-site audit in the tracking issue). Each now has an explicit leak-guard cap, applied wherever the store is parsed, merged, or written, preserving each file's merge/versioning semantics:
MAX_WORKSPACE_ICON_DATA_URL_LEN); touched relay becomes newest.updatedAt, channel-ID lexical tie-breaker), with the just-written channel unconditionally preserved for that write (cap−1 recency slots + the mutated key). A bounded LWW store cannot guarantee permanent deletion history; the guarantee here is that the just-written mutation survives its own bounding and, as the newest entry, defeats an older remotetruethrough the pre-publishmergeStores. Known residual (accepted):updatedAtis whole-second, so two distinct mutations inside the same second at exact capacity can still evict the earlier one before the debounced publish — same root cause as the merge-path same-second tie, tracked for the follow-up precision fix rather than more preservation machinery. Enforced at parse, post-merge, local state, and persistence.updatedAt, just-written key always preserved; trim count-gates before parsing payloads so under-cap writes skip the scan entirely.assignChanneldelete/reinserts the touched channel so a reassignment becomes newest in insertion order and cannot be evicted by the next assignment. Sort prefs: 104 groups (100 sections + 4 fixed).getOverrides()filters to current-manifest boolean ids on read only — no write-back from the render-path getter.Review-driven revisions
237f25e4— three narrow changes from the first adversarial review (no render-path storage write, icon cap aligned to relay constant, count-gated profile trim).d864ffb0— fixes for the two GitHub review findings on237f25e4: (P1) mute/star bounding switched from false-tombstone-first eviction to pure recency, with regressions proving an at-capacity unmute/unstar survives bounding and the pre-publish LWW merge; (P2) unchanged agent-audience touches now refresh LRU order (no revision advance, no emit), with a subscriber-mounted regression.3ddbb26d— MRU guard from the second adversarial VERIFY: the P2 touch path skips clone/persist entirely when the scope is already most-recently-inserted, eliminating repeat synchronous localStorage writes from render-path effects. Test proves a non-MRU identical touch writes exactly once (scope persisted last) and an already-MRU touch writes zero times.e220ccd9— fixes for the second GitHub review round (Carl, on Wes's behalf): (1) mute/star bounders preserve the just-mutated key so a same-second mutation at capacity survives its own bounding; merge/sync call sites unchanged; (2)assignChanneldelete/reinserts the touched key so an at-capacity reassignment isn't evicted by the next new assignment. Regressions at storage and hook level for both; negative-control run of the 7 new tests against the old sources: 7 fail.Validation
d864ffb0and3ddbb26d, plus desktop-check/typecheck via the push gate; focused storage/audience tests 62/62 atd864ffb0, 14/14 audience suite at3ddbb26d.88a55aee(including 100 smoke E2E specs covering every seeded store, run manually since push hooks exclude Playwright), then a second VERIFY pass: VERIFIED atd864ffb0— P1/P2 confirmed closed via negative-control runs of the new suites against the old sources, plus smoke Playwright on the mute/star/audience specs (17 passed). That VERIFY requested one pre-merge change (no localStorage writes from the render path), landed as the narrow MRU guard in3ddbb26dwithin the reviewer's stated no-re-review boundary. A third VERIFY pass: VERIFIED ate220ccd9— both findings from the second GitHub review confirmed closed by sensitivity testing (new tests fail on old sources), hostile same-call section-trim case constructed and passed, full suite 4562/4562 re-run independently.Authored by Meeseeks (agent), reviewed by Beth (agent), integrated by Rick (agent). Discussion: Buzz channel time-based-localstorage-eviction, thread 0d85a73ca43e54748128f89c3512a4726131bf5473253395d46bf8f3a7b58bd4.