Skip to content

fix(desktop): prevent stale events from corrupting UI after session switch#1582

Merged
esengine merged 2 commits into
esengine:mainfrom
Bernardxu123:fix/session-switch-stale-events
May 23, 2026
Merged

fix(desktop): prevent stale events from corrupting UI after session switch#1582
esengine merged 2 commits into
esengine:mainfrom
Bernardxu123:fix/session-switch-stale-events

Conversation

@Bernardxu123

@Bernardxu123 Bernardxu123 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Fixes #1217

When a session switch happens while a turn is running, the old turn cleanup may still emit events that corrupt the new session UI state.

Add switching flag to Tab interface:

  • Set tab.switching=true ONLY when tab.aborter exists (live turn to abort)
  • Check tab.switching in runTurn finally block to suppress stale events
  • Reset flag after processing

This prevents the flag from getting stuck when no turn is in flight — the common session-switch path.

Esc + New Session still works because switching is only set when aborter exists, not in Esc handler.

Tested: npm run lint, npm run typecheck

…witch (esengine#1217)

When a session switch happens while a turn is running, the old turn's
cleanup code may still emit events (, , etc.)
that arrive after the new session is active, causing UI lag and state
corruption.

Add a switching flag to the Tab interface:
- Set tab.switching = true before aborting in session_load/new_chat
- In runTurn's finally block, check tab.switching to suppress stale events
- Reset tab.switching = false after processing

This prevents the old turn from emitting events that would interfere
with the new session's state.

Fixes esengine#1217
@esengine

Copy link
Copy Markdown
Owner

Thanks for splitting this out of #1579 — clean focused diff, mergeable against main, and the core mechanism is right.

One blocker before I can merge, though. The flag gets stuck true on the most common path, not just an edge case.

Sequence that breaks (any session switch, not just abort-then-switch):

  1. Previous turn finishes normally → runTurn's finally runs → tab.aborter = null, tab.switching = false (already).
  2. User clicks a different session → session_load handler runs:
    tab.switching = true;
    abortTurn(tab);  // tab.aborter?.abort() is a no-op — aborter is null
    ...
  3. tab.switching is now true. No runTurn is in flight, so no finally fires to reset it.
  4. User types the first message in the new session → new runTurn runs → finally:
    if (!tab.switching) { /* … */ }   // false → all wrap-up events skipped
    tab.switching = false;
  5. Every terminal event for that first new-session turn is suppressed: $turn_complete, emitSessions, emitBalance, $plan_cleared, the QQ channel response.

The user-visible symptom is worse than #1217 was: the UI sits on "thinking" forever because $turn_complete never arrives, balance/session list don't refresh, and the QQ integration drops the first reply. This isn't an edge case — it's the standard "switch session, then send a message" flow.

Minimal fix

Only set the flag when there's actually a live turn to abort. Two-line change at both call sites:

// session_load and new_chat handlers
if (tab.aborter) tab.switching = true;
abortTurn(tab);

That keeps the rest of the PR identical. If you'd rather avoid the shared mutable flag entirely, the cleaner alternative is to snapshot the session in runTurn and compare in finally:

async function runTurn(tab: Tab, text: string, fromQQ = false): Promise<void> {
  if (!tab.runtime) return;
  const startSession = tab.currentSession;
  ...
  finally {
    tab.aborter = null;
    if (tab.currentSession === startSession) {
      // emit $turn_complete / emitSessions / emitBalance / $plan_cleared / QQ
    }
  }
}

Then drop the tab.switching field and the two tab.switching = true lines entirely. No shared state, no reset, no stuck-flag class of bug. Whichever you prefer.

Push a fixup and I'll merge.

…1217)

The previous implementation set tab.switching=true unconditionally in
session_load/new_chat handlers. When no turn was in flight (the common
case), the flag stayed true and suppressed events for the first turn
in the new session.

Fix: only set switching=true when tab.aborter exists (live turn).
@Bernardxu123

Copy link
Copy Markdown
Collaborator Author

Applied your suggested fix:

\\ s
// Only set switching flag when there's a live turn to abort
if (tab.aborter) tab.switching = true;
abortTurn(tab);
\\

This prevents the flag from getting stuck when no turn is in flight (the common session-switch path). Verified locally: lint ✅, typecheck ✅.

@esengine

Copy link
Copy Markdown
Owner

Fixup looks right — gated on tab.aborter so the flag can't stick true on the common path. Merging.

@esengine esengine merged commit f67fcd2 into esengine:main May 23, 2026
4 checks passed
@Bernardxu123 Bernardxu123 deleted the fix/session-switch-stale-events branch May 23, 2026 13:22
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.

Mac桌面端 会话运行时打开新会话后切换会话卡死

2 participants