fix: route server→client requests by sessionId to prevent cross-session popup leaks#12
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces session-based filtering for server-to-client requests to prevent cross-session leaks, along with a mechanism to re-queue unowned requests while preserving their order. Additionally, it improves the visual contrast of the quota progress bar by using a dark shade character instead of a full block and shortens the divider line. The reviewer suggested an optimization in handleServerRequests to process all requests belonging to the current session in a single iteration rather than processing only the first one and re-queueing the rest, which avoids unnecessary queue churn.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // Pick the first request belonging to this session; put the rest back. | ||
| // Requests without a sessionId field are unrouteable — claim them here | ||
| // so they don't sit in the queue forever. | ||
| let mine: ServerRequest | undefined; | ||
| const others: ServerRequest[] = []; | ||
| for (const r of all) { | ||
| const sid = (r.params as { sessionId?: string }).sessionId; | ||
| if (!mine && (sid === undefined || sid === mySid)) { | ||
| mine = r; | ||
| } else { | ||
| others.push(r); | ||
| } | ||
| } | ||
| // Re-queue the ones that don't belong to this session (prepend to preserve order). | ||
| if (others.length > 0) backend.requeueServerRequests(others); | ||
| if (!mine) return handled; | ||
| handled = true; | ||
| await handleOne(server, backend, cx, acpSid, req, pending, turn); | ||
| await handleOne(server, backend, cx, acpSid, mine, pending, turn); |
There was a problem hiding this comment.
Instead of picking only the first matching request and requeueing the rest (including other requests that also belong to the current session), we can separate the polled requests into those belonging to the current session (mine) and those belonging to other sessions (others). This allows us to requeue only the non-matching requests once, and process all of our session's requests sequentially in a single iteration. This avoids unnecessary queue churn and repeated polling/requeueing of our own requests.
// Separate requests belonging to this session from others.
// Requests without a sessionId field are unrouteable — claim them here
// so they don't sit in the queue forever.
const mine: ServerRequest[] = [];
const others: ServerRequest[] = [];
for (const r of all) {
const sid = (r.params as { sessionId?: string }).sessionId;
if (sid === undefined || sid === mySid) {
mine.push(r);
} else {
others.push(r);
}
}
// Re-queue the ones that don't belong to this session (prepend to preserve order).
if (others.length > 0) backend.requeueServerRequests(others);
if (mine.length === 0) return handled;
for (const req of mine) {
handled = true;
await handleOne(server, backend, cx, acpSid, req, pending, turn);
}
Summary
When two sessions share the same workspace (and thus the same ZCode subprocess), server→client requests — permission popups, ExitPlanMode approvals, AskUserQuestion forms — could leak across sessions. Session A's turn loop would pop session B's permission request from the shared queue and forward it to A's editor client, causing the popup to appear in the wrong session window.
Root cause
ZcodeBackend.serverRequestsis a single shared queue for all sessions (one subprocess serves them all).handleServerRequestsdrained the entire queue and forwarded every request using the caller'sacpSid— it never checkedparams.sessionIdto verify the request actually belonged to the current turn.Fix
handleServerRequests(server-requests.ts): when aturnis available, filter queued requests byparams.sessionId— only handle those matchingturn.zcodeSid. Non-matching requests are re-queued for their own session's turn loop to pick up.requeueServerRequests(client.ts): new method that prepends unowned requests back to the front of the queue (preserving arrival order).sessionIdfield (unrouteable) are claimed by whichever loop sees them first; whenturnis absent (tests), the legacy unfiltered path is used.Files
src/backend/client.tsrequeueServerRequests()src/handlers/server-requests.tshandleServerRequeststests/backend.test.tsTesting
session/eventnotifications were already routed bysessionId(listeners.get(sid)) — no change needed there.