feat(storage): support configurable runtime output directory (#2127)
Some checks failed
Qwen Code CI / Lint (push) Failing after 12s
Qwen Code CI / Test (push) Has been skipped
Qwen Code CI / Test-1 (push) Has been skipped
Qwen Code CI / Test-2 (push) Has been skipped
Qwen Code CI / Test-3 (push) Has been skipped
Qwen Code CI / Test-4 (push) Has been skipped
Qwen Code CI / Test-5 (push) Has been skipped
Qwen Code CI / Test-6 (push) Has been skipped
Qwen Code CI / Test-7 (push) Has been skipped
Qwen Code CI / Test-8 (push) Has been skipped
Qwen Code CI / CodeQL (push) Failing after 6s
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Failing after 5s
Qwen Code CI / Post Coverage Comment (push) Has been skipped
E2E Tests / E2E Test (Linux) - sandbox:none (push) Failing after 10m36s
E2E Tests / E2E Test - macOS (push) Has been cancelled

* feat(storage): support configurable runtime output directory (#2014)

Add `advanced.runtimeOutputDir` setting and `QWEN_RUNTIME_DIR` env var
to redirect runtime output (temp files, debug logs, session data, todos,
insights) to a custom directory while keeping config files at ~/.qwen.

- Introduce `Storage.setRuntimeBaseDir()` / `getRuntimeBaseDir()` with
  tilde expansion and relative path resolution
- Add `AsyncLocalStorage`-based `runWithRuntimeBaseDir()` for concurrent
  session isolation in ACP integration
- Update all runtime path methods to use `getRuntimeBaseDir()` instead
  of `getGlobalQwenDir()` (temp, debug, ide, projects, history dirs)
- Config paths (settings, oauth, installation_id, etc.) remain pinned
  to `~/.qwen` regardless of runtime dir configuration
- Add comprehensive tests covering path resolution, env var priority,
  async context isolation, and config path stability

* fix(core/storage): 支持 Windows 风格波浪号路径

扩展 setRuntimeBaseDir 以支持 Windows 风格的波浪号路径 (~\),
使用统一的路径分割逻辑处理 Unix 和 Windows 风格的路径分隔符

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core/debugLogger): runtime base dir 变更时创建新 debug 目录

添加 ensuredDebugDirPath 追踪变量,当 runtime base dir 发生变更时,
确保在新的目录下创建 debug 子目录

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli/acp): 支持 ACP runtime output dir 配置

新增 runWithAcpRuntimeOutputDir 辅助函数,在 ACP Agent 的
loadSession 和 listSessions 操作中应用配置的 runtimeOutputDir

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* docs(vscode-ide-companion/acpConnection): 补充 this 别名的使用说明

为 self = this 的用法添加解释性注释,说明在嵌套回调中需要使用 this

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* feat(cli): add runtime output directory configuration support

* fix(core): update test to use getUserSkillsDirs method

Update storage.test.ts to call getUserSkillsDirs() instead of the
non-existent getUserSkillsDir() method. The method was renamed to
return an array of skill directories.

* fix(core/todoWrite): use path.join for cross-platform path assertion in test

Replace hardcoded forward-slash path `.qwen/todos/` with `path.join('.qwen', 'todos')` to fix Windows CI failure where paths use backslashes.

Made-with: Cursor

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
易良 2026-03-20 13:53:05 +08:00 committed by GitHub
parent 87f03cf2e9
commit fbf5ed57d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1088 additions and 171 deletions

View file

@ -14,6 +14,7 @@ import {
DEFAULT_QWEN_MODEL,
OutputFormat,
NativeLspService,
Storage,
} from '@qwen-code/qwen-code-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import type { Settings } from './settings.js';
@ -2439,3 +2440,79 @@ describe('Telemetry configuration via environment variables', () => {
expect(config.getTelemetryLogPromptsEnabled()).toBe(false);
});
});
describe('loadCliConfig runtimeOutputDir', () => {
const originalArgv = process.argv;
const originalRuntimeEnv = process.env['QWEN_RUNTIME_DIR'];
beforeEach(() => {
process.argv = ['node', 'script.js'];
Storage.setRuntimeBaseDir(null);
delete process.env['QWEN_RUNTIME_DIR'];
});
afterEach(() => {
process.argv = originalArgv;
Storage.setRuntimeBaseDir(null);
if (originalRuntimeEnv !== undefined) {
process.env['QWEN_RUNTIME_DIR'] = originalRuntimeEnv;
} else {
delete process.env['QWEN_RUNTIME_DIR'];
}
vi.unstubAllEnvs();
vi.restoreAllMocks();
});
it('should set runtime base dir from settings with absolute path', async () => {
const runtimeDir = path.resolve('custom', 'runtime');
const argv = await parseArguments();
const settings: Settings = {
advanced: { runtimeOutputDir: runtimeDir },
};
await loadCliConfig(settings, argv);
expect(Storage.getRuntimeBaseDir()).toBe(runtimeDir);
});
it('should resolve relative runtimeOutputDir against cwd', async () => {
const argv = await parseArguments();
const settings: Settings = {
advanced: { runtimeOutputDir: '.qwen' },
};
const cwd = path.resolve('workspace', 'my-project');
await loadCliConfig(settings, argv, cwd);
expect(Storage.getRuntimeBaseDir()).toBe(path.join(cwd, '.qwen'));
});
it('should not set runtime base dir when runtimeOutputDir is absent', async () => {
const argv = await parseArguments();
const settings: Settings = {};
await loadCliConfig(settings, argv);
expect(Storage.getRuntimeBaseDir()).toBe(Storage.getGlobalQwenDir());
});
it('should let QWEN_RUNTIME_DIR env var take priority over settings', async () => {
const envDir = path.resolve('from-env');
const settingsDir = path.resolve('from-settings');
process.env['QWEN_RUNTIME_DIR'] = envDir;
const argv = await parseArguments();
const settings: Settings = {
advanced: { runtimeOutputDir: settingsDir },
};
await loadCliConfig(settings, argv);
// getRuntimeBaseDir checks env var first at call time
expect(Storage.getRuntimeBaseDir()).toBe(envDir);
});
it('should reset runtime base dir on subsequent load when runtimeOutputDir is absent', async () => {
const argv = await parseArguments();
const firstRuntimeDir = path.resolve('first', 'runtime');
await loadCliConfig(
{ advanced: { runtimeOutputDir: firstRuntimeDir } },
argv,
);
expect(Storage.getRuntimeBaseDir()).toBe(firstRuntimeDir);
await loadCliConfig({}, argv);
expect(Storage.getRuntimeBaseDir()).toBe(Storage.getGlobalQwenDir());
});
});

View file

@ -708,6 +708,11 @@ export async function loadCliConfig(
): Promise<Config> {
const debugMode = isDebugMode(argv);
// Set runtime output directory from settings (env var QWEN_RUNTIME_DIR
// is auto-detected inside getRuntimeBaseDir() at each call site).
// Pass cwd so that relative paths like ".qwen" resolve per-project.
Storage.setRuntimeBaseDir(settings.advanced?.runtimeOutputDir, cwd);
const ideMode = settings.ide?.enabled ?? false;
const folderTrust = settings.security?.folderTrust?.enabled ?? false;

View file

@ -1263,6 +1263,17 @@ const SETTINGS_SCHEMA = {
description: 'Configuration for the bug report command.',
showInDialog: false,
},
runtimeOutputDir: {
type: 'string',
label: 'Runtime Output Directory',
category: 'Advanced',
requiresRestart: true,
default: undefined as string | undefined,
description:
'Custom directory for runtime output (temp files, debug logs, session data, todos, etc.). ' +
'Config files remain at ~/.qwen. Env var QWEN_RUNTIME_DIR takes priority.',
showInDialog: false,
},
tavilyApiKey: {
type: 'string',
label: 'Tavily API Key (Deprecated)',