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.
This commit is contained in:
Haileyesus
2026-06-15 14:04:50 +03:00
parent 9cb2afd67e
commit 9fb2d91b26
4 changed files with 14 additions and 58 deletions

View File

@@ -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

View File

@@ -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,
});

View File

@@ -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',

View File

@@ -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 {