Skip to content

Pins are server-owned, so they survive paging and follow you between apps - #74234

Merged
OutThisLife merged 3 commits into
mainfrom
bb/session-pins-server-owned
Jul 29, 2026
Merged

Pins are server-owned, so they survive paging and follow you between apps#74234
OutThisLife merged 3 commits into
mainfrom
bb/session-pins-server-owned

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

A pin means "keep this reachable." Pins were stored as localStorage strings, which made that promise unenforceable in two directions: the sidebar could only resolve a pin against the rows it happened to have loaded, and a second app on the same gateway had its own separate copy. Ten open PRs are circling one or the other half of that.

The sessions.pinned column already exists and the desktop already mirrors pins into it. What was missing is that nothing ever read it back. This makes the server row authoritative: the list endpoints back-fill pinned conversations past their LIMIT, so a pinned row is always in the page, and the desktop reconciles against it in both directions.

Once the back-fill is in place the read-back needs no new endpoint — the rows the sidebar already fetches carry the flag.

What changes

include_pinned on list_sessions_rich — one bounded extra query for pinned rows the window missed, reusing the page's own WHERE clause. An archived or filtered-out conversation stays out; a pin is not a filter bypass. It runs before compression projection, so a back-filled root surfaces under its live tip like any other row.

The three list endpoints request the back-fill and expose pinned as a JSON boolean. Both merge paths re-window after sorting, so pinned rows are explicitly kept through the cap rather than re-dropped on recency, and the sidebar's "load more" signal discounts them so they can't fake a full page.

pullRemotePins adopts pins made elsewhere and drops local pins the server has released, keyed on the durable lineage root. A write in flight is guarded for the lifetime of its own request, so a list response that predates the PATCH can't silently undo the pin the user just made. A runtime without the flag sends no pinned, which is treated as no opinion.

Verification

Reproduced the fail state first — pin the oldest of 30 sessions, request a 10-row page, confirm it is absent — then confirmed the flag brings it back without disturbing the page. Covered: filters still apply to a back-filled pin, no duplicate when the pin is already on the page, compression roots project to their tip, cross-app adopt/drop, and the stale-page race.

tests/test_hermes_state.py 483 passed. Desktop store + sidebar/session suites 1040 passed. tsc --noEmit clean.

Supersedes

Same root cause, one fix:

Closes #72948

Co-authored-by: Kyzcreig 9063726+Kyzcreig@users.noreply.github.com
Co-authored-by: webtecnica 75556242+webtecnica@users.noreply.github.com
Co-authored-by: hrnbld 260600092+hrnbld@users.noreply.github.com
Co-authored-by: aman-merchant 274313970+aman-merchant@users.noreply.github.com
Co-authored-by: eason2026 209090628+eason2026@users.noreply.github.com
Co-authored-by: konsisumer 11262660+konsisumer@users.noreply.github.com
Co-authored-by: rerdi92 76791321+rerdi92@users.noreply.github.com
Co-authored-by: liuhao1024 11816344+liuhao1024@users.noreply.github.com
Co-authored-by: Tamaz-sujashvili 56168197+Tamaz-sujashvili@users.noreply.github.com
Co-authored-by: ferminquant 14808645+ferminquant@users.noreply.github.com

OutThisLife and others added 3 commits July 29, 2026 12:00
`list_sessions_rich` returns one recency-ordered window, so a pinned
conversation that hadn't been touched in a while simply wasn't in the
payload. The desktop's Pinned section resolves pins against the loaded
rows, so the pin rendered as nothing until something dragged the row
back onto the page.

A pin is a "this must always be reachable" statement, which makes
falling off the page a bug rather than a paging outcome. `include_pinned`
adds one bounded query for the rows carrying `pinned = 1` that the
window missed, reusing the page's own WHERE clause — an archived or
filtered-out conversation stays out, and a pin is never a filter bypass.
It runs before compression projection, so a back-filled root surfaces
under its live tip exactly like a row that made the page on its own.

Co-authored-by: hrnbld <260600092+hrnbld@users.noreply.github.com>
Co-authored-by: liuhao1024 <11816344+liuhao1024@users.noreply.github.com>
Co-authored-by: Tamaz-sujashvili <56168197+Tamaz-sujashvili@users.noreply.github.com>
Co-authored-by: ferminquant <14808645+ferminquant@users.noreply.github.com>
The three list endpoints (`/api/sessions`, `/api/profiles/sessions`, and
the batched sidebar route) now request the pinned back-fill and expose
`pinned` as a real JSON boolean alongside `archived`.

Both merge paths re-window rows after sorting, which would have thrown
away exactly what the back-fill fetched, so pinned rows survive the cap.
The sidebar's "load more" signal discounts them — they arrive past the
LIMIT by design and would otherwise fake a full page on a short list.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Co-authored-by: konsisumer <11262660+konsisumer@users.noreply.github.com>
Co-authored-by: eason2026 <209090628+eason2026@users.noreply.github.com>
The pin bridge only ever pushed: localStorage to the backend, never
back. Two apps on the same gateway each kept their own localStorage, so
a pin made on the Mac never appeared on the Windows app.

Now that a pinned row is guaranteed to be in the page, its absence says
nothing about its pin state — which makes the server row authoritative.
`pullRemotePins` adopts pins this app hasn't seen and drops local pins
the server says are gone, keying on the durable lineage root so a pin
survives compression tip rotation. Adopted pins are recorded as already
mirrored rather than echoed back as a redundant write.

A list request in flight when we PATCH still carries the old value, so
honouring it would silently undo the pin the user just made. Writes are
guarded for the lifetime of their own request — no timers, no wall-clock
windows. A runtime predating the flag sends no `pinned` at all; that's
treated as no opinion and leaves the local set alone.

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Co-authored-by: aman-merchant <274313970+aman-merchant@users.noreply.github.com>
Co-authored-by: rerdi92 <76791321+rerdi92@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on 8ce8b70

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 visual diff.

inline evidence is publishing...

@OutThisLife
OutThisLife merged commit abd9edb into main Jul 29, 2026
54 checks passed
@OutThisLife
OutThisLife deleted the bb/session-pins-server-owned branch July 29, 2026 17:31
@andrejsrna

Copy link
Copy Markdown

nice one ty!

randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…ns-server-owned

Pins are server-owned, so they survive paging and follow you between apps
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…ns-server-owned

Pins are server-owned, so they survive paging and follow you between apps
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/*) P2 Medium — degraded but workaround exists 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.

[Feature]: Sync Pinned Sessions Amid Multiple GUI Apps (Mac/Windows)

3 participants