fix(desktop): close cross-session leak windows in composer + session refs (#59305) - #70610
Closed
JoaoMarcos44 wants to merge 3 commits into
Closed
fix(desktop): close cross-session leak windows in composer + session refs (#59305)#70610JoaoMarcos44 wants to merge 3 commits into
JoaoMarcos44 wants to merge 3 commits into
Conversation
…refs (NousResearch#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>
…ousResearch#59305) 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>
Contributor
|
Merged via #70986 with your authorship preserved on both fix commits — outstanding work on this one: the #70609 root-cause analysis (two passive-effect race windows + missing submit coherence check) made the salvage mechanical, and all 141 tests + typecheck shipped clean. Only your infographic asset commit was dropped. Fixes #59305 and #70609. |
This was referenced Jul 28, 2026
Open
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
Root-cause fix for #59305 ("[Desktop] Chat tab messages leak across sessions — cross-tab content mixing"). Full root-cause writeup in #70609.
Two independent React passive-effect race windows let a fast session switch land content in the wrong chat, plus a third gap where nothing checked that the composer and the session-side refs agreed with each other at the instant of send:
use-session-state-cache.ts—activeSessionIdRef/selectedStoredSessionIdRefwere mirrored from props viauseEffect, which fires one commit after paint. A synchronous read in that window (a stable callback firing against the just-painted new session) still observed the outgoing session's ids.use-composer-draft.ts— the per-thread attachment-scope-swap effect (attachmentScope.$attachments.set(...)on session switch) was the same kind ofuseEffect. In the race window,$composerAttachmentsstill held the outgoing session's chips — a submit fired in that window would ship them into the new session's turn. This is the actual content leak, not just a visual glitch.submit.ts/session-context-drift.ts— even with 1–2 fixed, the composer (ChatBar) and the session state cache live in separate React subtrees and had no cross-check against each other at send time.Fix
use-session-state-cache.ts: session refs now mirror their prop synchronously during render instead of viauseEffect. Guarded to fire only when the prop itself actually changed (replicating auseEffect([dep])dependency array, just synchronously) — an unconditional resync was tried first and would have silently clobbered an intentional imperative ref pin (submit.tspins a freshly-resumed runtime id into the ref without touching the source atom, by design) on the next unrelated re-render. Caught by a dedicated regression test before it shipped.use-composer-draft.ts: the scope-swap effect is now auseLayoutEffect— runs before paint, closing the window. Also added a redacted[composer-rehydrate]diagnostic log (counts/kinds/scope only — never raw refs/URLs/paths) to help triage any future report in this class.session-context-drift.ts: new third drift prong comparing the composer's loaded scope (SubmitTextOptions.composerScope, threaded fromuse-composer-submit.ts) against the submit target. Compared against the target resolved into the same durable lineage-root domain the composer itself uses (resolveComposerSessionKey), not the raw tip id — comparing against the raw tip would have false-positive-aborted every submit into any session that has ever gone through auto-compression. Verified with a dedicated regression test that reproduces the false-positive against the naive comparison before fixing it.routes.ts/chat/index.tsx: the primary composer's durable scope key (primaryRouteSelectedSessionId) now prefers the route over a possibly momentarily-stale store selection during a switch.chat-runtime.ts:attachmentId()normalizes the value (URL trailing slash,\vs/paths) before hashing, so a re-attach of the same resource dedupes instead of appearing as a second attachment.Test plan
sleep/timing-dependent repros) — each proven to fail against the pre-fix code, then pass after.tsc --noEmit: no new errors in any touched file.test:ui: no new failures introduced (pre-existing unrelated locale/timing failures confirmed identical viagit stashcomparison against the same base).Closes #70609. Related to #59305.