fix(desktop): clear messaging sessions optimistically on archive and delete - #87774
fix(desktop): clear messaging sessions optimistically on archive and delete#87774jackulau wants to merge 3 commits into
Conversation
fix(desktop): clear messaging sessions optimistically on archive and delete
|
3731569 to
11c7a0f
Compare
|
Took the third point, and it was the right catch. Pushed as 3. Missing delete-rollback coverage. Correct, and the gap was worse than "one path untested": delete rolls back through the same Mutation-verified, two independent mutations because they prove different things:
Full file 60 passed, 2. Unconditional prepend losing position. This one I checked and I do not think it is reachable for the messaging slice, so I have left it alone rather than adding a sort I cannot justify. The atom order is not the display order there: If the messaging sections ever stop re-sorting at render, this becomes real, so it is worth knowing it is currently load-bearing. 1. Two helpers encoding the slice decision differently. I read this one as deliberate rather than accidental, and I would rather not unify them. The two helpers are answering different questions:
That is also why the "future case where a row legitimately appears in both slices" cuts the other way for me. A combined Happy to be argued out of this if you would rather have the single helper anyway. |
11c7a0f to
47018c7
Compare
|
The red That test builds its fixture from I have opened #88876 to pin the clock in that test. It is independent of this PR, so this one The three files this PR does change are covered by |
f75ea85 to
1c8d200
Compare
|
Rebased onto What collided
const removedFromMain = $sessions.get().find(...)
const removed = removedFromMain ?? $archivedSessions.get().find(...)That is the same class of bug this PR fixes, for a different slice. The sidebar is not two atoms, it is three, and none of them is a superset of another: recents are fetched with So I unioned rather than picked a side. The lookup now resolves across all three, and all three are evicted: const removedFromLive = findSidebarSession(storedSessionId) // recents ?? messaging
const removed =
removedFromLive ?? $archivedSessions.get().find(session => sessionMatchesStoredId(session, storedSessionId))The one place it cannot be unionedThe rollback. Hence Third commitThat distinction is invisible in the diff and easy to erase later, and the archived path had no test in this file, so I pinned it: a failed delete of an archived row must restore in exactly one place. Passing I would rather that line be guarded by a test than by a comment, given I am the one who rewrote it during a rebase rather than the person who wrote it originally. Verification
If a maintainer would rather keep |
|
Cross-reference for whoever triages this: #87798 by @webtecnica is the same fix for the same issue, opened about six hours after this one on 2026-08-16. My backward duplicate sweep only surfaced it today. Full comparison is on that PR rather than duplicated here. Short version, so nobody has to click: both heads fix the optimistic filter. This one also fixes the lookup in front of it, which is the half that leaks past the UI - for a messaging row Whichever head is preferred, the other should be closed rather than left open - |
…delete The sidebar keeps recents and messaging rows in two disjoint atoms. refreshSessions fetches recents with SIDEBAR_EXCLUDED_SOURCES, which spreads in every messaging source id, and refreshMessagingSessions fetches the inverse into $messagingSessions. archiveSession and removeSession only filtered $sessions, so a Telegram, Discord or Feishu row stayed on screen until the next refresh landed. The lookup in front of that filter had the same blind spot, and it is the more damaging half. Both actions read the row to derive the profile that routes the RPC, the _lineage_root_id the tombstone needs in order to match a compressed row, and the snapshot the rollback restores from. For a messaging row that lookup did not return undefined by accident, it returned undefined always: the delete went out with no profile, the tombstone covered only the stored id, and the failure rollback was unreachable. findSidebarSession reads both slices, and restoreSidebarSession puts a failed mutation back in the slice it came from, so a rollback cannot quietly relocate a messaging row into recents where the next refreshSessions would drop it a second time. Fixes NousResearch#87716
The archive path had rollback coverage in both directions, the delete path had none: only its success-path routing was pinned. Delete rolls back through the same restoreSidebarSession helper but from a different catch, so a regression that dropped or misrouted that call would have left the archive tests green. Two tests, mirroring the archive pair so the routing is pinned in both directions rather than only for the messaging slice. Verified by mutation: * forcing restoreSidebarSession to always write $sessions fails the messaging test of each pair (2 failed, 4 passed). * deleting the restoreSidebarSession(removed) call from the delete catch fails exactly the two new tests and leaves the archive pair green (2 failed, 4 passed). Full file: 60 passed. tsc --noEmit and eslint clean. Refs NousResearch#87716
…slices The rebase onto main had to reconcile this branch with an independent fix that added the archived view's own store to the same lookup / eviction / rollback triple in removeSession. Both changes are the same class of bug for a different slice, so the resolution unions them rather than picking a side: the lookup now resolves across recents, messaging and archived, and all three are evicted. The rollback cannot be unioned the same way. restoreSidebarSession routes a failed mutation back into the LIVE slice it came from and knows nothing about the archived store, which is restored from its own previousArchived snapshot. Running both would put an archived row back into recents as well: it would appear twice until the next refresh, then vanish from the Archived filter it was deleted from. So the live restore is keyed on removedFromLive, and the archived case is left to the snapshot. That distinction is invisible in the diff and easy to erase later, and the archived path had no test here, so pin it: a failed delete of an archived row must restore in exactly one place. Passing restoreSidebarSession the wider `removed` instead fails this test with the row leaked into recents.
1c8d200 to
8e44f0a
Compare
What does this PR do?
Archiving or deleting a messaging-platform session (Telegram, Discord, Feishu, Slack, ...) left the row in the sidebar until the next refresh landed, the 2-4s of stale UI in the report.
The sidebar is two disjoint atoms, not one.
refreshSessionsfetches recents withSIDEBAR_EXCLUDED_SOURCES, which spreads in every id fromMESSAGING_SESSION_SOURCE_IDS, andrefreshMessagingSessionsfetches the inverse into$messagingSessions.archiveSessionandremoveSessionoptimistically filtered$sessionsonly, so for a messaging row the optimistic clear was a no-op.The lookup in front of that filter has the same blind spot, and it is the more damaging half. Both actions start with
and then derive three separate things from it:
removed?.profile, which routes the RPC (deleteSession(id, profile)/setSessionArchived(id, true, profile))removed?._lineage_root_id, which the tombstone needs to match a compressed rowremoveditself, which is the snapshot thecatchrestores fromFor a messaging row that lookup does not return
undefinedby accident, it returnsundefinedalways. So the delete goes out with no profile, the tombstone covers only the stored id, andif (removed) { ... }in the rollback is dead code: a failed archive tears the row out of the UI and never puts it back. Adding the optimistic$messagingSessionsclear on its own would have made that last one worse, since it would start actually removing the row it cannot restore.So the fix is two small helpers in
utils.tsrather than a secondsetSessionscall:findSidebarSession(storedSessionId)reads$sessionsand falls back to$messagingSessions.resolveStoredSessionin the same file already reads all three slices for exactly this reason, so this follows the local precedent.restoreSidebarSession(session)puts a row back in the slice it came from, keyed onisMessagingSource(session.source). Restoring into$sessionsunconditionally would relocate a messaging row into recents, where it does not render and where the nextrefreshSessionswould drop it again: a rollback that looks like it worked and then silently loses the row a second time.Answering the two things I said I would check on the issue
deleteSessionhas the identical omission. Confirmed and fixed here.removeSessionis that path (session-actions-menuand the tile menu both route delete through it), and it had all four symptoms above, not just the visible one.Does the tombstone cover messaging ingestion, or only recents? It covers all of it, so the optimistic clear is safe and no re-add race remains.
dropTombstonedis applied at every ingestion point inuse-session-list-actions.ts: lines 139 and 192 on the messaging paths, 276 and 321 on the recents paths. That was the condition I flagged as the reason a clear alone might not be enough; it does not apply.Related Issue
Fixes #87716
Type of Change
Changes Made
apps/desktop/src/app/session/hooks/use-session-actions/utils.ts: addedfindSidebarSession(both-slice lookup) andrestoreSidebarSession(slice-preserving rollback), with comments explaining why the two atoms are disjoint.apps/desktop/src/app/session/hooks/use-session-actions/index.ts:removeSessionandarchiveSessionnow look the row up withfindSidebarSession, also filter$messagingSessionsoptimistically, and roll back throughrestoreSidebarSession. ThewasSelectedre-lookup inremoveSession'scatchuses the same helper, so a messaging row now restores its usage counters too.apps/desktop/src/app/session/hooks/use-session-actions.test.tsx: 4 regression tests.How to Test
$messagingSessions(Feishu in the report).The 4 new tests cover the same ground:
archiving drops the row from the messaging slice, not only from recentsfails onmainwith the row still in$messagingSessions.deleting routes the RPC through the profile of the row being deletedfails onmainwithdeleteSessioncalled as(id, undefined)instead of(id, 'work'). This is the silent half of the bug, and the reason it is worth a test rather than a comment.rolls a failed archive back into the messaging slice, not into recentspinsrestoreSidebarSession's routing.still rolls a failed archive of a local row back into recentsis the non-regression guard for the desktop/CLI path.Note on verification: I could not run vitest locally in this checkout, so CI is the gate on these.
scripts/check-windows-footguns.py --allpasses (973 files scanned).Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/A