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:
committed by
brooklyn!
parent
5c3172005d
commit
aaaf924a5c
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user