Repository navigation
fix(channels): keep client-only state (PSK selection, My Channels, unread) across a channel-list refresh #152
Description
Summary
loadChannels() in public/channels.js replaces the channel list with the server snapshot and carries nothing across. State that only exists in the tab is lost on every refresh, and an open PSK conversation is closed. Same root cause as upstream Kpa-clawbot/CoreScope#2095 (proposed fix Kpa-clawbot/CoreScope#2096); this fork has one extra trigger (shared-channel approval).
Where (master 571fb2c8)
loadChannels() (around line 1856): fetch → map lastActivityMs → ChannelProposals.mergeApprovedChannels(...) → sort → renderChannelList() → reconcileSelectionAfterChannelRefresh(). mergeUserChannels() is not called here.
reconcileSelectionAfterChannelRefresh() (around line 249) clears the selection when selectedHash is not in channels. A user:* PSK hash is never in the server snapshot, so it always is cleared.
Triggers:
- Region filter change (
RegionFilter.onChange, around line 1013). - "Show encrypted channels" toggle (
mc-channels-show-encrypted-changed, around line 1328). - Fork only: shared-channel approval (
ChannelProposals.mount({ onApproved }), around line 1047) callsmergeUserChannels()only afterloadChannels()has already reconciled.
Symptoms
With a user-added PSK channel open:
- the conversation closes ("Choose a channel from the sidebar"),
messagesis emptied and the URL is rewritten to#/channels; - the My Channels section disappears until something re-runs
mergeUserChannels(); - unread badges reset. Unread is only bumped for live-decrypted packets (
decryptedLocally), i.e. mainlyuser:*rows, so those are the badges that matter; - the sidebar preview of a
user:*row resets to "Encrypted — click to decrypt".
Proposed fix
In loadChannels(), capture the previous list, then:
fresh snapshot → mergeApprovedChannels → mergeUserChannels() → merge client state from the previous list → sort → render → reconcile
- A pure helper (like
mergeWsAppendedIntoRest()) carriesunread,userAdded,userLabelby hash, for server rows anduser:*rows, and never resurrects a row the fresh list does not contain (the region filter must keep working). - Newer live activity (
lastActivityMs,lastSender,lastMessage,messageCount, moved together) is decided by a local WS sequence, not by comparing timestamps: the WS path stamps rows with an incrementing counter,loadChannels()samples it when the request starts, and prev's activity is kept only if its stamp is newer. Comparing the browser'sDate.now()with the server'slastActivitydepends on clock skew (review finding on the upstream PR). - The
onApprovedpath then no longer needs its ownmergeUserChannels()call (keep or remove deliberately, with a test).
Acceptance criteria
- With a PSK channel open: region change, show-encrypted toggle and shared-channel approval keep the selection, the messages and the URL.
- My Channels rows and labels survive every refresh.
- Unread and preview on
user:*rows survive a refresh. - A server channel the new snapshot omits (region filter) does not come back.
- A WS update that arrives while the request is in flight wins regardless of browser/server clock skew; a stale client row never overrides a newer snapshot.
- Inputs are not mutated; explicit
unread: 0is preserved. - Unit tests call the real
loadChannels()(existing test hooks) and fail on master; mutation checks for the ordering, the sequence comparison and the user-row merge; a Playwright check of the region-change scenario with a PSK channel open, desktop and mobile width.
Non-goals
No server/API change; no fix for a brand-new WS channel dropped during the very first load (upstream leaves it too).
Activity
test(channels): F1 E2E — seed a real client-decrypted packet for the …
fix(channels): N1 — restart decryption when a remap happens mid-decrypt
fix(channels): R4-1 — removeKey clears every region-scoped decrypt ca…
fix(channels): R4-2 — bound the decrypt cache in characters, not just…
fix(channels): R4-3 — make the N1 pending-load flag per request
test(channels): wait for Web Crypto in the R4-3 S1 test instead of fi…
fix(channels): R4-4 — zero candidates drop the region's decrypt cache…
test(channels): R4-5 — signal-aware E2E process helpers, pin the sort…
test(channels): give the observed-path-hash-size ChannelDecrypt stub …
dborup commentedon Oct 4, 2026
Deferred upstream assessment — 2026-10-04
This existing issue covers the weekly upstream item; no duplicate is needed. Upstream PR 2096 is now merged.
What it does / value: retains client-only channel state when a fresh server channel list arrives—My Channels, labels, unread state and newer live previews—so a refresh does not unexpectedly close an open PSK conversation or reset its sidebar state.
What we already have: open #153 is our adaptation, with fork-specific shared-channel approval and sequence-based handling of concurrent WebSocket updates. #163 / open #166 track follow-ups; #154 separately covers overlapping requests returning in the wrong order. These are existing work, not reasons to copy upstream again.
Why not a blind port: preserve our approved-channel merge and PSK behavior, distinguish client state from channels legitimately excluded by the current region, and avoid using browser/server timestamp comparison as an ordering guarantee. A brand-new WebSocket channel arriving during the first load is a separate case, not automatically solved by this upstream merge.
Later checklist: review #153 and #166 together against current master; test region/encrypted-filter refresh and shared-channel approval with a PSK conversation open; cover unread, preview/count consistency, clock skew and overlapping loads; verify mobile and desktop behavior. Keep the scope boundary with #154 explicit.
Recommendation for later consideration: continue evaluating the existing fork implementation rather than opening a second port. No merge or implementation decision is made here.
Merge origin/master into codex/issue-152-channels-client-state
dborup commentedon Oct 4, 2026
Fixed by #153 (merged as
a2a151fb).loadChannels()now carries client-only state across a channel-list refresh: the PSK selection, My Channels and unread counts. This covers all three triggers: a region change, the "Show encrypted" toggle, and a shared-channel approval. Before the merge, #153 was brought up to date with master, and the review confirmed that none of master's channel changes were reverted.Leftovers:
dborup commentedon Oct 4, 2026
The auto-approve refresh leftover is tracked in #232.