From 07b033f458514816b631b79a57c59c276bbfd99b Mon Sep 17 00:00:00 2001 From: hugocasa Date: Tue, 1 Sep 2026 09:14:57 +0200 Subject: [PATCH] fix(ai-sessions): stop a disposed runtime's catch-up from writing back The catch-up probes asked only whether a newer turn-end had superseded this one, never whether the runtime it captured still exists. A session deleted in another tab mid-read disposed the runtime under the task, and the resuming catch-up re-armed the orphaned manager's job poller (a self-rescheduling chain nothing could reach to stop) and re-took the store handle disposal had just closed. The same identity check initRuntime already makes now sits beside each supersession probe. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019N8x51GqABUEFvg52V6aYo --- .../src/lib/components/sessions/sessionRuntime.svelte.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts index 97857d15a4..5b63935110 100644 --- a/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts +++ b/frontend/src/lib/components/sessions/sessionRuntime.svelte.ts @@ -1158,6 +1158,10 @@ async function applyTurnEnd( // only for that newer catch-up to redo them — and it settles the caught-up // gate too, so leave it held here. if (superseded()) return + // Disposal is the other thing the awaits can hide: a session deleted in + // another tab mid-read must not have its orphaned manager re-arm the job + // poller or re-take the store handle disposal just closed. + if (runtimes.get(sessionId) !== runtime) return if (found === 'unavailable') { // The store is unreadable *right now*, which says nothing about the // conversation — so this tab still holds the one from before the run. @@ -1186,6 +1190,7 @@ async function applyTurnEnd( // unlock the composer over a transcript missing that newer turn for as // long as the queued catch-up is still reading. if (superseded()) return + if (runtimes.get(sessionId) !== runtime) return caughtUp = true } catch (e) { // A read that threw leaves the same mismatched pair an 'unavailable' one