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
24 changes: 14 additions & 10 deletions packages/core/src/memory/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { Storage } from '../config/storage.js';
import { QWEN_DIR, sanitizeCwd } from '../utils/paths.js';
import { QWEN_DIR, resolvePath, sanitizeCwd } from '../utils/paths.js';
import type { AutoMemoryType } from './types.js';

export const AUTO_MEMORY_DIRNAME = 'memory';
Expand Down Expand Up @@ -81,38 +81,42 @@ function findCanonicalGitRoot(startPath: string): string | null {

/**
* Returns the base directory for all auto-memory storage.
* Defaults to the global qwen dir (`~/.qwen` or `$QWEN_HOME`);
* Defaults to the runtime output dir (`runtimeOutputDir`, `QWEN_RUNTIME_DIR`,
* or the global qwen dir);
* overridable via QWEN_CODE_MEMORY_BASE_DIR for tests.
*/
export function getMemoryBaseDir(): string {
if (process.env['QWEN_CODE_MEMORY_BASE_DIR']) {
return process.env['QWEN_CODE_MEMORY_BASE_DIR'];
return resolvePath(undefined, process.env['QWEN_CODE_MEMORY_BASE_DIR']);
}
return Storage.getGlobalQwenDir();
return Storage.getRuntimeBaseDir();

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.

test

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.

[Critical] Changing from getGlobalQwenDir() to getRuntimeBaseDir() silently relocates auto-memory from ~/.qwen/projects/... to <runtimeOutputDir>/projects/.... There is no migration logic, no symlink, and no fallback read from the old path. Users who have advanced.runtimeOutputDir set in settings.json will experience unexplained loss of all accumulated auto-memory (user preferences, project knowledge, feedback).

Suggested fix: Add a one-time migration — when the new path doesn't exist but the old getGlobalQwenDir()-based path does, either copy/symlink the old tree or fall back to reading from the old path with a deprecation log.

— qwen3.7-max via Qwen Code /review

}

// Memoize by projectRoot — findCanonicalGitRoot() walks the file system (existsSync
// per directory) and is called from hot-path code such as schedulers and scanners.
// Memoize by projectRoot plus the runtime-specific base dir. In daemon mode,
// different sessions can share a project root while writing to different output dirs.
const _autoMemoryRootCache = new Map<string, string>();

export function getAutoMemoryRoot(projectRoot: string): string {
const cached = _autoMemoryRootCache.get(projectRoot);
const useLocalMemory = process.env['QWEN_CODE_MEMORY_LOCAL'] === '1';
const memoryBaseDir = useLocalMemory ? '' : getMemoryBaseDir();
const cacheKey = `${useLocalMemory ? 'local' : memoryBaseDir}\0${projectRoot}`;
const cached = _autoMemoryRootCache.get(cacheKey);
if (cached !== undefined) return cached;

let result: string;
if (process.env['QWEN_CODE_MEMORY_LOCAL'] === '1') {
if (useLocalMemory) {
result = path.join(projectRoot, QWEN_DIR, AUTO_MEMORY_DIRNAME);
} else {
const canonicalRoot =
findCanonicalGitRoot(projectRoot) ?? path.resolve(projectRoot);
result = path.join(
getMemoryBaseDir(),
memoryBaseDir,
'projects',
sanitizeCwd(canonicalRoot),
AUTO_MEMORY_DIRNAME,
);
}
_autoMemoryRootCache.set(projectRoot, result);
_autoMemoryRootCache.set(cacheKey, result);
return result;
}

Expand Down
142 changes: 142 additions & 0 deletions packages/core/src/memory/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,67 @@ import {
getAutoMemoryMetadataPath,
getAutoMemoryRoot,
getAutoMemoryTopicPath,
clearAutoMemoryRootCache,
} from './paths.js';
import {
createDefaultAutoMemoryIndex,
createDefaultAutoMemoryMetadata,
ensureAutoMemoryScaffold,
readAutoMemoryIndex,
} from './store.js';
import { Storage } from '../config/storage.js';
import { sanitizeCwd } from '../utils/paths.js';

const originalMemoryLocal = process.env['QWEN_CODE_MEMORY_LOCAL'];
const originalMemoryBaseDir = process.env['QWEN_CODE_MEMORY_BASE_DIR'];
const originalRuntimeDir = process.env['QWEN_RUNTIME_DIR'];

describe('auto-memory storage scaffold', () => {
let tempDir: string;
let projectRoot: string;

beforeEach(async () => {
clearAutoMemoryRootCache();
Storage.setRuntimeBaseDir(null);
if (originalMemoryLocal === undefined) {
delete process.env['QWEN_CODE_MEMORY_LOCAL'];
} else {
process.env['QWEN_CODE_MEMORY_LOCAL'] = originalMemoryLocal;
}
if (originalMemoryBaseDir === undefined) {
delete process.env['QWEN_CODE_MEMORY_BASE_DIR'];
} else {
process.env['QWEN_CODE_MEMORY_BASE_DIR'] = originalMemoryBaseDir;
}
if (originalRuntimeDir === undefined) {
delete process.env['QWEN_RUNTIME_DIR'];
} else {
process.env['QWEN_RUNTIME_DIR'] = originalRuntimeDir;
}

tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'auto-memory-'));
projectRoot = path.join(tempDir, 'project');
await fs.mkdir(projectRoot, { recursive: true });
});

afterEach(async () => {
clearAutoMemoryRootCache();
Storage.setRuntimeBaseDir(null);
if (originalMemoryLocal === undefined) {
delete process.env['QWEN_CODE_MEMORY_LOCAL'];
} else {
process.env['QWEN_CODE_MEMORY_LOCAL'] = originalMemoryLocal;
}
if (originalMemoryBaseDir === undefined) {
delete process.env['QWEN_CODE_MEMORY_BASE_DIR'];
} else {
process.env['QWEN_CODE_MEMORY_BASE_DIR'] = originalMemoryBaseDir;
}
if (originalRuntimeDir === undefined) {
delete process.env['QWEN_RUNTIME_DIR'];
} else {
process.env['QWEN_RUNTIME_DIR'] = originalRuntimeDir;
}
await fs.rm(tempDir, {
recursive: true,
force: true,
Expand Down Expand Up @@ -63,6 +105,106 @@ describe('auto-memory storage scaffold', () => {
);
});

it('uses the runtime output directory for managed auto-memory', () => {
delete process.env['QWEN_CODE_MEMORY_LOCAL'];
const runtimeDir = path.join(tempDir, 'runtime-output');
Storage.setRuntimeBaseDir(runtimeDir);
clearAutoMemoryRootCache();

expect(getAutoMemoryRoot(projectRoot)).toBe(
path.join(
runtimeDir,
'projects',
sanitizeCwd(path.resolve(projectRoot)),
'memory',
),
);
});

it('uses QWEN_RUNTIME_DIR for managed auto-memory', () => {
delete process.env['QWEN_CODE_MEMORY_LOCAL'];
const envRuntimeDir = path.join(tempDir, 'env-runtime-output');
process.env['QWEN_RUNTIME_DIR'] = envRuntimeDir;
Storage.setRuntimeBaseDir(path.join(tempDir, 'settings-runtime-output'));
clearAutoMemoryRootCache();

expect(getAutoMemoryRoot(projectRoot)).toBe(
path.join(
envRuntimeDir,
'projects',
sanitizeCwd(path.resolve(projectRoot)),
'memory',
),
);
});

it('does not reuse cached roots across runtime output dirs', () => {
delete process.env['QWEN_CODE_MEMORY_LOCAL'];
const runtimeA = path.join(tempDir, 'runtime-a');
const runtimeB = path.join(tempDir, 'runtime-b');

const rootA = Storage.runWithRuntimeBaseDir(runtimeA, undefined, () =>
getAutoMemoryRoot(projectRoot),
);
const rootB = Storage.runWithRuntimeBaseDir(runtimeB, undefined, () =>
getAutoMemoryRoot(projectRoot),
);

expect(rootA).toBe(
path.join(
runtimeA,
'projects',
sanitizeCwd(path.resolve(projectRoot)),
'memory',
),
);
expect(rootB).toBe(
path.join(
runtimeB,
'projects',
sanitizeCwd(path.resolve(projectRoot)),
'memory',
),
);
});

it('keeps QWEN_CODE_MEMORY_BASE_DIR ahead of the runtime output directory', () => {
delete process.env['QWEN_CODE_MEMORY_LOCAL'];
const memoryBaseDir = path.join(tempDir, 'memory-base');
const runtimeDir = path.join(tempDir, 'runtime-output');
process.env['QWEN_CODE_MEMORY_BASE_DIR'] = memoryBaseDir;
Storage.setRuntimeBaseDir(runtimeDir);
clearAutoMemoryRootCache();

expect(getAutoMemoryRoot(projectRoot)).toBe(
path.join(
memoryBaseDir,
'projects',
sanitizeCwd(path.resolve(projectRoot)),
'memory',
),
);
});

it('resolves QWEN_CODE_MEMORY_BASE_DIR before using it', () => {
delete process.env['QWEN_CODE_MEMORY_LOCAL'];
const memoryBaseDir = path.join(tempDir, 'relative-memory-base');
process.env['QWEN_CODE_MEMORY_BASE_DIR'] = path.relative(
process.cwd(),
memoryBaseDir,
);
clearAutoMemoryRootCache();

expect(getAutoMemoryRoot(projectRoot)).toBe(
path.join(
memoryBaseDir,
'projects',
sanitizeCwd(path.resolve(projectRoot)),
'memory',
),
);
});

it('creates a complete managed auto-memory scaffold', async () => {
const now = new Date('2026-04-01T08:00:00.000Z');
await ensureAutoMemoryScaffold(projectRoot, now);
Expand Down
Loading