From d03451523d7adbba4bd81aad7a2ac5020741045f Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 3 Jun 2026 04:07:17 +0800 Subject: [PATCH 1/2] fix(core): honor runtime output dir for auto memory --- packages/core/src/memory/paths.ts | 5 +- packages/core/src/memory/store.test.ts | 76 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/packages/core/src/memory/paths.ts b/packages/core/src/memory/paths.ts index 2bc8d4c813..229552898c 100644 --- a/packages/core/src/memory/paths.ts +++ b/packages/core/src/memory/paths.ts @@ -81,14 +81,15 @@ 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 Storage.getGlobalQwenDir(); + return Storage.getRuntimeBaseDir(); } // Memoize by projectRoot — findCanonicalGitRoot() walks the file system (existsSync diff --git a/packages/core/src/memory/store.test.ts b/packages/core/src/memory/store.test.ts index 0d02cf865d..264fba8474 100644 --- a/packages/core/src/memory/store.test.ts +++ b/packages/core/src/memory/store.test.ts @@ -15,6 +15,7 @@ import { getAutoMemoryMetadataPath, getAutoMemoryRoot, getAutoMemoryTopicPath, + clearAutoMemoryRootCache, } from './paths.js'; import { createDefaultAutoMemoryIndex, @@ -22,18 +23,59 @@ import { 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, @@ -63,6 +105,40 @@ 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('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('creates a complete managed auto-memory scaffold', async () => { const now = new Date('2026-04-01T08:00:00.000Z'); await ensureAutoMemoryScaffold(projectRoot, now); From 389a38983577b778a427deecb8a23478c30dad0d Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Wed, 3 Jun 2026 17:54:16 +0800 Subject: [PATCH 2/2] fix(core): key memory root cache by runtime dir --- packages/core/src/memory/paths.ts | 19 ++++---- packages/core/src/memory/store.test.ts | 66 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 8 deletions(-) diff --git a/packages/core/src/memory/paths.ts b/packages/core/src/memory/paths.ts index 229552898c..2438ec078f 100644 --- a/packages/core/src/memory/paths.ts +++ b/packages/core/src/memory/paths.ts @@ -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'; @@ -87,33 +87,36 @@ function findCanonicalGitRoot(startPath: string): string | null { */ 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.getRuntimeBaseDir(); } -// 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(); 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; } diff --git a/packages/core/src/memory/store.test.ts b/packages/core/src/memory/store.test.ts index 264fba8474..1ec8a7d5b9 100644 --- a/packages/core/src/memory/store.test.ts +++ b/packages/core/src/memory/store.test.ts @@ -121,6 +121,53 @@ describe('auto-memory storage scaffold', () => { ); }); + 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'); @@ -139,6 +186,25 @@ describe('auto-memory storage scaffold', () => { ); }); + 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);