mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-24 16:54:43 +00:00
Merge pull request #69887 from NousResearch/bb/tile-resume-profile
fix(desktop): route tiled session resumes to the owning profile
This commit is contained in:
commit
61e7db2820
2 changed files with 114 additions and 2 deletions
|
|
@ -0,0 +1,100 @@
|
|||
import { renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type * as HermesModule from '@/hermes'
|
||||
import { setSessions } from '@/store/session'
|
||||
import { sessionTileDelegate } from '@/store/session-states'
|
||||
import type { SessionInfo } from '@/types/hermes'
|
||||
|
||||
import { useSessionTileDelegate } from './use-session-tile-delegate'
|
||||
|
||||
vi.mock('@/hermes', async importActual => ({
|
||||
...(await importActual<typeof HermesModule>()),
|
||||
getSessionMessages: vi.fn(async () => ({ messages: [], session_id: '' }))
|
||||
}))
|
||||
|
||||
const { getSessionMessages } = await import('@/hermes')
|
||||
|
||||
const row = (over: Partial<SessionInfo>): SessionInfo =>
|
||||
({
|
||||
ended_at: null,
|
||||
id: 'live',
|
||||
input_tokens: 0,
|
||||
is_active: false,
|
||||
last_active: 0,
|
||||
message_count: 1,
|
||||
model: null,
|
||||
output_tokens: 0,
|
||||
preview: null,
|
||||
profile: 'default',
|
||||
source: null,
|
||||
started_at: 0,
|
||||
title: null,
|
||||
...over
|
||||
}) as SessionInfo
|
||||
|
||||
function renderTile(requestGateway: ReturnType<typeof vi.fn>) {
|
||||
renderHook(() =>
|
||||
useSessionTileDelegate({
|
||||
archiveSession: vi.fn(async () => undefined),
|
||||
branchStoredSession: vi.fn(async () => undefined),
|
||||
executeSlashCommand: vi.fn(async () => undefined) as never,
|
||||
removeSession: vi.fn(async () => undefined),
|
||||
requestGateway: requestGateway as never,
|
||||
runtimeIdByStoredSessionIdRef: { current: new Map() },
|
||||
sessionStateByRuntimeIdRef: { current: new Map() },
|
||||
updateSessionState: vi.fn()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
describe('useSessionTileDelegate resumeTile', () => {
|
||||
beforeEach(() => {
|
||||
setSessions([])
|
||||
vi.mocked(getSessionMessages).mockClear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
setSessions([])
|
||||
})
|
||||
|
||||
it('carries the owning profile into a cold tile resume so it cannot fork profiles', async () => {
|
||||
// A tile opens a session owned by another profile. Resuming without the
|
||||
// profile lets the gateway fall back to the launch-profile DB and clone the
|
||||
// conversation into the wrong profile (#67603). The owning profile must ride
|
||||
// both the transcript prefetch and the resume RPC.
|
||||
setSessions([row({ id: 'stored-x', profile: 'ai-engineer' })])
|
||||
|
||||
const requestGateway = vi.fn(async (method: string) =>
|
||||
method === 'session.resume' ? ({ session_id: 'runtime-1' } as never) : ({} as never)
|
||||
)
|
||||
|
||||
renderTile(requestGateway)
|
||||
const runtimeId = await sessionTileDelegate()!.resumeTile('stored-x')
|
||||
|
||||
expect(runtimeId).toBe('runtime-1')
|
||||
expect(getSessionMessages).toHaveBeenCalledWith('stored-x', 'ai-engineer')
|
||||
expect(requestGateway).toHaveBeenCalledWith('session.resume', {
|
||||
session_id: 'stored-x',
|
||||
cols: 96,
|
||||
profile: 'ai-engineer'
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves and carries a default-profile session explicitly', async () => {
|
||||
setSessions([row({ id: 'stored-y', profile: 'default' })])
|
||||
|
||||
const requestGateway = vi.fn(async (method: string) =>
|
||||
method === 'session.resume' ? ({ session_id: 'runtime-2' } as never) : ({} as never)
|
||||
)
|
||||
|
||||
renderTile(requestGateway)
|
||||
await sessionTileDelegate()!.resumeTile('stored-y')
|
||||
|
||||
expect(requestGateway).toHaveBeenCalledWith('session.resume', {
|
||||
session_id: 'stored-y',
|
||||
cols: 96,
|
||||
profile: 'default'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -6,6 +6,7 @@ import { publishSessionState, setSessionTileDelegate } from '@/store/session-sta
|
|||
import type { SessionResumeResponse } from '@/types/hermes'
|
||||
|
||||
import type { usePromptActions } from '../../session/hooks/use-prompt-actions'
|
||||
import { resolveSessionProfile } from '../../session/hooks/use-session-actions/utils'
|
||||
import type { useSessionStateCache } from '../../session/hooks/use-session-state-cache'
|
||||
import type { GatewayRequester } from '../types'
|
||||
|
||||
|
|
@ -66,9 +67,20 @@ export function useSessionTileDelegate({
|
|||
return existing
|
||||
}
|
||||
|
||||
// Resolve the owning profile before binding a runtime. A tile can open a
|
||||
// session from any profile, not just the active one; resuming (or
|
||||
// reading messages) without a profile lets the gateway fall back to the
|
||||
// launch-profile DB and fork the conversation into the wrong profile —
|
||||
// the same cross-profile bleed the recovery resumes had (#67603).
|
||||
const profile = await resolveSessionProfile(storedSessionId)
|
||||
|
||||
const [prefetch, resumed] = await Promise.all([
|
||||
getSessionMessages(storedSessionId).catch(() => null),
|
||||
requestGateway<SessionResumeResponse>('session.resume', { session_id: storedSessionId, cols: 96 })
|
||||
getSessionMessages(storedSessionId, profile).catch(() => null),
|
||||
requestGateway<SessionResumeResponse>('session.resume', {
|
||||
session_id: storedSessionId,
|
||||
cols: 96,
|
||||
...(profile ? { profile } : {})
|
||||
})
|
||||
])
|
||||
|
||||
const runtimeId = resumed?.session_id
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue