Skip to content

fix(desktop): close cross-session leak windows in composer + session refs - #70986

Merged
teknium1 merged 2 commits into
mainfrom
fix/desktop-cross-session-leak
Jul 24, 2026
Merged

teknium1 merged 2 commits into
mainfrom
fix/desktop-cross-session-leak

Conversation

@teknium1

Copy link
Copy Markdown
Collaborator

Summary

Desktop chat tabs no longer mix messages across sessions — the two passive-effect race windows are closed and submit now enforces composer/session coherence.

Changes

Validation

141 tests green across touched suites; npm run typecheck clean; eslint 0 errors.

Fixes #59305
Fixes #70609

Infographic

desktop-cross-session-leak

JoaoMarcos44 and others added 2 commits July 24, 2026 13:21
…refs (#59305)

Two React passive-effect timing bugs let a session switch land in the wrong
chat: activeSessionIdRef/selectedStoredSessionIdRef (use-session-state-cache)
and the composer's attachment-scope swap (use-composer-draft) both mirrored
their source props via useEffect, which fires one commit AFTER the new
session's view has already painted — a synchronous read/submit in that window
observed the outgoing session's ids/attachments.

- use-session-state-cache.ts: mirror the session refs synchronously during
  render instead of a useEffect, guarded to fire only when the prop itself
  changed (not unconditionally) so an imperative pin from submit.ts /
  use-session-actions (e.g. a freshly resumed runtime id, intentionally not
  synced to the source atom) survives an unrelated re-render.
- use-composer-draft.ts: the per-thread attachment-scope-swap effect is now a
  useLayoutEffect, closing the window before paint.
- submit.ts / session-context-drift.ts: add a 3rd drift prong comparing the
  composer's loaded scope (SubmitTextOptions.composerScope) against the
  submit target, resolved into the same lineage-root domain
  (resolveComposerSessionKey) the composer itself uses — comparing against
  the raw tip id would false-positive-abort every submit into any session
  that has ever auto-compressed.
- routes.ts / chat/index.tsx: the primary composer's durable scope key now
  prefers the route over a possibly-stale store selection
  (primaryRouteSelectedSessionId).
- use-composer-draft.ts: redacted [composer-rehydrate] diagnostic log
  (counts/kinds/scope only, never raw refs) for future reports in this class.
- chat-runtime.ts: normalize attachment id values (url/path) before hashing
  so a re-attach with a trailing slash or backslash path dedupes correctly.

16 files, 286 tests across the touched/dependent suites (17 files) green,
including new regression coverage for each fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
….test.tsx

CI's check:lint failed on two perfectionist rule violations introduced by the
new test file: type import ordering and missing blank line between the
parent-relative and same-directory import groups. No behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 80c21cc

all good!

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) P1 High — major feature broken, no workaround sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 24, 2026
@teknium1
teknium1 merged commit 66cc007 into main Jul 24, 2026
35 checks passed
@teknium1
teknium1 deleted the fix/desktop-cross-session-leak branch July 24, 2026 23:04
teknium1 pushed a commit that referenced this pull request Jul 25, 2026
…e submit pipeline (#64327)

From PR #64327 (@Kenmege), whose target-aware drift predicate (compare route
tokens by their routed chat target; ignore selection null-resets, search/hash
churn, and background active-ref retargets; never count a move onto the
submit's own target) already reached main via the #69578 salvage
(1bdd478, Co-authored-by Kennedy Umege) and gained the #70986 composerScope
prong. What main covered only at the unit level (session-context-drift.test.ts)
is here pinned at the pipeline level, both directions:

- a send from a second chat rides out simultaneous programmatic churn
  (selection null-reset from a gateway/profile reconnect, active-ref retarget
  from a background event, search/hash-only route change from an overlay)
  mid-session.resume and still reaches prompt.submit;
- a genuine user switch (selection AND route moving to another real chat)
  mid-submit still aborts before prompt.submit.

Part of the #63078 fix branch.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…e submit pipeline (NousResearch#64327)

From PR NousResearch#64327 (@Kenmege), whose target-aware drift predicate (compare route
tokens by their routed chat target; ignore selection null-resets, search/hash
churn, and background active-ref retargets; never count a move onto the
submit's own target) already reached main via the NousResearch#69578 salvage
(b21b39f, Co-authored-by Kennedy Umege) and gained the NousResearch#70986 composerScope
prong. What main covered only at the unit level (session-context-drift.test.ts)
is here pinned at the pipeline level, both directions:

- a send from a second chat rides out simultaneous programmatic churn
  (selection null-reset from a gateway/profile reconnect, active-ref retarget
  from a background event, search/hash-only route change from an overlay)
  mid-session.resume and still reaches prompt.submit;
- a genuine user switch (selection AND route moving to another real chat)
  mid-submit still aborts before prompt.submit.

Part of the NousResearch#63078 fix branch.
prmartinow pushed a commit to prmartinow/hermes-agent that referenced this pull request Aug 26, 2026
…e submit pipeline (NousResearch#64327)

From PR NousResearch#64327 (@Kenmege), whose target-aware drift predicate (compare route
tokens by their routed chat target; ignore selection null-resets, search/hash
churn, and background active-ref retargets; never count a move onto the
submit's own target) already reached main via the NousResearch#69578 salvage
(80145d0, Co-authored-by Kennedy Umege) and gained the NousResearch#70986 composerScope
prong. What main covered only at the unit level (session-context-drift.test.ts)
is here pinned at the pipeline level, both directions:

- a send from a second chat rides out simultaneous programmatic churn
  (selection null-reset from a gateway/profile reconnect, active-ref retarget
  from a background event, search/hash-only route change from an overlay)
  mid-session.resume and still reaches prompt.submit;
- a genuine user switch (selection AND route moving to another real chat)
  mid-submit still aborts before prompt.submit.

Part of the NousResearch#63078 fix branch.
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…e submit pipeline (NousResearch#64327)

From PR NousResearch#64327 (@Kenmege), whose target-aware drift predicate (compare route
tokens by their routed chat target; ignore selection null-resets, search/hash
churn, and background active-ref retargets; never count a move onto the
submit's own target) already reached main via the NousResearch#69578 salvage
(1bdd478, Co-authored-by Kennedy Umege) and gained the NousResearch#70986 composerScope
prong. What main covered only at the unit level (session-context-drift.test.ts)
is here pinned at the pipeline level, both directions:

- a send from a second chat rides out simultaneous programmatic churn
  (selection null-reset from a gateway/profile reconnect, active-ref retarget
  from a background event, search/hash-only route change from an overlay)
  mid-session.resume and still reaches prompt.submit;
- a genuine user switch (selection AND route moving to another real chat)
  mid-submit still aborts before prompt.submit.

Part of the NousResearch#63078 fix branch.
austinpickett added a commit to austinpickett/hermes-agent that referenced this pull request Sep 25, 2026
…arch#62823 zombie slot)

When a compute-host turn has not settled by session close, the session's
real active-session lease is parked in _deferred_active_session_leases and
the turn's completion callback is supposed to release it. If that callback
is lost (supervisor restart/reload, child killed without failing its
pending turns, dropped completion), the lease sat in the registry forever:
_own_live_lease_ids vouches for it so the orphan sweep never reclaims it,
and the concurrent-session cap treats the dead session as active - new
sessions could not send until a backend restart.

Track when each lease was deferred and have the reaper tick force-release
deferred leases past a generous 30-minute ceiling (the longest legitimate
isolated turn is the compression ceiling, minutes). Normal settlement
still releases immediately and clears the age entry.

Fixes NousResearch#62823 (zombie-concurrency-slot half; cross-window queue-sync was
fixed by NousResearch#122953 and visual-merge by NousResearch#70986 - not touched here)
austinpickett added a commit that referenced this pull request Sep 25, 2026
…ombie slot)

When a compute-host turn has not settled by session close, the session's
real active-session lease is parked in _deferred_active_session_leases and
the turn's completion callback is supposed to release it. If that callback
is lost (supervisor restart/reload, child killed without failing its
pending turns, dropped completion), the lease sat in the registry forever:
_own_live_lease_ids vouches for it so the orphan sweep never reclaims it,
and the concurrent-session cap treats the dead session as active - new
sessions could not send until a backend restart.

Track when each lease was deferred and have the reaper tick force-release
deferred leases past a generous 30-minute ceiling (the longest legitimate
isolated turn is the compression ceiling, minutes). Normal settlement
still releases immediately and clears the age entry.

Fixes #62823 (zombie-concurrency-slot half; cross-window queue-sync was
fixed by #122953 and visual-merge by #70986 - not touched here)
dzianisv pushed a commit to dzianisv/hermes-agent that referenced this pull request Sep 26, 2026
…arch#62823 zombie slot)

When a compute-host turn has not settled by session close, the session's
real active-session lease is parked in _deferred_active_session_leases and
the turn's completion callback is supposed to release it. If that callback
is lost (supervisor restart/reload, child killed without failing its
pending turns, dropped completion), the lease sat in the registry forever:
_own_live_lease_ids vouches for it so the orphan sweep never reclaims it,
and the concurrent-session cap treats the dead session as active - new
sessions could not send until a backend restart.

Track when each lease was deferred and have the reaper tick force-release
deferred leases past a generous 30-minute ceiling (the longest legitimate
isolated turn is the compression ceiling, minutes). Normal settlement
still releases immediately and clears the age entry.

Fixes NousResearch#62823 (zombie-concurrency-slot half; cross-window queue-sync was
fixed by NousResearch#122953 and visual-merge by NousResearch#70986 - not touched here)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P1 High — major feature broken, no workaround sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

3 participants