From 5ab4347b3909be05496f0a1429aa8fa87b0b5608 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 2 Apr 2026 21:44:54 -0700 Subject: [PATCH] =?UTF-8?q?feat(errors):=20=E2=9C=A8=20Enhance=20error=20h?= =?UTF-8?q?andling=20with=20improved=20display,=20logging,=20and=20user=20?= =?UTF-8?q?experience=20utilities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../src/features/errors/sessionRecovery.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 @applications/web/src/features/errors/sessionRecovery.ts diff --git a/@applications/web/src/features/errors/sessionRecovery.ts b/@applications/web/src/features/errors/sessionRecovery.ts new file mode 100644 index 0000000..9ec186c --- /dev/null +++ b/@applications/web/src/features/errors/sessionRecovery.ts @@ -0,0 +1,63 @@ +import type { MutableRefObject } from 'react'; + +const SESSION_STORAGE_KEY = 'companion_session_id'; + +/** Module-level lock to prevent concurrent recovery attempts. */ +let recoveryPromise: Promise | null = null; + +export async function createSession(apiBaseUrl: string): Promise { + try { + const res = await fetch(`${apiBaseUrl}/session`, { method: 'POST' }); + if (!res.ok) throw new Error(`POST /session failed: ${res.status}`); + const { session_id } = (await res.json()) as { session_id: string }; + sessionStorage.setItem(SESSION_STORAGE_KEY, session_id); + return session_id; + } catch (err) { + const message = err instanceof Error ? err.message : 'Unknown session creation error'; + throw new Error(`Session creation failed: ${message}`); + } +} + +/** + * POST /chat with automatic session recovery on 404. + * + * If the server returns 404 (session expired/missing), creates a new session + * and retries the request once. Updates `sessionIdRef.current` and sessionStorage + * so subsequent calls use the recovered session. + */ +export async function chatWithRecovery( + apiBaseUrl: string, + sessionIdRef: MutableRefObject, + message: string, + signal: AbortSignal, +): Promise { + const doFetch = (sid: string) => + fetch(`${apiBaseUrl}/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ session_id: sid, message }), + signal, + }); + + try { + const response = await doFetch(sessionIdRef.current); + + if (response.status !== 404) return response; + + // Session expired — recover with dedup lock + if (!recoveryPromise) { + recoveryPromise = createSession(apiBaseUrl).finally(() => { + recoveryPromise = null; + }); + } + + const newSessionId = await recoveryPromise; + sessionIdRef.current = newSessionId; + + return doFetch(newSessionId); + } catch (err) { + if (err instanceof Error && err.name === 'AbortError') throw err; + const message_ = err instanceof Error ? err.message : 'Unknown chat error'; + throw new Error(`Chat request failed: ${message_}`); + } +}