fix(desktop): guard session.info state patches with explicitSid - #60973
fix(desktop): guard session.info state patches with explicitSid#60973yingliang-zhang wants to merge 1 commit into
Conversation
teknium1
left a comment
There was a problem hiding this comment.
Thanks for isolating the state-patch path rather than changing display-event routing.
Problems
- The changed guard has no direct regression coverage.
apps/desktop/src/lib/gateway-events.test.ts:21-25only checks the classifier; it does not invokeuseGatewayEventHandleror verifyupdateSessionStatefor an unscoped, state-patchedsession.info. - The claimed unscoped background-event route was not established from current main.
_emitalways serializessession_idintui_gateway/server.py:1143-1147, while the desktop routing contract says missing IDs represent the focused turn inapps/desktop/src/lib/gateway-events.ts:21-23.
Suggested changes
- Add a hook-level regression test for an unscoped state patch with an active session (no per-session mutation), plus an explicit-ID event that updates the addressed session.
- Identify the valid gateway emission path that produces the Race 3 event so the new
session.infoexception is covered by a concrete contract.
Automated hermes-sweeper review.
| // Only apply per-session state patches when we have a real session_id. | ||
| // Without explicitSid, sessionId falls back to activeSessionIdRef.current | ||
| // — patching state with that fallback could misattribute a background | ||
| // session's model/cwd/branch onto the active session during a switch. |
There was a problem hiding this comment.
Please add a hook-level regression test for this guard. The current gateway-events.test.ts only permits unscoped session.info; it does not verify that an unscoped state patch cannot call updateSessionState for the active session, while an explicit session ID still can.
…o state patches Race 5 (composer queue cross-session migration): When a prompt is queued in session A and the user switches to session B before the queue drains, the drain used the currently-active session id (B) instead of the source session (A). Now each QueuedPromptEntry stamps sourceRuntimeId and sourceStoredId at enqueue time, and runDrain threads them through onSubmit to submitPromptText so the prompt always lands in the original session. removeQueuedPrompt also uses the entry's source key. Fixes NousResearch#56390 (complementary to PR NousResearch#56444 which takes a similar approach). Race 3 (desktop event layer fallback): session.info events with empty session_id fell back to activeSessionIdRef.current for updateSessionState calls, potentially patching the active session's model/cwd/branch with a background session's state during a switch. Now the per-session state-patch path requires explicitSid - unscoped events still render (deltas, completions) but can't mutate per-session state attributes.
e9dba00 to
616a3a4
Compare
|
Closing after re-auditing the current gateway/Desktop contract against the maintainer review. On current main, Verification on current main: Desktop gateway-events Vitest 6 passed, Desktop typecheck PASS, and the replacement worktree has no delta. Thanks for the review—it correctly exposed that the original Race 3 premise was not established. |
|
Correction to my previous comment: the old combined commit is not wholly patch-equivalent to current main ( |
Summary
The desktop event handler in
gateway-event.tsresolvessessionId = explicitSid || activeSessionIdRef.current. WhenexplicitSidis empty (unscoped event), events fall back to the active session — which is correct for display (deltas, completions are the focused turn's own output, see #42178). But thesession.infostate-patch path (updateSessionState) used the same fallbacksessionIdto patch per-session model/cwd/branch.During a session switch, an unscoped
session.infocould patch the active session's state attributes with a background session's values — the Race 3 gap left open by #53936.Fix: require
explicitSidfor theupdateSessionStatecall in thesession.infohandler. Display operations (delta append, completion) still use the fallback — only per-session state mutations are gated.Before (leaks state patches via fallback sessionId)
After (only patches when we have a real session_id)
Why this is safe
session_idstill get their state patches (the common case).session.infoevents from the active turn still apply global setters (setCurrentModel,setCurrentCwd, etc.) via the existingapplyguard — only the per-sessionupdateSessionStatecall is gated.message.delta,message.complete,reasoning.delta,tool.start, etc.) are unaffected — they usesessionIdfor rendering but don't callupdateSessionStatewith state patches.Relationship to existing work
session_idfilter (Races 2, 2b)session.infostate-patch guard (Race 3)#53936 explicitly notes the desktop Electron layer fallback as remaining. This PR closes that gap.
Tests
NODE_ENV=test npm --workspace apps/desktop run typecheck— passesvitest run src/lib/gateway-events.test.ts— 3/3 passNODE_ENV=production npm --workspace apps/desktop run build— passesChecklist
zhangyingliang@outlook.com.envin diff