diff --git a/packages/core/src/memory/relevanceSelector.test.ts b/packages/core/src/memory/relevanceSelector.test.ts index 1dcc6a1fc86..ac94cc5071e 100644 --- a/packages/core/src/memory/relevanceSelector.test.ts +++ b/packages/core/src/memory/relevanceSelector.test.ts @@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { runSideQuery } from '../utils/sideQuery.js'; +import type { Config } from '../config/config.js'; import type { ScannedAutoMemoryDocument } from './scan.js'; import { selectRelevantAutoMemoryDocumentsByModel } from './relevanceSelector.js'; @@ -37,27 +38,29 @@ const docs: ScannedAutoMemoryDocument[] = [ ]; describe('selectRelevantAutoMemoryDocumentsByModel', () => { - const mockConfig = {} as Parameters< - typeof selectRelevantAutoMemoryDocumentsByModel - >[0]; + const mockConfig = { + getFastModel: vi.fn().mockReturnValue(undefined), + } as unknown as Config; beforeEach(() => { - vi.clearAllMocks(); + vi.resetAllMocks(); }); it('returns documents chosen by the side-query selector', async () => { vi.mocked(runSideQuery).mockResolvedValue({ - selected_memories: ['reference.md'], + selected_memories: ['user.md'], }); - const selected = await selectRelevantAutoMemoryDocumentsByModel( + const result = await selectRelevantAutoMemoryDocumentsByModel( mockConfig, - 'check the latency dashboard', + 'check preferences', docs, 2, + [], ); - expect(selected).toEqual([docs[1]]); + expect(result).toEqual([docs[0]]); + expect(runSideQuery).toHaveBeenCalledWith( mockConfig, expect.objectContaining({ @@ -126,6 +129,52 @@ describe('selectRelevantAutoMemoryDocumentsByModel', () => { ); }); + it('passes the fast model to runSideQuery when configured', async () => { + vi.mocked(mockConfig.getFastModel).mockReturnValue('fast-flash-model'); + vi.mocked(runSideQuery).mockResolvedValue({ + selected_memories: ['reference.md'], + }); + + await selectRelevantAutoMemoryDocumentsByModel( + mockConfig, + 'check the latency dashboard', + docs, + 2, + ); + + expect(runSideQuery).toHaveBeenCalledWith( + mockConfig, + expect.objectContaining({ + purpose: 'auto-memory-recall', + model: 'fast-flash-model', + config: { temperature: 0 }, + }), + ); + }); + + it('passes undefined model when no fast model is configured', async () => { + vi.mocked(mockConfig.getFastModel).mockReturnValue(undefined); + vi.mocked(runSideQuery).mockResolvedValue({ + selected_memories: ['reference.md'], + }); + + await selectRelevantAutoMemoryDocumentsByModel( + mockConfig, + 'check the latency dashboard', + docs, + 2, + ); + + expect(runSideQuery).toHaveBeenCalledWith( + mockConfig, + expect.objectContaining({ + purpose: 'auto-memory-recall', + model: undefined, + config: { temperature: 0 }, + }), + ); + }); + it('throws when selector returns unknown relative paths', async () => { vi.mocked(runSideQuery).mockImplementation(async (_config, options) => { const error = options.validate?.({ diff --git a/packages/core/src/memory/relevanceSelector.ts b/packages/core/src/memory/relevanceSelector.ts index 69f6fe7194f..2c54929ff85 100644 --- a/packages/core/src/memory/relevanceSelector.ts +++ b/packages/core/src/memory/relevanceSelector.ts @@ -92,8 +92,12 @@ export async function selectRelevantAutoMemoryDocumentsByModel( contents, schema: RESPONSE_SCHEMA, abortSignal: callerAbortSignal - ? AbortSignal.any([AbortSignal.timeout(2_000), callerAbortSignal]) - : AbortSignal.timeout(2_000), + ? AbortSignal.any([AbortSignal.timeout(1_000), callerAbortSignal]) + : AbortSignal.timeout(1_000), + + // Use the fast model for this background side-query to reduce latency and + // cost. Falls back to the main session model if no fast model is configured. + model: config.getFastModel(), systemInstruction: SELECT_MEMORIES_SYSTEM_PROMPT, config: { temperature: 0, diff --git a/packages/core/src/utils/paths.test.ts b/packages/core/src/utils/paths.test.ts index ab6c20479f3..11824de98de 100644 --- a/packages/core/src/utils/paths.test.ts +++ b/packages/core/src/utils/paths.test.ts @@ -25,6 +25,7 @@ import { isSubpath, shortenPath, tildeifyPath, + expandHomeDir, getProjectHash, _resetValidatePathCacheForTest, } from './paths.js'; @@ -911,3 +912,45 @@ describe('getProjectHash', () => { platformSpy.mockRestore(); }); }); + +describe('expandHomeDir', () => { + const homeDir = os.homedir(); + + it('should return empty string for empty input', () => { + expect(expandHomeDir('')).toBe(''); + }); + + it('should expand ~ to home directory', () => { + expect(expandHomeDir('~')).toBe(path.normalize(homeDir)); + }); + + it('should expand ~/path to home directory path', () => { + expect(expandHomeDir('~/documents')).toBe(path.join(homeDir, 'documents')); + }); + + it('should not expand ~path (no slash)', () => { + expect(expandHomeDir('~documents')).toBe('~documents'); + }); + + it('should expand %userprofile% (case-insensitive) to home directory', () => { + expect(expandHomeDir('%userprofile%')).toBe(path.normalize(homeDir)); + expect(expandHomeDir('%USERPROFILE%')).toBe(path.normalize(homeDir)); + }); + + it('should expand %userprofile%\\path to home directory path', () => { + const result = expandHomeDir('%userprofile%\\documents'); + expect(result).toBe(path.normalize(homeDir + '\\documents')); + }); + + it('should return regular absolute path unchanged (but normalized)', () => { + expect(expandHomeDir('/absolute/path')).toBe( + path.normalize('/absolute/path'), + ); + }); + + it('should return relative path unchanged (but normalized)', () => { + expect(expandHomeDir('relative/path')).toBe( + path.normalize('relative/path'), + ); + }); +}); diff --git a/packages/core/src/utils/paths.ts b/packages/core/src/utils/paths.ts index ef858cdfa3f..e11fa0b3ffc 100644 --- a/packages/core/src/utils/paths.ts +++ b/packages/core/src/utils/paths.ts @@ -75,6 +75,24 @@ export function tildeifyPath(path: string): string { return path; } +/** + * Expands tilde (~) and Windows-style %userprofile% to the full home directory path. + * @param p - The path to expand. + * @returns The expanded path. + */ +export function expandHomeDir(p: string): string { + if (!p) { + return ''; + } + let expandedPath = p; + if (p.toLowerCase().startsWith('%userprofile%')) { + expandedPath = os.homedir() + p.substring('%userprofile%'.length); + } else if (p === '~' || p.startsWith('~/')) { + expandedPath = os.homedir() + p.substring(1); + } + return path.normalize(expandedPath); +} + /** * Shortens a path string if it exceeds maxLen, prioritizing the start and end segments. * Shows root + first segment + "..." + end segments when middle segments are omitted.