fix(desktop): prune dead session references from renderer storage - #81313
Open
DavidMetcalfe wants to merge 1 commit into
Open
fix(desktop): prune dead session references from renderer storage#81313DavidMetcalfe wants to merge 1 commit into
DavidMetcalfe wants to merge 1 commit into
Conversation
DavidMetcalfe
force-pushed
the
fix/79001-dead-session-prune
branch
from
August 7, 2026 20:38
7d19541 to
28df109
Compare
…usResearch#79001) Pins, composer drafts, and queued prompts persist session ids in localStorage and re-assert/probe them at boot. Sessions removed from state.db (retention purge, manual delete) leave dead ids behind: nothing ever drops them, so every boot re-requests the dead ids and the gateway answers 404 "Session not found" once per stale reference, forever. Add a dead-session prune sweep that runs after the first sidebar list payload (and re-runs, debounced, on later list changes). A stored id is only declared dead after a by-id probe 404s on every known profile — a 404 is profile-scoped, so a single miss never prunes anything, and non-404 probe failures (gateway mid-restart, network) defer the id instead of dropping it. Dead pins are forgotten from pin-sync bookkeeping before unpinning so the reconcile does not re-PATCH the dead id (which would recreate the 404). Fixes NousResearch#79001
DavidMetcalfe
force-pushed
the
fix/79001-dead-session-prune
branch
from
August 7, 2026 20:42
28df109 to
4523544
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #79001 — Desktop boot 404s ("Session not found") for sessions deleted from state.db.
When a session is removed from state.db (retention purge, manual delete), the desktop keeps its id in renderer localStorage: pinned sessions (
hermes.desktop.pinnedSessions), composer drafts (hermes:composer-drafts:v3), and the composer queue (hermes.desktop.composerQueue.v1). The ids survive every boot, and the boot-time probes that do fire — the route-resume chain for the last-open session and the background queue drain (up to 4 attempts per entry) — answer404 {"detail":"Session not found"}for each dead reference. Pins and drafts never probe on their own (a row-less pin is never PATCHed; drafts are pure localStorage text), which is exactly why nothing ever drops them.Root cause
session-pin-sync.tsre-asserts every pin at boot, andpullRemotePins()only consults rows present in the payload — a pin whose row is gone is never dropped (its id sits inpendingindefinitely).Fix
A new dead-session prune sweep (
store/dead-session-prune.ts), started once per app next towatchSessionPins():GET /api/sessions/{id}on the active profile, then each named profile.__new__key), and clears queued prompts.resetDeadSessionPrune()clears the alive cache, cancels a pending sweep, and bumps an epoch that invalidates in-flight probes, so a pre-switch verdict can never be applied to the new backend. A live-alive verdict expires after 10 minutes so a mid-session deletion heals on a later sweep instead of being cached forever.This complements the runtime-session-drop recovery in #81261 rather than overlapping it: that handles the gateway's in-memory session dropping; this handles sessions permanently removed from state.db.
Testing
store/dead-session-prune.test.ts(14 tests): all-profile-404 prunes; session-on-another-profile keeps the pin and caches it alive (no re-probe); alive verdict TTL expiry re-probes; non-404 failure defers and retries; drafts pruned but__new__kept; queue entries pruned only when dead; rows covered by the loaded list are never probed; unknown profile list defers without probing; genuinely empty backend prunes; gateway-switch reset re-probes; in-flight verdicts discarded on mid-sweep reset; watcher sweeps after the first payload; debounce clamps under churn; fresh debounce window after a fired sweep.session-pin-sync.test.tsgains a test thatforgetPinSyncStatesuppresses the re-PATCH of a rowless pin.vitest run --project ui src/store/→ 666 passed;tsc -p . --noEmitclean; eslint clean on the changed files.Notes
Open question: pins are per-app localStorage and intentionally survive gateway switches (the pin-sync re-asserts them against whichever backend is live). On a switch to a backend that lacks the pinned session, this sweep's all-profiles-404 verdict prunes that pin — matching the issue's requested semantics ("a 404 on re-assert/fetch should drop the local entry"), but a user switching between local and remote gateways would find local-backend pins pruned while on the remote backend. Tracking "previously seen alive on the current backend" instead would not cover the primary repro (app closed while the purge happened), which is why the sweep probes ground truth. Happy to gate on a stricter signal if maintainers prefer.
Related (open, unmerged)