Skip to content

feat(desktop): server-synced session pins with live cross-device sync - #62716

Closed
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:feat/server-synced-session-pins
Closed

feat(desktop): server-synced session pins with live cross-device sync#62716
Kyzcreig wants to merge 1 commit into
NousResearch:mainfrom
ANG-Ventures:feat/server-synced-session-pins

Conversation

@Kyzcreig

@Kyzcreig Kyzcreig commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What

Session pins currently live in localStorage ($pinnedSessionIds persistentAtom) — 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

  • hermes_state: pinned column on sessions (additive migration, DEFAULT 0), set_session_pinned(), exposed as a real JSON boolean in list projections.
  • web_server: PATCH /api/sessions/{id} accepts pinned alongside title/archived; lists return it.
  • gateway: session.pin RPC for socket clients.
  • desktop: $pinnedSessionIds becomes 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).
  • Live propagation: BroadcastChannel only reaches same-machine windows 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 (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).
  • Desktop: typecheck clean (both tsconfig projects); touched-store suites (layout/updates) 29/29.
  • Full UI suite: identical failure set to clean 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.
  • Live E2E on a two-Mac install: pin on machine A appears on machine B within the poll window, both directions, no restart.

Cherry-picked from a fork lineage where this has been running in daily use; contributor credit preserved via rebase-merge.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) comp/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 11, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #60925 (narrow fix — persist pins to state.db so they survive upgrades, for #60918) and issue #60918. This PR is the superset: server-owned pins with live cross-device sync (pin column + PATCH /api/sessions/{id} + session.pin RPC + desktop store migration). Not a duplicate — different scope. Flagging the cluster so a maintainer can pick: #62716 for full cross-device sync vs #60925 for a smaller, easier-to-review persistence-only fix.

@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 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 new set_session_pinned() CTE follows every child beneath a compression-ended parent. The current projection deliberately excludes _branched_from, _delegate_from, and source='tool' at hermes_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 in apps/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.

Comment thread hermes_state.py
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'

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 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.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 11, 2026
@Kyzcreig
Kyzcreig force-pushed the feat/server-synced-session-pins branch from ae8f531 to 259f4c3 Compare July 12, 2026 08:40
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.
@OutThisLife

Copy link
Copy Markdown
Collaborator

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.

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/dashboard Web dashboard / control panel UI (dashboard/, landing) comp/desktop Electron desktop app (apps/desktop/*) comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants