Skip to content

fix(desktop): preserve session list on fetch failure instead of dropping to empty - #64159

Open
DavidMetcalfe wants to merge 2 commits into
NousResearch:mainfrom
DavidMetcalfe:fix/session-fetch-silent-failure
Open

fix(desktop): preserve session list on fetch failure instead of dropping to empty#64159
DavidMetcalfe wants to merge 2 commits into
NousResearch:mainfrom
DavidMetcalfe:fix/session-fetch-silent-failure

Conversation

@DavidMetcalfe

Copy link
Copy Markdown
Contributor

Summary

Fixes #64157: refreshSessions() silently drops the session list to empty on transient fetch failures, making the sidebar appear as if all sessions are gone.

What this PR does

Adds a catch block to refreshSessions() in apps/desktop/src/app/session/hooks/use-session-list-actions.ts. Previously the function used try/finally with no catch — when listAllProfileSessions() threw (backend timeout on cold start after update, network error), $sessions stayed at its initial atom value of [] and $sessionsLoading flipped to false. The sidebar rendered "no sessions" with zero error indication.

The catch block preserves the previous session list on transient failure. The spinner still stops (finally), but the sidebar keeps whatever was loaded before the failure instead of dropping to empty.

Notes

  • A follow-up could add an explicit error state atom + retry affordance in the sidebar. This PR is the minimal fix to prevent the silent data-loss appearance.
  • The missing active-profile.json migration (separate issue) compounds this — on first boot after update, the Desktop defaults to the "default" profile, and if that profile's session fetch times out, the user sees an empty sidebar and assumes everything is gone.

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P3 Low — cosmetic, nice to have labels Jul 14, 2026
…ing to empty

refreshSessions() used try/finally with no catch block. When
listAllProfileSessions() threw (backend timeout on cold start after
update, network error), $sessions stayed at [] (initial atom value)
and $sessionsLoading flipped to false — the sidebar rendered "no
sessions" with zero error indication. On first boot after an update
this looks exactly like data loss.

Add a catch block that preserves the previous session list and logs
the error to console for debugging. The spinner still stops (finally),
but the sidebar keeps whatever sessions were loaded before the failure.
@DavidMetcalfe
DavidMetcalfe force-pushed the fix/session-fetch-silent-failure branch from c5a23e4 to 8b572b6 Compare July 14, 2026 05:35

@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 isolating the failure path. The current change needs adjustment before it can deliver the stated UX guarantee.

Problems

  • refreshSessions() already leaves an existing list intact on rejection: setSessions is only reached after a successful result at apps/desktop/src/app/session/hooks/use-session-list-actions.ts:175-182. The added catch does not write session state, while $sessions begins as [] at apps/desktop/src/store/session.ts:215; therefore it cannot preserve a list during the reported cold-start failure.
  • The catch also suppresses the boot failure path. Boot awaits refreshSessions() at apps/desktop/src/app/gateway/hooks/use-gateway-boot.ts:479-485; a rejection currently reaches failDesktopBoot/notifyError at lines 487-492. With this catch, boot completes while the initial list remains empty.

Suggested changes

  • Keep an initial fetch failure observable (for example, an explicit recoverable session-load error) rather than treating it as a successful empty result, while retaining stale rows after a later refresh failure.
  • Add rejection-path coverage for both an already-populated list and the initial empty state.

Automated hermes-sweeper review.

@@ -182,6 +182,13 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg
setSessionsTotal(typeof result.total === 'number' ? result.total : result.sessions.length)
setSessionProfileTotals(result.profile_totals ?? {})
}

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 catch resolves the initial fetch failure without changing $sessions. Since $sessions starts as [], boot then reaches completeDesktopBoot() (use-gateway-boot.ts:479-485) with an empty sidebar instead of the existing boot error path (use-gateway-boot.ts:487-492). Please keep an initial failure observable or provide a recoverable error/retry state rather than swallowing it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @teknium1 — both findings were correct. The original catch preserved $sessions only when there was something to preserve, so cold-start boot ended up with an empty sidebar and no error path because the rejection was swallowed before the existing boot error handler could see it. Pushed a rework at 3f83bfcb4 that:

  • Introduces $sessionLoadError (a typed SessionLoadError atom in store/session.ts) plus a SessionRefreshError class so the boot hook can tell a session-list failure apart from other boot failures.
  • Adds an isBoot?: boolean option to refreshSessions(). The boot caller passes isBoot: true and the catch re-throws as SessionRefreshError, which keeps the existing failDesktopBoot + notifyError path intact. Background callers (sidebar refresh, profile switch, reconnect) don't pass the flag and resolve normally with $sessionLoadError set, so their stale rows stay visible.
  • Clears $sessionLoadError inside the existing success block — a transient success auto-clears the banner without a Retry click.
  • Adds boot.errors.sessionLoadFailed (en/ja/zh/zh-hant) so the toast/overlay says "Couldn't load recent sessions" instead of the generic "Desktop boot failed" copy.
  • 5 rejection-path tests in a new use-session-list-actions.test.tsx: cold-boot re-throws as SessionRefreshError, populated-list refresh failure preserves rows + records the error, success-after-failure clears the atom, non-Error rejection values are handled, and the explicit-isBoot contract is pinned (so future refactors don't start using $sessions.length === 0 as a substitute).

The "couldn't refresh — Retry" affordance in the sidebar UI is intentionally out of scope here — $sessionLoadError is now exposed so a follow-up PR can wire it up without re-touching this state machine. Happy to take that as a sibling if you'd rather have it land together.

Cross-vendor dual review (Flash + GPT-OSS) on the rework landed without BLOCKERs; the consensus was an explicit isBoot flag over a $hasFetchedSessions atom, plus the typed error class and the session-specific i18n key.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 16, 2026
Address maintainer review on NousResearch#64159 (review #4709792830):

The original change added a silent catch to refreshSessions(), but on
cold start $sessions is [] (store/session.ts:217) and setSessions is
only reached inside the success block
(use-session-list-actions.ts:178-182), so the catch preserved nothing.
It also re-threw nothing, which meant the existing boot failure path
at use-gateway-boot.ts:486-492 (failDesktopBoot + notifyError) never
fired — boot silently completed with an empty sidebar and no error UI.

Rework:

* Add $sessionLoadError atom (store/session.ts) so the sidebar can
  surface a recoverable error after a populated-list refresh failure
  without dropping existing rows.
* Add SessionRefreshError class + { isBoot?: boolean } option to
  refreshSessions. The boot caller passes isBoot:true so cold-start
  failures re-throw and reach the existing boot overlay; background
  callers resolve normally with the error recorded.
* clearSessionLoadError() on every successful setSessions write so
  a transient success auto-clears the banner without a Retry click.
* Add boot.errors.sessionLoadFailed i18n key (en/ja/zh/zh-hant) so
  the boot overlay tells the user it was the session fetch, not the
  whole desktop boot. Generic copy is preserved for all other boot
  failures.
* Rejection-path coverage: 5 tests in
  use-session-list-actions.test.tsx covering cold-boot re-throw,
  populated-list preservation, success-clears-error, non-Error
  rejection values, and the explicit-isBoot-flag contract.

Closes the inline review (#4709792830 / comment id 3592083182).
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/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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 session sidebar renders empty on transient fetch failure (missing catch in refreshSessions)

3 participants