diff --git a/apps/desktop/src/app/contrib/hooks/use-session-tile-delegate.test.ts b/apps/desktop/src/app/contrib/hooks/use-session-tile-delegate.test.ts index 1297690584ee2..4a236aa2b45b4 100644 --- a/apps/desktop/src/app/contrib/hooks/use-session-tile-delegate.test.ts +++ b/apps/desktop/src/app/contrib/hooks/use-session-tile-delegate.test.ts @@ -10,10 +10,10 @@ import { useSessionTileDelegate } from './use-session-tile-delegate' vi.mock('@/hermes', async importActual => ({ ...(await importActual()), - getSessionMessages: vi.fn(async () => ({ messages: [], session_id: '' })) + getLatestSessionMessages: vi.fn(async () => ({ messages: [], session_id: '' })) })) -const { getSessionMessages } = await import('@/hermes') +const { getLatestSessionMessages } = await import('@/hermes') const row = (over: Partial): SessionInfo => ({ @@ -51,7 +51,7 @@ function renderTile(requestGateway: ReturnType) { describe('useSessionTileDelegate resumeTile', () => { beforeEach(() => { setSessions([]) - vi.mocked(getSessionMessages).mockClear() + vi.mocked(getLatestSessionMessages).mockClear() }) afterEach(() => { @@ -73,7 +73,7 @@ describe('useSessionTileDelegate resumeTile', () => { const runtimeId = await sessionTileDelegate()!.resumeTile('stored-x') expect(runtimeId).toBe('runtime-1') - expect(getSessionMessages).toHaveBeenCalledWith('stored-x', 'ai-engineer') + expect(getLatestSessionMessages).toHaveBeenCalledWith('stored-x', 'ai-engineer') expect(requestGateway).toHaveBeenCalledWith('session.resume', { session_id: 'stored-x', cols: 96, diff --git a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx index 5bbcecaffb6fd..3d2cfa6447af4 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-session-actions.test.tsx @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { $terminalTakeover, setTerminalTakeover } from '@/app/right-sidebar/store' import { noteActiveTreeGroup, revealTreePane } from '@/components/pane-shell/tree/store' -import { getSession, getSessionMessages, type SessionInfo } from '@/hermes' +import { getAllSessionMessages, getLatestSessionMessages, getSession, type SessionInfo } from '@/hermes' import { createClientSessionState } from '@/lib/chat-runtime' import { clearSessionDraft, stashSessionDraft, takeSessionDraft } from '@/store/composer' import { $activeGatewayProfile, $newChatProfile, ensureGatewayProfile } from '@/store/profile' @@ -46,7 +46,8 @@ vi.mock('@/hermes', async importOriginal => ({ ...(await importOriginal>()), deleteSession: vi.fn(), getSession: vi.fn(), - getSessionMessages: vi.fn(), + getAllSessionMessages: vi.fn(), + getLatestSessionMessages: vi.fn(), listAllProfileSessions: vi.fn(), setApiRequestProfile: vi.fn(), setSessionArchived: vi.fn() @@ -677,7 +678,7 @@ describe('resumeSession failure recovery', () => { }) // ...and the REST transcript fallback also rejects (backend unreachable). - vi.mocked(getSessionMessages).mockRejectedValue(new Error('network down')) + vi.mocked(getLatestSessionMessages).mockRejectedValue(new Error('network down')) await runResume(requestGateway) @@ -697,7 +698,7 @@ describe('resumeSession failure recovery', () => { return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [ { content: 'hello', role: 'user', timestamp: 1 }, { content: 'hi there', role: 'assistant', timestamp: 2 } @@ -739,7 +740,7 @@ describe('resumeSession failure recovery', () => { { content: 'earlier answer', role: 'assistant', timestamp: 2 } ] - vi.mocked(getSessionMessages).mockResolvedValue({ messages: storedMessages, session_id: 'stored-1' } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: storedMessages, session_id: 'stored-1' } as never) const requestGateway = vi.fn(async (method: string) => { if (method === 'session.resume') { @@ -772,7 +773,7 @@ describe('resumeSession failure recovery', () => { { content: 'earlier answer', role: 'assistant', timestamp: 2 } ] - vi.mocked(getSessionMessages).mockResolvedValue({ messages: storedMessages, session_id: 'stored-1' } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: storedMessages, session_id: 'stored-1' } as never) const requestGateway = vi.fn(async (method: string) => { if (method === 'session.resume') { @@ -825,7 +826,7 @@ describe('resumeSession failure recovery', () => { { content: 'answer after compression', role: 'assistant', timestamp: 4 } ] - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: parentMessages, session_id: 'stored-1' } as never) @@ -873,7 +874,7 @@ describe('resumeSession failure recovery', () => { return {} as never }) - vi.mocked(getSessionMessages).mockRejectedValue(new Error('network down')) + vi.mocked(getLatestSessionMessages).mockRejectedValue(new Error('network down')) // resumeSession must resolve (swallow the fallback failure), not reject. await expect(runResume(requestGateway)).resolves.toBeUndefined() @@ -891,7 +892,7 @@ describe('resumeSession failure recovery', () => { return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ messages: [] } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [] } as never) await runResume(requestGateway) @@ -915,7 +916,7 @@ describe('resumeSession failure recovery', () => { return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ messages: [] } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [] } as never) await runResume(requestGateway) @@ -935,7 +936,7 @@ describe('resumeSession failure recovery', () => { return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ messages: [], session_id: 'stored-1' } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [], session_id: 'stored-1' } as never) await runResume(requestGateway) @@ -990,7 +991,7 @@ describe('resumeSession failure recovery', () => { return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [{ content: 'existing text', role: 'user', timestamp: 1 }], session_id: 'stored-1' } as never) @@ -1071,7 +1072,7 @@ describe('branchStoredSession desktop source tagging', () => { // Parent is the currently-open (primary) chat. setSessions([storedSession({ id: 'stored-parent', message_count: 1 })]) setSelectedStoredSessionId('stored-parent') - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getAllSessionMessages).mockResolvedValue({ messages: [{ content: 'branch me', role: 'user', timestamp: 1 }], session_id: 'stored-parent' } as never) @@ -1110,7 +1111,7 @@ describe('branchStoredSession desktop source tagging', () => { }) setSessions([storedSession({ id: 'stored-parent', message_count: 1 })]) - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getAllSessionMessages).mockResolvedValue({ messages: [{ content: 'branch me', role: 'user', timestamp: 1 }], session_id: 'stored-parent' } as never) @@ -1184,7 +1185,7 @@ describe('branchStoredSession desktop source tagging', () => { it('resolves and swaps to the parent profile when the branched session is not cached', async () => { setSessions([]) vi.mocked(getSession).mockResolvedValue(storedSession({ id: 'stored-parent', message_count: 1, profile: 'work' })) - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getAllSessionMessages).mockResolvedValue({ messages: [{ content: 'branch me', role: 'user', timestamp: 1 }], session_id: 'stored-parent' } as never) @@ -1210,7 +1211,7 @@ describe('branchStoredSession desktop source tagging', () => { await expect(branchStoredSession!('stored-parent')).resolves.toBe(true) expect(ensureGatewayProfile).toHaveBeenCalledWith('work') - expect(getSessionMessages).toHaveBeenCalledWith('stored-parent', 'work') + expect(getAllSessionMessages).toHaveBeenCalledWith('stored-parent', 'work') // The create itself must carry the owning profile: in app-global remote // mode the soft gateway swap alone is not enough — an omitted profile // lands the branch on the launch (default) profile's state.db. @@ -1221,7 +1222,7 @@ describe('branchStoredSession desktop source tagging', () => { it('creates the branch on the cached parent session profile', async () => { setSessions([storedSession({ id: 'stored-parent', message_count: 1, profile: 'work' })]) - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getAllSessionMessages).mockResolvedValue({ messages: [{ content: 'branch me', role: 'user', timestamp: 1 }], session_id: 'stored-parent' } as never) @@ -1250,7 +1251,7 @@ describe('branchStoredSession desktop source tagging', () => { it('omits profile for a profile-less parent so single-profile users are unchanged', async () => { setSessions([storedSession({ id: 'stored-parent', message_count: 1 })]) - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getAllSessionMessages).mockResolvedValue({ messages: [{ content: 'branch me', role: 'user', timestamp: 1 }], session_id: 'stored-parent' } as never) @@ -1306,7 +1307,7 @@ describe('resumeSession drops a redundant tile when the session loads into main' return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ messages: [] } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [] } as never) let resume: ((storedSessionId: string, replaceRoute?: boolean) => Promise) | null = null render( (resume = r)} requestGateway={requestGateway} />) @@ -1331,7 +1332,7 @@ describe('resumeSession drops a redundant tile when the session loads into main' return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ messages: [] } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [] } as never) let resume: ((storedSessionId: string, replaceRoute?: boolean) => Promise) | null = null render( (resume = r)} requestGateway={requestGateway} />) @@ -1382,7 +1383,7 @@ describe('resumeSession warm-cache mapping integrity', () => { return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ messages: [] } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [] } as never) let resume: ((storedSessionId: string, replaceRoute?: boolean) => Promise) | null = null render( @@ -1435,7 +1436,7 @@ describe('resumeSession warm-cache mapping integrity', () => { return {} as never }) - vi.mocked(getSessionMessages).mockResolvedValue({ messages: [], session_id: 'stored-A' } as never) + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: [], session_id: 'stored-A' } as never) let resume: ((storedSessionId: string, replaceRoute?: boolean) => Promise) | null = null render( @@ -1455,7 +1456,7 @@ describe('resumeSession warm-cache mapping integrity', () => { const methods = requestGateway.mock.calls.map(([method]) => method) expect(methods).toContain('session.activate') expect(methods).not.toContain('session.resume') - expect(getSessionMessages).toHaveBeenCalledWith('stored-A', undefined) + expect(getLatestSessionMessages).toHaveBeenCalledWith('stored-A', undefined) expect(requestGateway).toHaveBeenCalledWith( 'session.activate', expect.objectContaining({ omit_messages: true, session_id: 'rt-A' }) @@ -1492,7 +1493,7 @@ describe('resumeSession warm-cache mapping integrity', () => { { content: 'It is a photo.', role: 'assistant', timestamp: 2 } ] - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: persistedMessages, session_id: 'stored-A' } as never) @@ -1529,7 +1530,7 @@ describe('resumeSession warm-cache mapping integrity', () => { await resume!('stored-A', true) expect(requestGateway.mock.calls.map(([method]) => method)).toContain('session.activate') - expect(getSessionMessages).toHaveBeenCalledWith('stored-A', undefined) + expect(getLatestSessionMessages).toHaveBeenCalledWith('stored-A', undefined) expect(resumedState?.messages[0]?.attachmentRefs).toEqual(['@image:/tmp/photo.png']) }) @@ -1566,7 +1567,7 @@ describe('resumeSession warm-cache mapping integrity', () => { { content: 'answer saved after compression', role: 'assistant', timestamp: 4 } ] - vi.mocked(getSessionMessages).mockResolvedValue({ + vi.mocked(getLatestSessionMessages).mockResolvedValue({ messages: persistedMessages, session_id: 'stored-A' } as never) diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index d83b9a2613041..857fd06d0f2cc 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -662,10 +662,19 @@ export function getSessionMessages( page: { limit?: number; offset?: number; order?: 'latest' | 'oldest' } = {} ): Promise { const query = new URLSearchParams() - if (profile) query.set('profile', profile) - if (page.limit !== undefined) query.set('limit', String(page.limit)) - if (page.offset !== undefined) query.set('offset', String(page.offset)) - if (page.order) query.set('order', page.order) + + if (profile) { + query.set('profile', profile) + } + if (page.limit !== undefined) { + query.set('limit', String(page.limit)) + } + if (page.offset !== undefined) { + query.set('offset', String(page.offset)) + } + if (page.order) { + query.set('order', page.order) + } const suffix = query.size ? `?${query.toString()}` : '' return window.hermesDesktop.api({