From 9fb2d91b26bef9579337d953a29718802c466fed Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:04:50 +0300 Subject: [PATCH] fix: resolve session provider on backend reads Session history and token usage reads already have a stable app session id. Passing provider and project hints from the frontend kept those reads coupled with provider-specific state that the backend can resolve from the session row. Resolve token usage provider server-side and narrow the session store read API to session id plus pagination. This keeps provider-specific storage decisions behind the backend boundary and makes reconnect, pagination, and load-all use the same session-owned contract. --- server/index.js | 9 ++++- .../chat/hooks/useChatSessionState.ts | 40 +++---------------- src/components/chat/view/ChatInterface.tsx | 12 +----- src/stores/useSessionStore.ts | 11 ----- 4 files changed, 14 insertions(+), 58 deletions(-) diff --git a/server/index.js b/server/index.js index 4301733b..9ebe74d6 100755 --- a/server/index.js +++ b/server/index.js @@ -1135,7 +1135,6 @@ app.post('/api/projects/:projectId/upload-images', authenticateToken, async (req app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticateToken, async (req, res) => { try { const { projectId, sessionId } = req.params; - const { provider = 'claude' } = req.query; const homeDir = os.homedir(); // Allow only safe characters in sessionId @@ -1146,8 +1145,14 @@ app.get('/api/projects/:projectId/sessions/:sessionId/token-usage', authenticate // Provider artifacts on disk (JSONL file names, OpenCode sqlite rows) // are keyed by the provider-native session id, while the caller sends - // the app-facing id. Resolve the mapping once for all branches below. + // the app-facing id. Resolve provider and id mapping from the indexed + // session row so the frontend does not choose provider-specific paths. const sessionRow = sessionsDb.getSessionById(safeSessionId); + if (!sessionRow) { + return res.status(404).json({ error: 'Session not found', sessionId: safeSessionId }); + } + + const provider = sessionRow.provider || 'claude'; const providerNativeSessionId = sessionRow?.provider_session_id || safeSessionId; // Handle Cursor sessions - they use SQLite and don't have token usage info diff --git a/src/components/chat/hooks/useChatSessionState.ts b/src/components/chat/hooks/useChatSessionState.ts index f625e625..652edd87 100644 --- a/src/components/chat/hooks/useChatSessionState.ts +++ b/src/components/chat/hooks/useChatSessionState.ts @@ -5,7 +5,7 @@ import { authenticatedFetch } from '../../../utils/api'; import type { MarkSessionIdle, SessionActivityMap } from '../../../hooks/useSessionProtection'; import type { Project, ProjectSession, LLMProvider } from '../../../types/app'; import type { SessionStore, NormalizedMessage } from '../../../stores/useSessionStore'; -import type { ChatMessage, Provider } from '../types/types'; +import type { ChatMessage } from '../types/types'; import { createCachedDiffCalculator, type DiffCalculator } from '../utils/messageTransforms'; import { normalizedToChatMessages } from './useChatMessages'; @@ -328,18 +328,12 @@ export function useChatSessionState({ if (allMessagesLoadedRef.current) return false; if (!hasMoreMessages || !selectedSession || !selectedProject) return false; - const sessionProvider = selectedSession.__provider || 'claude'; - isLoadingMoreRef.current = true; const previousScrollHeight = container.scrollHeight; const previousScrollTop = container.scrollTop; try { const slot = await sessionStore.fetchMore(selectedSession.id, { - provider: sessionProvider as LLMProvider, - // DB-assigned projectId replaces the legacy folder-derived name. - projectId: selectedProject.projectId, - projectPath: selectedProject.fullPath || selectedProject.path || '', limit: MESSAGES_PER_PAGE, }); if (!slot || slot.serverMessages.length === 0) return false; @@ -458,8 +452,7 @@ export function useChatSessionState({ return; } - const provider = (selectedSession.__provider || localStorage.getItem('selected-provider') as Provider) || 'claude'; - const sessionKey = `${selectedSession.id}:${selectedProject.projectId}:${provider}`; + const sessionKey = `${selectedSession.id}:${selectedProject.projectId}`; // Skip if already loaded and fresh if (lastLoadedSessionKeyRef.current === sessionKey && sessionStore.has(selectedSession.id) && !sessionStore.isStale(selectedSession.id)) { @@ -512,9 +505,6 @@ export function useChatSessionState({ // Fetch from server → store updates → chatMessages re-derives automatically setIsLoadingSessionMessages(true); sessionStore.fetchFromServer(selectedSession.id, { - provider: (selectedSession.__provider || provider) as LLMProvider, - projectId: selectedProject.projectId, - projectPath: selectedProject.fullPath || selectedProject.path || '', limit: MESSAGES_PER_PAGE, offset: 0, }).then(slot => { @@ -544,15 +534,9 @@ export function useChatSessionState({ const reloadExternalMessages = async () => { try { - const provider = (localStorage.getItem('selected-provider') as Provider) || 'claude'; - // Skip store refresh during active streaming if (!isProcessing) { - await sessionStore.refreshFromServer(selectedSession.id, { - provider: (selectedSession.__provider || provider) as LLMProvider, - projectId: selectedProject.projectId, - projectPath: selectedProject.fullPath || selectedProject.path || '', - }); + await sessionStore.refreshFromServer(selectedSession.id); if (Boolean(autoScrollToBottom) && isNearBottom()) { setTimeout(() => scrollToBottom(), 200); @@ -598,13 +582,9 @@ export function useChatSessionState({ const scrollToTarget = async () => { if (!allMessagesLoadedRef.current && selectedSession && selectedProject) { - const sessionProvider = selectedSession.__provider || 'claude'; try { // Load all messages into the store for search navigation const slot = await sessionStore.fetchFromServer(selectedSession.id, { - provider: sessionProvider as LLMProvider, - projectId: selectedProject.projectId, - projectPath: selectedProject.fullPath || selectedProject.path || '', limit: null, offset: 0, }); @@ -678,13 +658,10 @@ export function useChatSessionState({ setTokenBudget(null); return; } - const sessionProvider = selectedSession.__provider || 'claude'; - const fetchInitialTokenUsage = async () => { try { - // Token usage endpoint is now keyed by the DB projectId. - const params = new URLSearchParams({ provider: sessionProvider }); - const url = `/api/projects/${selectedProject.projectId}/sessions/${selectedSession.id}/token-usage?${params.toString()}`; + // The backend resolves the provider from the indexed session row. + const url = `/api/projects/${selectedProject.projectId}/sessions/${selectedSession.id}/token-usage`; const response = await authenticatedFetch(url); if (response.ok) { setTokenBudget(await response.json()); @@ -696,7 +673,7 @@ export function useChatSessionState({ } }; fetchInitialTokenUsage(); - }, [selectedProject, selectedSession?.id, selectedSession?.__provider]); + }, [selectedProject, selectedSession?.id]); const visibleMessages = useMemo(() => { if (chatMessages.length <= visibleMessageCount) return chatMessages; @@ -756,8 +733,6 @@ export function useChatSessionState({ const loadAllMessages = useCallback(async () => { if (!selectedSession || !selectedProject) return; if (isLoadingAllMessages) return; - const sessionProvider = selectedSession.__provider || 'claude'; - const requestSessionId = selectedSession.id; allMessagesLoadedRef.current = true; isLoadingMoreRef.current = true; @@ -770,9 +745,6 @@ export function useChatSessionState({ try { const slot = await sessionStore.fetchFromServer(requestSessionId, { - provider: sessionProvider as LLMProvider, - projectId: selectedProject.projectId, - projectPath: selectedProject.fullPath || selectedProject.path || '', limit: null, offset: 0, }); diff --git a/src/components/chat/view/ChatInterface.tsx b/src/components/chat/view/ChatInterface.tsx index 7630c052..5efe6af4 100644 --- a/src/components/chat/view/ChatInterface.tsx +++ b/src/components/chat/view/ChatInterface.tsx @@ -6,7 +6,6 @@ import { useWebSocket } from '../../../contexts/WebSocketContext'; import PermissionContext from '../../../contexts/PermissionContext'; import { QuickSettingsPanel } from '../../quick-settings-panel'; import type { ChatInterfaceProps, Provider } from '../types/types'; -import type { LLMProvider } from '../../../types/app'; import { useChatProviderState } from '../hooks/useChatProviderState'; import { useChatSessionState } from '../hooks/useChatSessionState'; import { useChatRealtimeHandlers } from '../hooks/useChatRealtimeHandlers'; @@ -223,16 +222,7 @@ function ChatInterface({ // missed live events, and re-attaches a still-running stream to this socket. const handleWebSocketReconnect = useCallback(async () => { if (!selectedProject || !selectedSession) return; - const providerVal = - selectedSession.__provider - || (localStorage.getItem('selected-provider') as LLMProvider) - || 'claude'; - await sessionStore.refreshFromServer(selectedSession.id, { - provider: providerVal as LLMProvider, - // Use DB projectId; legacy folder-derived projectName is no longer accepted here. - projectId: selectedProject.projectId, - projectPath: selectedProject.fullPath || selectedProject.path || '', - }); + await sessionStore.refreshFromServer(selectedSession.id); statusCheckSentAtRef.current.set(selectedSession.id, Date.now()); sendMessage({ type: 'chat.subscribe', diff --git a/src/stores/useSessionStore.ts b/src/stores/useSessionStore.ts index 3e67026d..999e1645 100644 --- a/src/stores/useSessionStore.ts +++ b/src/stores/useSessionStore.ts @@ -454,9 +454,6 @@ export function useSessionStore() { const fetchFromServer = useCallback(async ( sessionId: string, opts: { - provider?: LLMProvider; - projectId?: string; - projectPath?: string; limit?: number | null; offset?: number; } = {}, @@ -511,9 +508,6 @@ export function useSessionStore() { const fetchMore = useCallback(async ( sessionId: string, opts: { - provider?: LLMProvider; - projectId?: string; - projectPath?: string; limit?: number; } = {}, ) => { @@ -592,11 +586,6 @@ export function useSessionStore() { */ const refreshFromServer = useCallback(async ( sessionId: string, - _opts: { - provider?: LLMProvider; - projectId?: string; - projectPath?: string; - } = {}, ) => { const slot = getSlot(sessionId); try {