fix(desktop): stop pin-sync pull from reverting the user's own pin click - #75295
fix(desktop): stop pin-sync pull from reverting the user's own pin click#75295hutao562 wants to merge 1 commit into
Conversation
reconcile() ran pullRemotePins() on every trigger, including changes to $pinnedSessionIds itself. On a local pin/unpin click the reconcile fires before the mirroring PATCH is even issued, so every session row still carries the pre-click server value; the pull pass read that stale value back as authoritative and instantly reverted the user's click (pin: row.pinned=false -> unpin; unpin: row.pinned=true -> re-pin). With the backend returning the pinned flag on every row, sidebar pinning could never stick. Gate the pull pass on the reconcile source: session-list refreshes and boot still pull (that is where new server truth arrives), pin-set changes only push. Adds regression tests for both directions against rows that carry the flag.
|
Thanks for the focused regression fix. The premise is confirmed on current main: The source-tagged reconciliation preserves server pulls for boot/session refreshes while making a local click push-only, and the added tests exercise both conflicting initial row values. GitHub comparison reports the PR base is identical to current Automated hermes-sweeper review. |
Duplicate of #74788: both separate local pin-click push handling from session-list remote reconciliation so stale pinned values cannot immediately reverse a user action. |
SummaryThree PRs address the same pull-before-push race in Desktop session pin synchronization: #74638 skips locally changed IDs during the stale pull, #75295 makes local pin-set reconciliation push-only, and #75342 tracks current-process intent separately from restored pins while preserving server authority at boot. Related pull requests
Duplicates#74638, #75295, and #75342 implement competing repairs for the same stale pull-before-push pin-sync race. Discussion also identifies #75295 as a duplicate of #74788, so the duplicate chain is #74638/#75342 → #75295 → potentially #74788, subject to direct diff comparison with #74788. Suggested consolidationKeep #75295 open with a salvage path: use its focused source-based pull gating as the consolidation point, verify the contributor-reported overlap with #74788, add generation-safe overlapping pin→unpin and unpin→pin tests if needed, and port #75342's restart-after-remote-unpin regression. Then close #74638 as a duplicate of #75295 because its guard leaves the documented overlapping-write gap, and close #75342 as a duplicate of #75295 once its boot-authority test is preserved; if direct comparison confirms that #74788 already contains the same viable mechanism, close #75295 through that explicit duplicate chain instead. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
subgraph Dup74638 ["PRs duplicating each other"]
P74638["PR #74638 (open)"]
P75295["PR #75295 (open)"]
P75342["PR #75342 (open)"]
end
class P74638 open
class P75295 open
class P75342 open
class P75295 target
click P74638 "https://github.com/NousResearch/hermes-agent/pull/74638"
click P75295 "https://github.com/NousResearch/hermes-agent/pull/75295"
click P75342 "https://github.com/NousResearch/hermes-agent/pull/75342"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 3 pull requests and 0 issues in this complex. Each diff was read against this issue; Assessment working set: 19 kB of PR diffs, 7 kB of issue/PR text, 3 kB of discussion (6 comments), 0 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
|
Superseded by #80711. Same pin-sync pull-before-push revert; the guard now holds until a page confirms the written value. |
Summary
Sidebar pinning in the Desktop app could never stick against a current backend: clicking Pin (or Unpin) looked like a no-op. The session never moved to the Pinned section, and the pin was silently reverted within milliseconds.
Root cause
watchSessionPins()inapps/desktop/src/store/session-pin-sync.tsrunsreconcile()on every trigger — including changes to$pinnedSessionIdsitself.reconcile()unconditionally starts withpullRemotePins(), which treats server rows as authoritative and drops local pins the server reports as unpinned.But a reconcile triggered by the user's own click fires before the mirroring PATCH has even been issued, so every session row still carries the pre-click value:
row.pinned === false→ "server says it's gone" →unpinSession()reverts the click. The PATCH is never sent.row.pinned === true→pinSession()re-adds it.Backends that omit the
pinnedflag on session rows are unaffected (the pull skipsundefined), which is why the existing tests — all using rows without the flag — passed while the feature was broken in production, where the list endpoint returnspinnedon every row.Verified live: after a pin click, localStorage's
hermes.desktop.pinnedSessionsis written and immediately deleted, no PATCH reaches/api/sessions/{id}, andsessions.pinnedin state.db stays 0.Fix
Gate the pull pass on the reconcile source:
.listen(() => reconcile('pins'))vs$sessions.listen(() => reconcile('sessions')); the push/mirror logic is unchanged, including the in-flightunconfirmedguard for stale list pages.Tests
Two regression tests reproducing the production shape (rows that carry the flag):
keeps a fresh local pin when the row already carries pinned=false— failed before the fix (expected [] to include 'a'), passes afterkeeps a fresh local unpin when the row still carries pinned=trueFull suite:
npx vitest run src/store/session-pin-sync.test.ts→ 13/13 green (11 pre-existing + 2 new), no behavior change for cross-app pin adoption, stale-page races, or legacy backends without the column.