Skip to content

fix(desktop): keep a just-finished session visible after switching away - #42285

Merged
OutThisLife merged 1 commit into
mainfrom
fix/desktop-session-vanishes-after-switch
Jun 8, 2026
Merged

fix(desktop): keep a just-finished session visible after switching away#42285
OutThisLife merged 1 commit into
mainfrom
fix/desktop-session-vanishes-after-switch

Conversation

@OutThisLife

Copy link
Copy Markdown
Collaborator

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. A refreshSessions() 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.

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 → idle transition in setSessionWorking() — in a short, auto-expiring (30s) grace window, and add those ids to the merge keep-set in sessionsToKeep(). This bridges the persistence race for non-active chats.

  • Only a genuine working → idle transition grants grace (updateSessionState re-asserts false on every idle tick — those must not pin an idle chat forever).
  • Going busy again clears the grace entry.
  • Entries auto-expire on read, so the set stays bounded with no timer.
  • Cannot resurrect a deleted/archived session: 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 into setSessionWorking().
  • app/desktop-controller.tsx — include recently-settled ids in sessionsToKeep().
  • store/session.test.ts — grace lifecycle coverage (window, idle re-assert no-op, re-busy clears).

Validation

Before After
Start chat → send → switch away before reply lands new session vanishes from sidebar stays visible; real row supersedes it on next refresh
Idle chat (no turn) unaffected unaffected (no grace granted)
Deleted/archived chat stays gone stays gone (not revived)
  • vitest run src/store/session.test.ts — 13/13 passing
  • tsc -b — clean
  • eslint on changed files — 0 errors/warnings

Repro

Start a new chat, send a message, then click another session before the reply lands — the new session disappears from the sidebar.

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.
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: fix/desktop-session-vanishes-after-switch vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10477 on HEAD, 10477 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5495 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@OutThisLife
OutThisLife merged commit 395ed91 into main Jun 8, 2026
20 checks passed
@OutThisLife
OutThisLife deleted the fix/desktop-session-vanishes-after-switch branch June 8, 2026 17:32
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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant