Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions packages/core/src/memory/relevanceSelector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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?.({
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/memory/relevanceSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
43 changes: 43 additions & 0 deletions packages/core/src/utils/paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
isSubpath,
shortenPath,
tildeifyPath,
expandHomeDir,
getProjectHash,
_resetValidatePathCacheForTest,
} from './paths.js';
Expand Down Expand Up @@ -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'),
);
});
});
18 changes: 18 additions & 0 deletions packages/core/src/utils/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This adds expandHomeDir() as a new exported core helper, but the existing CLI implementation in packages/cli/src/ui/commands/directoryCommand.tsx is still the only production call site. That leaves the new export unused while the same path-expansion behavior now exists in two places, so future changes to ~ or %userprofile% handling can drift and the API intent is unclear.

Either remove this new helper if it is not needed for the memory-recall fix, or complete the extraction by importing the core helper from the CLI command and deleting the local duplicate implementation.

— gpt-5.5 via Qwen Code /review

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.
Expand Down
Loading