Skip to content

fix(desktop): stop pin/unpin sidebar races against lagging session pages - #75705

Closed
drewTuzson wants to merge 1 commit into
NousResearch:mainfrom
drewTuzson:fix/desktop-unpin-sticky-sidebar-main
Closed

fix(desktop): stop pin/unpin sidebar races against lagging session pages#75705
drewTuzson wants to merge 1 commit into
NousResearch:mainfrom
drewTuzson:fix/desktop-unpin-sticky-sidebar-main

Conversation

@drewTuzson

Copy link
Copy Markdown

Summary

Fixes the Desktop sidebar pin/unpin race where a lagging sessions list page undoes the user click in the same tick.

Root cause

pullRemotePins() treated the current page as authoritative before local pin/unpin intent was pushed. A page still carrying pinned: false stripped brand-new pins; a page still carrying pinned: true resurrected just-unpinned chats. The PATCH never stuck.

Fix

  • Push local pin and unpin intent first (with sticky guards until backend confirms).
  • Only then pull remote truth for ids with no opposing local intent.
  • Regression coverage for pin, unpin, boot re-assert, and lagging-page cases.

Test plan

  • cd apps/desktop && npm test -- --run src/store/session-pin-sync.test.ts → 14/14 pass
  • Manual: pin a recents chat → stays in PINNED across list refresh
  • Manual: unpin a pinned chat → stays out of PINNED across list refresh
  • Manual: quit/relaunch Desktop → pin state survives

Related open reports/PRs: #75342, #74638, #75295

Keep local pin intent sticky until the backend confirms it. Push both pin
and unpin writes before pull-adoption so a stale sessions page cannot
strip a brand-new pin or resurrect a just-unpinned chat in the same tick.
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state needs-decision Awaiting maintainer decision before any implementation labels Jul 31, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to the Desktop pin-sync repair cluster (#74788, #74444, #74418, #74638, #75295, #75342). This patch uses sticky post-ack intent plus lineage aliases; the alternatives differ in reachable reconciliation semantics, so it is not a duplicate.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the real stale-page race: current main pulls remote rows before it pushes local intent in apps/desktop/src/store/session-pin-sync.ts:102.

Problems

  • apps/desktop/src/store/session-pin-sync.ts:314 calls unpinSession(pinId, row.id), but unpinSession accepts one string at apps/desktop/src/store/layout.ts:379; this does not type-check.
  • apps/desktop/src/store/session-pin-sync.ts:190 drops an unpin if the preceding pin PATCH is still pending. That path neither records unpin intent nor schedules it after the pending write settles, so a rapid pin → unpin can leave the original pinned: true write as the only backend mutation.

Suggested changes

  • Make lineage alias removal use the actual unpinSession API.
  • Queue or serialize the latest per-id intent, and add a deferred-Promise pin → unpin test that resolves the earlier pin request last.

Automated hermes-sweeper review.

}

for (const id of toUnpin) {
if (unconfirmed.has(id)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This skips a just-requested unpin while the preceding pin PATCH is pending, but does not record or queue the unpin for after that request settles. A rapid pin → unpin can therefore send only pinned: true; retain the latest desired value and issue it once the in-flight request completes.

unpinSession(local.has(pinId) ? pinId : row.id)
// Drop every alias that names this conversation (legacy live-id pins and
// durable lineage-root pins both count).
unpinSession(pinId, row.id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unpinSession currently accepts one string argument (apps/desktop/src/store/layout.ts:379), so this second string argument fails type-checking. Remove both aliases through an API-compatible operation.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 31, 2026
@OutThisLife

Copy link
Copy Markdown
Collaborator

Superseded by #80711. Pin/unpin no longer flips back when a stale list page lands after the PATCH ack — the guard holds until a page confirms the written value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/desktop Electron desktop app (apps/desktop/*) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants