feat(desktop): server-synced session pins with live cross-device sync - #62716
feat(desktop): server-synced session pins with live cross-device sync#62716Kyzcreig wants to merge 1 commit into
Conversation
Related: #60925 (narrow fix — persist pins to |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for carrying the server-owned pin design through the database, REST, RPC, and desktop layers. The local-only premise is real: current main stores pins only in apps/desktop/src/store/layout.ts:69,294-309.
Problems
hermes_state.py's newset_session_pinned()CTE follows every child beneath a compression-ended parent. The current projection deliberately excludes_branched_from,_delegate_from, andsource='tool'athermes_state.py:3179-3182; the new CTE does not. Pinning a compressed root can therefore change pin state for an independent branch or delegate child.- The legacy migration runs from the bounded recents response, then calls
setSessionPinned()for IDs that may not be loaded. Those calls lack profile ownership, but mutations are explicitly profile-routed inapps/desktop/src/hermes.ts:259-269. A legacy pin outside the first page or on another profile can be sent to the wrong backend and remain hidden after the server-capability switch.
Suggested changes
- Reuse the existing compression-child exclusions in the pin lineage query and add branch/delegate isolation coverage.
- Resolve legacy pin ownership across profiles/slices before clearing localStorage; retain local membership until every migration succeeds.
Automated hermes-sweeper review.
| FROM ancestors a | ||
| JOIN sessions child ON child.id = a.id | ||
| JOIN sessions parent ON parent.id = child.parent_session_id | ||
| WHERE parent.end_reason = 'compression' |
There was a problem hiding this comment.
This recursion treats every child of a compression-ended parent as part of the same logical conversation. Please apply the same _branched_from, _delegate_from, and source != 'tool' exclusions used by the current projection CTE at hermes_state.py:3179-3182; otherwise pinning a compressed root also pins independent branch/delegate children.
ae8f531 to
259f4c3
Compare
Session pins currently live in localStorage ($pinnedSessionIds persistentAtom)
— each install keeps its own list, so pins don't follow the user across
machines or survive a profile move.
This makes pins server-owned and syncs them live across devices:
- hermes_state: 'pinned' column on sessions (additive migration, default 0),
set_session_pinned(), exposed through the session list projections as a real
JSON boolean.
- web_server: PATCH /api/sessions/{id} accepts 'pinned' alongside
title/archived; session lists return it.
- gateway: session.pin RPC for socket clients.
- desktop: $pinnedSessionIds becomes a server-derived computed over session
rows (pins keyed on the compression-stable lineage-root id); localStorage
pins are migrated to the server once on first load of a list that carries
the pinned field. Drag-reorder of the pinned section is preserved via a
per-device order atom ($sidebarPinnedOrderIds) layered over the server-
synced membership SET — membership syncs, visual order stays local.
- live propagation: BroadcastChannel only reaches windows on the same machine
and session.info events route to the owning transport only, so a pin on
machine A never reached an already-open app on machine B (only visible
after restart). The sidebar now re-pulls the list on window focus + a 30s
focused poll (mirrors the existing poll-based livesync pattern; no new
RPCs) — pin/unpin/title/create propagate across devices within seconds.
Verified on a two-Mac install: pin on machine A appears on machine B without
restart, both directions.
259f4c3 to
c9c6a67
Compare
|
Superseded by #74234, which makes pins server-owned so they survive paging and sync between apps. Your work is carried in it and you're credited as a co-author — thank you. |
What
Session pins currently live in localStorage (
$pinnedSessionIdspersistentAtom) — each install keeps its own list, so pins don't follow the user across machines and are lost with browser data. This makes pins server-owned and syncs them live across devices.Changes
pinnedcolumn onsessions(additive migration,DEFAULT 0),set_session_pinned(), exposed as a real JSON boolean in list projections.PATCH /api/sessions/{id}acceptspinnedalongsidetitle/archived; lists return it.session.pinRPC for socket clients.$pinnedSessionIdsbecomes a server-derived computed (pins keyed on the compression-stable lineage-root id, so auto-compression id rotation doesn't evaporate pins); one-shot localStorage→server migration for existing pins; drag-reorder preserved via a per-device order atom layered over the server-synced membership set (membership syncs, visual order stays local — this was a real regression the first time we built it, caught and covered by tests).session.infoevents route to the owning transport only, so a pin on machine A never reached an already-open app on machine B (visible only after restart). The sidebar now re-pulls on window focus + a 30s focused poll (mirrors the existing poll-based livesync pattern; no new RPCs).Verification
tests/test_hermes_state.py+tests/test_tui_gateway_server.py: 673 pass on this base (incl. new pin coverage: migration, setter, RPC, list projection).origin/main(the ambient electron-suite red that test(desktop): fix ambient failing electron/vitest suites (extensionless TS imports) #62398 fixes) — this diff adds zero failures.Cherry-picked from a fork lineage where this has been running in daily use; contributor credit preserved via rebase-merge.