fix(desktop): stop re-persisting an exhausted session after cleanup (#98467)

Salvage of PR #98475 by kokhlo, rebased onto current main (deps array
re-merged after display.resume_last_session / disk-plugins landed).

The cleanup effect in useDesktopIntegrations drops the remembered
navigation once a bounded resume retry exhausts, but the persistence
effect re-runs on ordinary session-list refreshes with no idea the
session is stranded — it writes the dead routed id straight back into
hermes.desktop.lastSessionId/lastRoute, so every relaunch reopens the
resume-error screen until the user manually hits New chat.

Treat resumeExhaustedSessionId as a write barrier in the persistence
effect: skip the setRememberedSessionId/setRememberedRoute writes while
the routed id is the exhausted one, and add the latch to the effect's
deps. Regression test covers the routed-at-the-dead-session shape plus
a later session-list refresh that re-runs only the persistence effect.

Closes #98467.
This commit is contained in:
Konstantin Khlopkov
2026-08-30 09:45:00 +00:00
committed by brooklyn!
parent 5c3172005d
commit aaaf924a5c
2 changed files with 49 additions and 1 deletions

View File

@@ -321,6 +321,46 @@ describe('useDesktopIntegrations', () => {
})
})
describe('resume-exhausted write barrier (#98467)', () => {
it('does not re-persist an exhausted session on a session-list refresh', () => {
window.localStorage.setItem('hermes.desktop.lastRoute.profile.default', '/exhausted-session')
window.localStorage.setItem('hermes.desktop.lastSessionId.profile.default', 'exhausted-session')
const sessions = [session({ id: 'exhausted-session', profile: 'default' })]
const result = render({ profileReady: true, sessions })
// The cleanup effect drops the remembered exhausted session...
result.rerender({
activeProfile: 'default',
locationPathname: '/exhausted-session',
profileReady: true,
resumeExhaustedSessionId: 'exhausted-session',
resumeLastSession: true,
routedSessionId: 'exhausted-session',
sessions
})
// ...and a routine session-list refresh (same sessions, ordinary re-render)
// must not write the dead id back into remembered navigation.
result.rerender({
activeProfile: 'default',
locationPathname: '/exhausted-session',
profileReady: true,
resumeExhaustedSessionId: 'exhausted-session',
resumeLastSession: true,
routedSessionId: 'exhausted-session',
sessions: [...sessions, session({ id: 'other-session', profile: 'default' })]
})
expect(window.localStorage.getItem('hermes.desktop.lastSessionId.profile.default')).toBeNull()
expect(window.localStorage.getItem('hermes.desktop.lastRoute.profile.default')).toBeNull()
>>>>>>> a771a14a49 (fix(desktop): stop re-persisting an exhausted session after cleanup (#98467))
})
})
describe('ownership validation', () => {
it('refuses to restore a session route owned by another profile', () => {
window.localStorage.setItem('hermes.desktop.lastRoute.profile.default', '/ai-session')

View File

@@ -239,7 +239,14 @@ export function useDesktopIntegrations({
// non-overlay route (a page like /skills, or a session route) per profile.
// Session-shaped routes require an explicit matching owner; unresolved and
// wrong-profile rows must not replace known-safe navigation.
if (routedSessionId && sessionBelongsToProfile(sessions, routedSessionId, activeProfile)) {
// The resume-exhausted session must not be written back into remembered
// navigation: the cleanup effect above drops it once, but this
// persistence effect re-runs on every session-list refresh while its
// deps are unchanged — without the barrier the dead id outlives every
// restart and the window boots into the resume-error screen each time.
const exhausted = routedSessionId !== null && routedSessionId === resumeExhaustedSessionId
if (routedSessionId && !exhausted && sessionBelongsToProfile(sessions, routedSessionId, activeProfile)) {
// A delegate child (source='subagent') is never itself a rememberable
// destination: it is invisible in the sidebar, so a restart would resume
// an orphan chat while the sidebar highlights its parent (#56983).
@@ -266,6 +273,7 @@ export function useDesktopIntegrations({
locationPathname,
navigate,
profileReady,
resumeExhaustedSessionId,
resumeLastSession,
routedSessionId,
sessions