Merge pull request #89833 from NousResearch/bb/scope-session-lookup
fix(desktop): scope session lookup to the active profile (supersedes #79522)
This commit is contained in:
+18
-18
@@ -62,31 +62,33 @@ describe('resolveStoredSession profile ownership', () => {
|
||||
it('treats a profile-less cache hit as unresolved when multiple profiles exist', async () => {
|
||||
$sessions.set([session({ id: 's1' })])
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockResolvedValueOnce(session({ id: 's1', profile: 'default' }))
|
||||
|
||||
const resolved = await resolveStoredSession('s1')
|
||||
|
||||
expect(resolved?.profile).toBe('default')
|
||||
// rung 2 (bare) then rung 3 (stamped probes: active profile first)
|
||||
expect(mockGetSession).toHaveBeenNthCalledWith(1, 's1')
|
||||
expect(mockGetSession).toHaveBeenNthCalledWith(2, 's1', 'meta')
|
||||
expect(mockGetSession).toHaveBeenNthCalledWith(3, 's1', 'default')
|
||||
// rung 2 (active profile) then rung 3 (stamped cross-profile probe)
|
||||
expect(mockGetSession).toHaveBeenNthCalledWith(1, 's1', 'meta')
|
||||
expect(mockGetSession).toHaveBeenNthCalledWith(2, 's1', 'default')
|
||||
})
|
||||
|
||||
it('probes the ACTIVE profile when the unscoped lookup 404s (hidden Bot Mode chat)', async () => {
|
||||
// A hidden Bot Mode canonical chat is never in the sidebar cache, and the
|
||||
// unscoped GET routes to the PRIMARY backend (404). The owner is the
|
||||
// active profile itself — it must be probed, not skipped.
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockResolvedValueOnce(session({ id: 's1', message_count: 2 }))
|
||||
it('scopes the first by-id lookup so a miss does not skip the active profile', async () => {
|
||||
$activeGatewayProfile.set('brain')
|
||||
$profiles.set(profiles('default', 'brain'))
|
||||
mockGetSession.mockImplementation(async (id, profile) => {
|
||||
if (profile === 'brain') {
|
||||
return session({ id, profile: 'brain' })
|
||||
}
|
||||
|
||||
throw new Error('404: Session not found')
|
||||
})
|
||||
|
||||
const resolved = await resolveStoredSession('s1')
|
||||
|
||||
expect(resolved?.profile).toBe('meta')
|
||||
expect(mockGetSession).toHaveBeenNthCalledWith(1, 's1')
|
||||
expect(mockGetSession).toHaveBeenNthCalledWith(2, 's1', 'meta')
|
||||
expect($sessions.get().find(s => s.id === 's1')?.profile).toBe('meta')
|
||||
expect(resolved?.profile).toBe('brain')
|
||||
expect(mockGetSession).toHaveBeenCalledWith('s1', 'brain')
|
||||
expect(mockGetSession).not.toHaveBeenCalledWith('s1')
|
||||
expect(mockGetSession).not.toHaveBeenCalledWith('s1', 'default')
|
||||
})
|
||||
|
||||
it('accepts a profile-less cache hit for single-profile users', async () => {
|
||||
@@ -105,6 +107,7 @@ describe('resolveStoredSession profile ownership', () => {
|
||||
const resolved = await resolveStoredSession('s1')
|
||||
|
||||
expect(resolved?.profile).toBe('meta')
|
||||
expect(mockGetSession).toHaveBeenCalledWith('s1', 'meta')
|
||||
// the upserted cache row is owned too, so the next hit short-circuits
|
||||
expect($sessions.get().find(s => s.id === 's1')?.profile).toBe('meta')
|
||||
})
|
||||
@@ -113,7 +116,6 @@ describe('resolveStoredSession profile ownership', () => {
|
||||
// Per-profile remote override: Electron strips the desktop alias before
|
||||
// forwarding, so the standalone backend stamps its backend-local root.
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockResolvedValueOnce(session({ id: 's1', profile: 'default' }))
|
||||
$activeGatewayProfile.set('default')
|
||||
$profiles.set(profiles('default', 'meta'))
|
||||
@@ -125,7 +127,6 @@ describe('resolveStoredSession profile ownership', () => {
|
||||
})
|
||||
|
||||
it('stamps the probed profile on a scoped hit from an older backend that omits it', async () => {
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockResolvedValueOnce(session({ id: 's1' }))
|
||||
|
||||
@@ -137,7 +138,6 @@ describe('resolveStoredSession profile ownership', () => {
|
||||
})
|
||||
|
||||
it('resolveSessionProfile routes a default-profile session from a non-default gateway', async () => {
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockRejectedValueOnce(new Error('404: Session not found'))
|
||||
mockGetSession.mockResolvedValueOnce(session({ id: 's1', profile: 'default' }))
|
||||
|
||||
|
||||
@@ -1307,17 +1307,19 @@ export async function resolveStoredSession(storedSessionId: string): Promise<Ses
|
||||
return cached
|
||||
}
|
||||
|
||||
// Direct by-id on the live backend — one row lookup, no list scan. Covers
|
||||
// single-profile users and any id on the active profile (e.g. an old session
|
||||
// past the sidebar's recent window). 404 just means it's not on this profile.
|
||||
try {
|
||||
const session = await getSession(storedSessionId)
|
||||
// Direct by-id on the active profile — one row lookup, no list scan. Electron
|
||||
// routes an unscoped GET to the primary backend, which may not own the
|
||||
// active profile. A 404 there used to skip that profile in the probes below,
|
||||
// so the session was never found.
|
||||
const activeKey = normalizeProfileKey($activeGatewayProfile.get())
|
||||
|
||||
// Older backends omit `profile` on unscoped GETs; the serving backend is
|
||||
// the active gateway's, so back-fill that rather than caching an unowned
|
||||
// row. A present stamp is preserved: in app-global remote mode a bare hit
|
||||
// can legitimately carry another profile's row (see the branch tests).
|
||||
session.profile ||= normalizeProfileKey($activeGatewayProfile.get())
|
||||
try {
|
||||
const session = await getSession(storedSessionId, activeKey)
|
||||
|
||||
// Older backends can omit `profile`; this request targeted the active
|
||||
// profile, so back-fill that rather than caching an unowned row. A present
|
||||
// stamp is preserved for backend compatibility.
|
||||
session.profile ||= activeKey
|
||||
|
||||
upsertResolvedSession(session, storedSessionId)
|
||||
|
||||
@@ -1326,30 +1328,16 @@ export async function resolveStoredSession(storedSessionId: string): Promise<Ses
|
||||
// Not on the active profile — fall through to the cross-profile probe.
|
||||
}
|
||||
|
||||
// Multi-profile only: probe each profile by id (still one cheap lookup
|
||||
// each) rather than pulling every profile's recent sessions. The first hit
|
||||
// carries its owning `profile`, which routes the resume to the right backend.
|
||||
//
|
||||
// The ACTIVE profile is probed too, and first. The unscoped rung above does
|
||||
// NOT cover it: Electron routes a profile-less /api/sessions GET to the
|
||||
// PRIMARY backend, not the active gateway's, so a session owned by the
|
||||
// active non-primary profile 404s there. Skipping the active key then left
|
||||
// it as the ONE profile never probed — a hidden Bot Mode chat (never in the
|
||||
// sidebar cache) owned by the focused bot resolved to undefined on every
|
||||
// switch, the transcript prefetch went unscoped to the primary, and the
|
||||
// thread painted empty until an explicitly-profiled row (right-click →
|
||||
// Sessions) seeded the cache.
|
||||
const activeKey = normalizeProfileKey($activeGatewayProfile.get())
|
||||
// Multi-profile only: probe each remaining profile by id (still one cheap
|
||||
// lookup each) rather than pulling every profile's recent sessions. The
|
||||
// first hit carries its owning `profile`, which routes the resume to the
|
||||
// right backend. The active profile was already tried above.
|
||||
const otherProfiles = $profiles
|
||||
.get()
|
||||
.map(profile => normalizeProfileKey(profile.name))
|
||||
.filter(key => key !== activeKey)
|
||||
|
||||
const probeProfiles = [
|
||||
activeKey,
|
||||
...$profiles
|
||||
.get()
|
||||
.map(profile => normalizeProfileKey(profile.name))
|
||||
.filter(key => key !== activeKey)
|
||||
]
|
||||
|
||||
for (const profile of probeProfiles) {
|
||||
for (const profile of otherProfiles) {
|
||||
try {
|
||||
const session = await getSession(storedSessionId, profile)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user