chore: surface swallowed errors + remove dead session_renamed paths

Swallowed-error logging (audit Feature 3):
- file_index.ts:36-37 (git mtime probes): comment — best-effort, project
  may not be a git repo.
- useUserEvents.ts:44 / 53 (ws.close on error / unmount): comments —
  best-effort, socket may already be closing.
- RightRail.tsx:38 (localStorage write): comment — best-effort, quota or
  private mode.
- App.tsx:21 (api.sessions.get for RightRail projectId): replaced silent
  catch with console.warn.
- Session.tsx:38, 41 (session fetch + project list for breadcrumb):
  replaced silent catches with console.warn.

H1: ProjectSidebar.tsx:189 — dropped the local sessionEvents.emit
({type:'session_renamed'}) after PATCH. Server publishes via
broker.publishUser since v1.4; useUserEvents forwards.

H2: useSessionStream.ts session_renamed case removed (dead — no
server code path publishes session_renamed on the per-session WS
channel; only user channel via broker.publishUser). Also dropped the
session_renamed variant from WsFrame (in apps/web/src/api/types.ts)
to keep the discriminated-union switch exhaustive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 04:35:49 +00:00
parent 1ecb79476e
commit 2f6be39efd
8 changed files with 17 additions and 15 deletions

View File

@@ -33,6 +33,7 @@ async function snapMtimes(root: string): Promise<MtimeSnap> {
const rootStat = await fs.stat(root); const rootStat = await fs.stat(root);
let gitHead: number | null = null; let gitHead: number | null = null;
let gitIndex: number | null = null; let gitIndex: number | null = null;
// best-effort; ignore failure because the project may not be a git repo
try { gitHead = (await fs.stat(path.join(root, '.git', 'HEAD'))).mtimeMs; } catch {} try { gitHead = (await fs.stat(path.join(root, '.git', 'HEAD'))).mtimeMs; } catch {}
try { gitIndex = (await fs.stat(path.join(root, '.git', 'index'))).mtimeMs; } catch {} try { gitIndex = (await fs.stat(path.join(root, '.git', 'index'))).mtimeMs; } catch {}
return { root: rootStat.mtimeMs, gitHead, gitIndex }; return { root: rootStat.mtimeMs, gitHead, gitIndex };

View File

@@ -18,7 +18,10 @@ function SessionRightRail() {
function RightRailForSession({ sessionId }: { sessionId: string }) { function RightRailForSession({ sessionId }: { sessionId: string }) {
const [projectId, setProjectId] = useState<string | null>(null); const [projectId, setProjectId] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
api.sessions.get(sessionId).then((s) => setProjectId(s.project_id)).catch(() => {}); api.sessions
.get(sessionId)
.then((s) => setProjectId(s.project_id))
.catch((err) => console.warn('RightRail: failed to fetch session', err));
}, [sessionId]); }, [sessionId]);
if (!projectId) return null; if (!projectId) return null;
return <RightRail projectId={projectId} />; return <RightRail projectId={projectId} />;

View File

@@ -191,6 +191,5 @@ export type WsFrame =
finished_at?: string | null; finished_at?: string | null;
} }
| { type: 'messages_deleted'; message_ids: string[]; chat_id?: string } | { type: 'messages_deleted'; message_ids: string[]; chat_id?: string }
| { type: 'session_renamed'; session_id: string; name: string; chat_id?: string }
| { type: 'chat_renamed'; chat_id: string; name: string } | { type: 'chat_renamed'; chat_id: string; name: string }
| { type: 'error'; message_id?: string; chat_id?: string; error: string }; | { type: 'error'; message_id?: string; chat_id?: string; error: string };

View File

@@ -19,7 +19,6 @@ import {
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { AddProjectModal } from './AddProjectModal'; import { AddProjectModal } from './AddProjectModal';
import { api } from '@/api/client'; import { api } from '@/api/client';
import { sessionEvents } from '@/hooks/sessionEvents';
import { useSidebar } from '@/hooks/useSidebar'; import { useSidebar } from '@/hooks/useSidebar';
import type { SidebarProject } from '@/api/types'; import type { SidebarProject } from '@/api/types';
import { giteaUrlFor } from '@/lib/projectUrls'; import { giteaUrlFor } from '@/lib/projectUrls';
@@ -186,7 +185,8 @@ export function ProjectSidebar() {
if (!trimmed) return; if (!trimmed) return;
try { try {
await api.sessions.update(sessionId, { name: trimmed }); await api.sessions.update(sessionId, { name: trimmed });
sessionEvents.emit({ type: 'session_renamed', session_id: sessionId, name: trimmed }); // Server publishes session_renamed via broker.publishUser; useUserEvents
// forwards onto the bus. No local emit needed.
} catch (err) { } catch (err) {
toast.error(err instanceof Error ? err.message : 'failed to rename session'); toast.error(err instanceof Error ? err.message : 'failed to rename session');
} }

View File

@@ -35,6 +35,7 @@ export function RightRail({ projectId }: Props) {
const [viewerFile, setViewerFile] = useState<{ path: string; content: string } | null>(null); const [viewerFile, setViewerFile] = useState<{ path: string; content: string } | null>(null);
useEffect(() => { useEffect(() => {
// best-effort; ignore failure because localStorage may be unavailable (quota, private mode)
try { localStorage.setItem(`${STORAGE_KEY}.open`, String(open)); } catch {} try { localStorage.setItem(`${STORAGE_KEY}.open`, String(open)); } catch {}
}, [open]); }, [open]);

View File

@@ -2,6 +2,10 @@ import { useEffect, useRef, useState } from 'react';
import type { Message, WsFrame } from '@/api/types'; import type { Message, WsFrame } from '@/api/types';
import { sessionEvents } from './sessionEvents'; import { sessionEvents } from './sessionEvents';
// session_renamed frame removed from WsFrame — it was declared but never
// published on the per-session WS channel (server publishes via broker.publishUser
// since v1.4). chat_renamed remains; auto_name.ts publishes it on session WS.
interface State { interface State {
messages: Message[]; messages: Message[];
connected: boolean; connected: boolean;
@@ -118,14 +122,6 @@ function applyFrame(state: State, frame: WsFrame): State {
messages: state.messages.filter((m) => !removeSet.has(m.id)), messages: state.messages.filter((m) => !removeSet.has(m.id)),
}; };
} }
case 'session_renamed': {
sessionEvents.emit({
type: 'session_renamed',
session_id: frame.session_id,
name: frame.name,
});
return state;
}
case 'chat_renamed': { case 'chat_renamed': {
sessionEvents.emit({ sessionEvents.emit({
type: 'chat_updated', type: 'chat_updated',

View File

@@ -40,7 +40,8 @@ export function useUserEvents(): void {
}; };
ws.onerror = () => { ws.onerror = () => {
// close handler will trigger reconnect // close handler will trigger reconnect; best-effort, ignore failure
// because the socket may already be closing
try { ws?.close(); } catch {} try { ws?.close(); } catch {}
}; };
}; };
@@ -50,6 +51,7 @@ export function useUserEvents(): void {
return () => { return () => {
unmounted = true; unmounted = true;
if (reconnectTimer) clearTimeout(reconnectTimer); if (reconnectTimer) clearTimeout(reconnectTimer);
// best-effort cleanup; ignore failure because the socket may already be closed
if (ws) try { ws.close(); } catch {} if (ws) try { ws.close(); } catch {}
}; };
}, []); }, []);

View File

@@ -38,9 +38,9 @@ export function Session() {
if (cancelled) return; if (cancelled) return;
const p = projects.find((x) => x.id === s.project_id); const p = projects.find((x) => x.id === s.project_id);
if (p) setProject(p); if (p) setProject(p);
}).catch(() => {}); }).catch((err) => console.warn('Session: failed to load project for breadcrumb', err));
}) })
.catch(() => {}); .catch((err) => console.warn('Session: failed to fetch session', err));
return () => { return () => {
cancelled = true; cancelled = true;
}; };