From c8b26eced6855c440c1aee55ea40add8d4a1a5aa Mon Sep 17 00:00:00 2001 From: Johnpaul Date: Tue, 28 Apr 2026 23:59:23 +0100 Subject: [PATCH] fix deleted session hang and ram leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the active session was deleted, SessionPage stayed mounted on the dead id. The 3s title poll swallowed 404s and kept running, the workspace event sync stayed subscribed, and the sidebar showed the stale entry during the refetch window — so clicking it remounted against a missing session and pinned the UI. onDeleteSession now optimistically filters the session out of the sidebar list and navigates to /session (when the deleted session was active) before issuing the HTTP DELETE. Navigating synchronously flips selectedSessionId to null, which fires trackWorkspaceSessionSync cleanup and drops the React Query caches. refreshRouteState still runs afterwards to reconcile with the server. refreshSelectedSessionTitle now treats a 404 (or missing item) as a deleted session: it cancels the interval, removes the session from the sidebar list, and navigates back to /session instead of looping forever. --- .../app/src/react-app/shell/session-route.tsx | 54 ++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/apps/app/src/react-app/shell/session-route.tsx b/apps/app/src/react-app/shell/session-route.tsx index 20542ce1c..4366f6482 100644 --- a/apps/app/src/react-app/shell/session-route.tsx +++ b/apps/app/src/react-app/shell/session-route.tsx @@ -6,6 +6,7 @@ import type { ConfigProvidersResponse, FilePartInput, ProviderListResponse, + Session, TextPartInput, } from "@opencode-ai/sdk/v2/client"; @@ -14,6 +15,7 @@ import { listCommands, shellInSession } from "../../app/lib/opencode-session"; import { buildOpenworkWorkspaceBaseUrl, createOpenworkServerClient, + OpenworkServerError, readOpenworkServerSettings, type OpenworkServerClient, type OpenworkWorkspaceInfo, @@ -626,10 +628,33 @@ export function SessionRoute() { if (!client || !selectedWorkspaceId || !selectedSessionId) return; let cancelled = false; + let interval: number | null = null; + + const handleMissingSession = (missingSessionId: string, missingWorkspaceId: string) => { + if (cancelled) return; + cancelled = true; + if (interval !== null) { + window.clearInterval(interval); + interval = null; + } + setSessionsByWorkspaceId((current) => { + const list = current[missingWorkspaceId]; + if (!list) return current; + const next = list.filter((session: Session) => session?.id !== missingSessionId); + if (next.length === list.length) return current; + return { ...current, [missingWorkspaceId]: next }; + }); + navigate("/session", { replace: true }); + }; + const refreshSelectedSessionTitle = async () => { try { const response = await client.getSession(selectedWorkspaceId, selectedSessionId); - if (cancelled || !response.item) return; + if (cancelled) return; + if (!response.item) { + handleMissingSession(selectedSessionId, selectedWorkspaceId); + return; + } setSessionsByWorkspaceId((current) => { const list = current[selectedWorkspaceId] ?? []; const index = list.findIndex((session: any) => session?.id === selectedSessionId); @@ -640,18 +665,25 @@ export function SessionRoute() { nextList[index] = nextSession; return { ...current, [selectedWorkspaceId]: nextList }; }); - } catch { + } catch (error) { + if (error instanceof OpenworkServerError && error.status === 404) { + handleMissingSession(selectedSessionId, selectedWorkspaceId); + return; + } // Best-effort title sync; the session surface still owns messages. } }; void refreshSelectedSessionTitle(); - const interval = window.setInterval(() => void refreshSelectedSessionTitle(), 3_000); + interval = window.setInterval(() => void refreshSelectedSessionTitle(), 3_000); return () => { cancelled = true; - window.clearInterval(interval); + if (interval !== null) { + window.clearInterval(interval); + interval = null; + } }; - }, [client, selectedSessionId, selectedWorkspaceId]); + }, [client, navigate, selectedSessionId, selectedWorkspaceId]); useEffect(() => { workspacesRef.current = workspaces; @@ -1712,9 +1744,15 @@ export function SessionRoute() { onDeleteSession={ client && selectedWorkspaceId ? async (sessionId) => { - await client.deleteSession(selectedWorkspaceId, sessionId); - if (selectedSessionId === sessionId) { - navigate("/session"); + const wasActive = selectedSessionId === sessionId; + const workspaceId = selectedWorkspaceId; + if (wasActive) { + navigate("/session", { replace: true }); + } + try { + await client.deleteSession(workspaceId, sessionId); + } catch (error) { + console.error("[session-route] deleteSession failed", error); } await refreshRouteState(); }