fix(desktop): keep a just-finished session visible after switching away - #42285
Merged
Conversation
A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
Contributor
🔎 Lint report:
|
jhjaggars-hermes
pushed a commit
to jhjaggars/hermes-agent
that referenced
this pull request
Jun 8, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
changman
pushed a commit
to changman/hermes-agent
that referenced
this pull request
Jun 10, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
…ay (#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
davidgut1982
pushed a commit
to davidgut1982/hermes-agent
that referenced
this pull request
Jun 17, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
waefrebeorn
pushed a commit
to waefrebeorn/slermes
that referenced
this pull request
Jul 2, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
santhreal
pushed a commit
to santhreal/hermes-agent
that referenced
this pull request
Jul 13, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
donbowman
pushed a commit
to donbowman/hermes-agent
that referenced
this pull request
Jul 13, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
1 task
Gravezzz
pushed a commit
to Gravezzz/hermes-agent
that referenced
this pull request
Jul 21, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
leewenjie
pushed a commit
to leewenjie/hermes-agent
that referenced
this pull request
Aug 7, 2026
…ay (NousResearch#42285) A brand-new session's first turn persists to the SessionDB a beat after the gateway emits message.complete, so a refresh fired in that window gets a listSessions(min_messages=1) page that omits the new row. sessionsToKeep() already shields the *active* chat from this race, but a session you started and then navigated away from is — at the next refresh — neither working, pinned, nor active, so mergeSessionPage() evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts. Track sessions whose turn just settled (a real working->idle transition) in a short, auto-expiring grace window and add them to the merge keep-set. This bridges the persist race for non-active chats without resurrecting deleted rows (mergeSessionPage only revives rows still in the in-memory list, which optimistic delete/archive already drop). Repro: start a new chat, send a message, then click another session before the reply lands — the new session vanishes from the sidebar.
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 the report: "started a new session in the desktop app … after starting the session and then going to a different session, the session that I was in at the start just disappeared." (also a facet of #38989).
Root cause (a persist race, not a missing refresh): a brand-new session's first turn is written to the SessionDB a beat after the gateway emits
message.complete. ArefreshSessions()fired in that window gets alistSessions(min_messages=1)page that omits the new row.sessionsToKeep()already shields the active chat from this race — but a session you started and then navigated away from is, at the next refresh, neither working, pinned, nor active, somergeSessionPage()evicts it. Nothing re-fetches afterward, so it stays gone until the app restarts.This is distinct from #37908 (keep in-flight chats visible while working): here the turn has already finished, and the chat is no longer the active one.
Fix
Track sessions whose turn just settled — a real
working → idletransition insetSessionWorking()— in a short, auto-expiring (30s) grace window, and add those ids to the merge keep-set insessionsToKeep(). This bridges the persistence race for non-active chats.working → idletransition grants grace (updateSessionStatere-assertsfalseon every idle tick — those must not pin an idle chat forever).mergeSessionPage()only revives rows still present in the in-memory list, which optimistic delete/archive already drop.Changes
store/session.ts— grace-window tracking (getRecentlySettledSessionIds()) wired intosetSessionWorking().app/desktop-controller.tsx— include recently-settled ids insessionsToKeep().store/session.test.ts— grace lifecycle coverage (window, idle re-assert no-op, re-busy clears).Validation
vitest run src/store/session.test.ts— 13/13 passingtsc -b— cleaneslinton changed files — 0 errors/warningsRepro
Start a new chat, send a message, then click another session before the reply lands — the new session disappears from the sidebar.