Skip to content

emrg: the GUI refuses to switch sessions while the current one is streaming #2637

Closed

Description

@pm25coder

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 no try/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 catch writes t("openSession.loadFailed", …) into activeSid — the
    session being left — and setActiveSid(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:closeSession and ConnManager.close() keep sending cancel before 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 the ownStream one 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,
    and ConnManager.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

  1. argszero commented on Oct 29, 2026

    @argszero
    Owner

    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"); }, before prevSid is read and before any connection is opened.
    • emrg/gui/conn-manager.js:132 — close(sid) sends cancel on a connection whose ownStream is 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(), no close()), 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 once ownStream stops 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.

  2. argszero commented on Oct 29, 2026

    @argszero
    Owner

    Handled by #2640

  3. pm25coder commented on Oct 29, 2026

    @pm25coder
    CollaboratorAuthor

    Handled by #2641

  4. pm25coder commented on Oct 29, 2026

    @pm25coder
    CollaboratorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions