Repository navigation
emrg: the GUI refuses to switch sessions while the current one is streaming #2637
Description
What happens
The GUI has two ways to open a session, and both call the same IPC handler
emrg:switchSession (emrg/gui/preload.js:20 → emrg/gui/main.js:571):
- the sidebar's two-step project → session flow,
selectProjectSession
(renderer/src/components/Shell.tsx:323) - the task panel's 「打开会话」 button,
openTaskSession
(renderer/src/components/Shell.tsx:332)
While the session that is currently active still has a turn streaming on its connection, the
main process refuses the switch before doing anything else (main.js:587):
// G65:自有流运行中禁止切会话(每连接独立锁,查当前激活连接)
if (connManager?.get(currentSessionId)?.ownStream) {
logger.warn(`[gui:switch] REJECT own stream in progress (sid=${currentSessionId})`);
throw new Error("stream in progress — cannot switch");
}Nothing switches, and the two callers fail in different, both-bad ways:
- Sidebar:
await b.switchSession({ sessionId: sid })has notry/catch, and
setActiveSid(sid)sits after it. The rejection is an unhandled promise rejection — no
visible change at all, and a console error the user does not see. - Task panel: the
catchwritest("openSession.loadFailed", …)intoactiveSid— the
session being left — andsetActiveSid(task.session_id)never runs. The user is
looking at the task list, so the message lands in a transcript that is not on screen; the
click looks like it did nothing.
Either way the turn that is running keeps running. The stream is unaffected by the refusal
it caused; only the navigation is blocked.
Why that refusal no longer protects anything
The check comes from a design in which switching a session closed the old connection.
ConnManager.close() sends cancel to a connection whose ownStream is set before
dropping its websocket (emrg/gui/conn-manager.js:129), so allowing a switch would have
quietly killed the turn that was running. Refusing the switch was the safe half of that
pair.
Switching no longer closes anything. A switch opens or reuses the target session's own
connection and deliberately leaves the previous one alone (main.js:625: 「切走不再关闭
旧会话连接」); closing is a separate action, emrg:closeSession. Every session already has
its own websocket and its own transcript bucket, and the daemon runs several sessions
concurrently. So the guard now protects nothing and only refuses: a turn streaming on
session A is unaffected by moving the window to session B, and coming back to A finds it
where it was.
What the fix must satisfy
- Switching sessions is possible at any time, including while the session being left is
still streaming. Nothing on the switch path refuses on that fact. - Everything switching does today is unchanged: the previous session's connection stays
open, its stream keeps writing its own transcript bucket, and returning to it shows the
turn intact — not restarted, not duplicated. emrg:closeSessionandConnManager.close()keep sendingcancelbefore dropping a
busy connection (conn-manager.js:133). That is the place where a turn would otherwise
be cut off mid-flight, and it stays exactly as it is.- The other refusals on this path — invalid
session_id, invalid agent path, the
20-open-session cap (main.js:573–:586) — are untouched; only theownStreamone goes. - A refusal this path keeps or gains must be visible where the user acted. A message in a
transcript the user is not looking at, or an unhandled rejection, is exactly what makes a
click look ignored. - A regression test holds the property: the handler region no longer reads
ownStream,
andConnManager.close()'s cancel-before-close still does.
Reading the behaviour
~/.emrg/logs/emrg-gui.log carries one [gui:switch] line per attempt — REQUEST,
REJECT <reason>, openSession OK, DONE. A switch is refused precisely when its attempt
logs REJECT own stream in progress; the control is the same session switching normally
while idle.
Activity
argszero commentedon Oct 29, 2026
Verified against master
3988a2b3, and the premise holds on all three points the report names:emrg/gui/main.js:587— the guard is exactly as quoted:if (connManager?.get(currentSessionId)?.ownStream) { ... throw new Error("stream in progress — cannot switch"); }, beforeprevSidis read and before any connection is opened.emrg/gui/conn-manager.js:132—close(sid)sendscancelon a connection whoseownStreamis set, then closes it. So the refusal is the safe half of a pair whose other half no longer exists on the switch path.emrg/gui/main.js:625— switching leaves the previous connection alone (clearGroups(), noclose()), which is the change that made the refusal obsolete.One addition to "what the fix must satisfy", because it decides where the fix goes: the sidebar's missing
try/catch(renderer/src/components/Shell.tsx:323) is the same defect one layer up, and it will still surface any other rejection on that path onceownStreamstops producing one. The two callers should get the same treatment as the guard — a refusal the user can see where they acted — or the next refusal to appear will be just as invisible.argszero commentedon Oct 29, 2026
Handled by #2640
pm25coder commentedon Oct 29, 2026
Handled by #2641
pm25coder commentedon Oct 29, 2026
Correction to the link written above: #2640 carries this issue, not #2641.
#2641 removed the same refusal but answered the visibility requirement by writing the failure
into the current session's transcript — which is the defect this issue names for the task panel.
#2640 keeps the user where they acted and reports the refusal there, so #2641 was folded into it
and closed rather than kept as a second PR for this issue.