Skip to content

fix: route server→client requests by sessionId to prevent cross-session popup leaks#12

Merged
william0wang merged 3 commits into
mainfrom
fix/session-isolation-server-requests
Jul 8, 2026
Merged

fix: route server→client requests by sessionId to prevent cross-session popup leaks#12
william0wang merged 3 commits into
mainfrom
fix/session-isolation-server-requests

Conversation

@william0wang

Copy link
Copy Markdown
Owner

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.serverRequests is a single shared queue for all sessions (one subprocess serves them all). handleServerRequests drained the entire queue and forwarded every request using the caller's acpSid — it never checked params.sessionId to verify the request actually belonged to the current turn.

Fix

  • handleServerRequests (server-requests.ts): when a turn is available, filter queued requests by params.sessionId — only handle those matching turn.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).
  • Edge cases: requests without a sessionId field (unrouteable) are claimed by whichever loop sees them first; when turn is absent (tests), the legacy unfiltered path is used.

Files

File Change
src/backend/client.ts +requeueServerRequests()
src/handlers/server-requests.ts session-filtered drain in handleServerRequests
tests/backend.test.ts regression test for requeue + order preservation

Testing

  • 136 tests pass (10 files), including 1 new regression test.
  • TypeScript type-check passes.
  • session/event notifications were already routed by sessionId (listeners.get(sid)) — no change needed there.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +98 to +115
// 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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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);
    }

@william0wang william0wang merged commit 174614f into main Jul 8, 2026
1 check passed
@william0wang william0wang deleted the fix/session-isolation-server-requests branch July 8, 2026 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant