Skip to content

fix(desktop): latch a resume that paints nothing when the list row is stale - #124789

Merged
austinpickett merged 1 commit into
NousResearch:mainfrom
austinpickett:bb/session-resume-transcript
Sep 27, 2026
Merged

austinpickett merged 1 commit into
NousResearch:mainfrom
austinpickett:bb/session-resume-transcript

Conversation

@austinpickett

@austinpickett austinpickett commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes one root cause behind a session-resume flow that can leave the primary thread blank with no way out.

resumeSession's cold path computes the final transcript and, when it comes up empty, arms $resumeFailedSessionId so use-route-resume re-attempts the resume (bounded backoff, then an explicit error + manual Retry). That latch armed only when the cached sessions-list row reported message_count > 0:

if (sessionShouldHaveTranscript(stored) && preferredMessages.length === 0) {

stored comes from $sessions / resolveStoredSession — it is a cache of backend truth, and it lags exactly the two flows that report a vanished thread:

In both cases a resume that paints nothing falls past the latch, then sets activeSessionId to the resumed runtime and publishes an empty message set. The thread is not "loading" (routedSessionIsLoading returns false once activeSessionId is set and the row has no known history), so the UI shows an empty chat with no error and no retry — history looks lost even though the session is alive.

The resume RPC is the authoritative source and always reports the stored transcript size — tui_gateway/methods_session.py:814 fills message_count from the stored size even when messages_omitted is set for Desktop's cold resume. This PR treats it (and a non-empty REST page) as the other rungs of the same ladder:

const saidToHaveTranscript =
  sessionShouldHaveTranscript(stored) ||
  (resumed.message_count || 0) > 0 ||
  Boolean(prefetchedResult?.messages.length)

if (saidToHaveTranscript && preferredMessages.length === 0) {

A resume that paints nothing now arms the retry latch whenever any source says the session has a transcript, so the window self-heals through use-route-resume's bounded retry and surfaces an explicit error + manual Retry once exhausted, instead of going silently blank. The provisional cached-tail rollback inside the branch is unchanged.

Common root cause, one PR

Both reports are the same flow (use-session-actions/index.ts) where a resume that ends empty leaves the user on a blank thread; the single widened precondition covers both. No second code path was needed.

Parts of each report NOT covered here (deliberately)

Behavior

  • Before: a cold resume that paints nothing while the sessions-list row is stale leaves an empty thread bound to a live runtime — no retry, no error.
  • After: the same resume arms the retry latch; use-route-resume re-attempts with backoff and, if it stays stranded, the chat shows the explicit error + Retry state.

Acceptance coverage

Two regression tests in apps/desktop/src/app/session/hooks/use-session-actions.test.tsx (both fail on pre-fix source, pass with it):

Validation (real output)

$ cd apps/desktop && npm run typecheck
> tsc -p . --noEmit && tsc -p tsconfig.electron.json --noEmit && tsc -p tsconfig.e2e.json --noEmit && ...
(exit 0, no diagnostics)
$ npx vitest run src/app/session/hooks/use-session-actions.test.tsx
 Test Files  1 passed (1)
      Tests  119 passed (119)
$ npx vitest run src/app/session/hooks/use-route-resume.test.tsx \
    src/app/chat/thread-loading.test.ts src/app/session/hooks/use-session-actions/utils.test.ts
 Test Files  3 passed (3)
      Tests  153 passed (153)
$ npx eslint src/app/session/hooks/use-session-actions/index.ts \
    src/app/session/hooks/use-session-actions.test.tsx
(exit 0, clean)

Proving the tests are the fix (source file reverted, tests kept):

$ git stash push -- apps/desktop/src/app/session/hooks/use-session-actions/index.ts
$ npx vitest run src/app/session/hooks/use-session-actions.test.tsx -t "arms the failure latch when"
 ❯ ... > arms the failure latch when a stale list row hides history the resume RPC still reports
   AssertionError: expected null to be 'stored-1'
 ❯ ... > arms the failure latch when a wake warm-resume bail falls to an empty cold resume
   AssertionError: expected null to be 'stored-1'
 Test Files  1 failed (1)
      Tests  2 failed | 1 passed | 116 skipped (119)

No macOS hardware was available to physically exercise sleep/wake; the tests drive the exact code path (warm bind → activate 404 → cold resume painting nothing) and the assertions are on the observable latch/active-session state, not on internal calls.

Reused work

Fixes #83154

hermes-agent #82806 is not closed by this PR. Its original mechanism — the warm-path empty-REST
wipe — is already fixed on main (PR #82843's persisted.messages.length || !activatedMessages.length
guard, credited above); this PR does not re-touch it. What it adds is the failure latch for the
sleep/wake warm-bail flow, i.e. the residual that left the thread blank with no retry. Whether that
fully resolves #82806 is a maintainer call — leaving it open until someone confirms on a build with
this change.

🤖 AI-assisted (Hermes Agent); verification above is real command output.

… stale

The resume-failure latch ("this window is stranded, retry it") armed only
when the cached sessions-list row reported message_count > 0. That row is a
cache of backend truth and lags the two flows that report a vanished thread:
after a sleep/wake reconnect the list can still carry the respawned
backend's session at 0 rows, and a context-compression tip can show 0 rows
while the stored transcript is intact. Either way the cold resume painted an
empty thread with an ACTIVE runtime and no latch at all — no auto-retry, no
error surface, just a silently blank chat that looks like lost history.

The resume RPC is authoritative and always reports the stored transcript
size (tui_gateway fills message_count from state.db even when
messages_omitted), so treat it — and a non-empty REST page — as the other
rungs of the same ladder. A resume that paints nothing now arms the retry
latch whenever ANY source says the session has a transcript, so the window
recovers through use-route-resume's bounded retry (and surfaces an explicit
error + manual Retry once exhausted) instead of going silently blank.

Fixes NousResearch#82806
Fixes NousResearch#83154
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) area/sessions Session lifecycle, resume, persistence, history sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state duplicate This issue or pull request already exists labels Sep 27, 2026
@alt-glitch

Copy link
Copy Markdown

This was generated by AI during triage.

Duplicate of #83802: its open broader resume repair already uses response message_count to arm the empty-transcript retry latch when sidebar state is stale or absent.

@austinpickett
austinpickett merged commit f454288 into NousResearch:main Sep 27, 2026
40 checks passed
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/*) duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

2 participants