Skip to content

fix(desktop): prune dead session references from renderer storage - #81313

Open
DavidMetcalfe wants to merge 1 commit into
NousResearch:mainfrom
DavidMetcalfe:fix/79001-dead-session-prune
Open

fix(desktop): prune dead session references from renderer storage#81313
DavidMetcalfe wants to merge 1 commit into
NousResearch:mainfrom
DavidMetcalfe:fix/79001-dead-session-prune

Conversation

@DavidMetcalfe

@DavidMetcalfe DavidMetcalfe commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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) — answer 404 {"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.ts re-asserts every pin at boot, and pullRemotePins() only consults rows present in the payload — a pin whose row is gone is never dropped (its id sits in pending indefinitely).
  • Drafts and queue entries are keyed by session id in localStorage and are never validated against the backend.

Fix

A new dead-session prune sweep (store/dead-session-prune.ts), started once per app next to watchSessionPins():

  • Runs after the first sidebar list payload, then re-runs debounced on list changes — a purge while the app is running heals within one pass.
  • Probes every stored id that no loaded row covers: GET /api/sessions/{id} on the active profile, then each named profile.
  • Declares an id dead only when the probe 404s on every known profile. A 404 is profile-scoped ("not on this profile's state.db") — a single miss never prunes anything, and non-404 probe failures (gateway mid-restart, network) defer the id instead of dropping it.
  • Drops dead references: unpins (forgetting pin-sync bookkeeping first so the reconcile doesn't re-PATCH the dead id — which would recreate the 404), discards the draft (never the __new__ key), and clears queued prompts.
  • Verdicts are guarded against gateway switches: 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

  • New 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.ts gains a test that forgetPinSyncState suppresses the re-PATCH of a rowless pin.
  • vitest run --project ui src/store/ → 666 passed; tsc -p . --noEmit clean; 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)

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/desktop Electron desktop app (apps/desktop/*) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Aug 7, 2026
@DavidMetcalfe
DavidMetcalfe force-pushed the fix/79001-dead-session-prune branch from 7d19541 to 28df109 Compare August 7, 2026 20:38
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Desktop boot 404s ("Session not found") for sessions deleted from state.db — renderer storage never drops dead IDs

2 participants