Skip to content
2 changes: 1 addition & 1 deletion docs/design/auto-memory/memory-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Managed Auto-Memory 是一套在 AI 会话过程中**自动**积累、整合和
>
> - `QWEN_CODE_MEMORY_BASE_DIR`:替换全局基础目录
> - `QWEN_CODE_MEMORY_LOCAL=1`:改用项目内路径 `.qwen/memory/`
> - `QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace`:按精确 workspace 目录分区项目记忆(默认 `git-root` 按 Git 根目录共享)。取值会做 trim / 小写归一,无法识别的值会告警一次并回退到 `git-root`。
> - `QWEN_CODE_MEMORY_PROJECT_SCOPE=workspace`:按精确 workspace 目录分区项目记忆。`qwen serve` 未显式设置或值为空白时会注入 `workspace`;standalone CLI 仍默认按 Git 根目录共享。非空取值会做 trim / 小写归一,无法识别的值会告警一次并回退到 `git-root`。
> - 团队记忆(`getTeamAutoMemoryRoot`)仍按 Git 根目录分区:同一 checkout 内的嵌套 workspace 仍共享团队记忆——团队记忆本就应跨 workspace 共享,不随本开关改变。
> - 切换 scope 不做迁移:切到 `workspace` 后,此前写在 git-root key 下的项目记忆会“失联”(切回去则看不到 workspace key 下新写的内容)。
> - 目录 key 由 `sanitizeCwd` 生成(非字母数字字符替换为 `-`),仅在标点上不同的兄弟目录(如 `feature_1` 与 `feature-1`)会映射到同一记忆目录;`workspace` 分区下这类命名会共享记忆,命名时需避开。
Expand Down
34 changes: 18 additions & 16 deletions docs/developers/daemon/17-configuration.md

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions docs/users/qwen-serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ Notes:
| `--max-total-sessions <n>` | derived | Optional non-negative integer daemon-wide cap on fresh session creation across all registered workspace runtimes. It applies to new child sessions, session restore, and branch/fork-created sessions; attaching to an existing live session does not consume a slot. Set to `0` for unlimited. When omitted with several startup/restored workspaces, the daemon derives a fixed cap from the per-workspace limit and the startup workspace count; later dynamic registration does not recompute it. |
| `--max-pending-prompts-per-session <n>` | `5` | Per-session cap on prompts accepted by `POST /session/:id/prompt` but not yet settled, including queued prompts and the active prompt. The bridge rejects overflow synchronously with `503`, `Retry-After: 5`, and `code: "prompt_queue_full"` before returning a `promptId`. Set to `0` to disable. `branchSession` serializes on the same FIFO but does not count against this prompt cap. |
| `--workspace <path>` | `process.cwd()` | Absolute workspace directory registered by this daemon. Repeat the flag to host multiple workspaces in one process; the first is primary and remains the default when a request omits `cwd`. Relative values are rejected. Session requests whose canonical `cwd` is not registered return `400 workspace_mismatch`. |
| `--memory-project-scope <mode>` | `git-root` | Project-memory partitioning mode. `git-root` (default) shares memory among workspaces resolved to the same Git root; `workspace` keys memory by the exact registered workspace directory so each daemon workspace gets its own isolated memory. Overrides `QWEN_CODE_MEMORY_PROJECT_SCOPE` when provided; an unrecognized env value is ignored with a one-time warning and falls back to `git-root`. Switching to `workspace` does not migrate existing git-root project memory — those entries stop being visible until you switch back. |
| `--memory-project-scope <mode>` | `workspace` | Project-memory partitioning mode. `workspace` (default) keys memory by the exact registered workspace directory so each daemon workspace gets its own isolated memory; `git-root` is the legacy compatibility mode shared by workspaces resolved to the same Git root. Overrides `QWEN_CODE_MEMORY_PROJECT_SCOPE` when provided; a blank env value is treated as unset, while an unrecognized non-empty value is ignored with a one-time warning and retains the legacy `git-root` behavior. The new default does not migrate existing git-root project memory — use an explicit `git-root` scope to read those entries during migration. |
| `--channel <name\|all>` | — | Experimental daemon-managed channel worker. Repeat the flag to select multiple configured channels, or pass `all` to start every configured channel. `all` cannot be combined with named channels. Selected channel `cwd` values must resolve to a registered workspace; a multi-workspace daemon runs one worker per owning workspace. The worker is owned by `qwen serve`; stop the daemon to stop serve-managed channels. |
| `--max-connections <n>` | `256` | Listener-level TCP connection cap (`server.maxConnections`). Bounds raw socket count irrespective of session count — slow / phantom SSE clients get rejected at accept time once full. Raise alongside `--max-sessions` if your deployment expects many SSE subscribers per session. |
| `--memory-budget-mb <n>` | 50% of cgroup/host | Total memory budget in MB for the whole daemon process tree. When unset, derived as 50% of the cgroup limit or host memory; either way the effective value is capped at resolved available memory, and both the configured and effective figures are reported. Currently observation only — it does not change how any `qwen --acp` child is sized. Resolved figures appear under `limits.memory` in `GET /daemon/status`, alongside registered and live child counts and advisory per-child shares under `runtime.memory`. A host too small for the minimum reports `insufficientMemory` rather than being clamped upward; because the derived fraction is 50%, any host under ~2 GB trips this. Pass an explicit `--memory-budget-mb 1024` on such a host to override the derived figure (the flag still requires at least 1024 MB of available memory to clear the warning). Must be an integer in `[1024, 1048576]`. |
Expand All @@ -417,12 +417,12 @@ Notes:

> **Memory project scope caveats.**
>
> - **Daemon vs. standalone CLI.** `--memory-project-scope` (and
> `QWEN_CODE_MEMORY_PROJECT_SCOPE`) only affects daemon-managed runtimes.
> A standalone `qwen` TUI started in the same directory still uses the
> git-root scope unless the environment variable is exported globally. To keep
> both entry points consistent, pin the scope in the workspace `.env` or
> `settings.env` so every process that reads the workspace agrees.
> - **Daemon vs. standalone CLI.** The flag or daemon launch environment sets
> one frozen scope for every runtime owned by that daemon. A workspace `.env`
> or `settings.env` cannot override it for one registered workspace. A
> standalone `qwen` TUI still defaults to git-root scope; to keep both entry
> points consistent, export `QWEN_CODE_MEMORY_PROJECT_SCOPE` in the shell or
> service environment that launches them.
> - **Directory-name collisions.** The storage key is derived by
> `sanitizeCwd`, which replaces every non-alphanumeric character with
> `-`. Sibling directories that differ only in punctuation (e.g.
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/commands/serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,12 +586,10 @@ describe('serve rate limit env parsing', () => {
webShellMounted: false,
});

await startServeHandlerWithArgs(
'--no-web --memory-project-scope workspace',
);
await startServeHandlerWithArgs('--no-web --memory-project-scope git-root');

expect(mockRunQwenServe).toHaveBeenCalledWith(
expect.objectContaining({ memoryProjectScope: 'workspace' }),
expect.objectContaining({ memoryProjectScope: 'git-root' }),
);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ export const serveCommand: CommandModule<unknown, ServeArgs> = {
choices: MEMORY_PROJECT_SCOPES,
description:
'Choose how project memory is partitioned. ' +
'"git-root" preserves the legacy shared scope; "workspace" keeps each daemon workspace isolated. ' +
'Defaults to "workspace" so each daemon workspace stays isolated; "git-root" preserves the legacy shared scope. ' +
Comment thread
qqqys marked this conversation as resolved.
Resolved
'Overrides QWEN_CODE_MEMORY_PROJECT_SCOPE when provided.',
})
.option('max-connections', {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/config/shared-env-keys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ describe('PROJECT_ENV_HARDCODED_EXCLUSIONS', () => {
);
});

it('keeps daemon memory scope operator-owned', () => {
expect(PROJECT_ENV_HARDCODED_EXCLUSIONS).toContain(
'QWEN_CODE_MEMORY_PROJECT_SCOPE',
);
});

it('excludes attribution markers so a project .env cannot spoof channel', () => {
expect(PROJECT_ENV_HARDCODED_EXCLUSIONS).toContain('QWEN_CODE_SERVE');
expect(PROJECT_ENV_HARDCODED_EXCLUSIONS).toContain('QWEN_CODE_DESKTOP');
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/config/shared-env-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export const PROJECT_ENV_HARDCODED_EXCLUSIONS = [
// This is an operator rollout policy. A project must not be able to promote
// itself from the default shadow cohort into warning or enforcement.
ENV_ACP_REPEATED_TOOL_FAILURE_GUARD,
// Project memory routing is frozen daemon-wide before workspace env files
// load, so only the operator's launch environment or CLI flag may set it.
'QWEN_CODE_MEMORY_PROJECT_SCOPE',
// QWEN_TLS_INSECURE (and NODE_TLS_REJECT_UNAUTHORIZED, which it mirrors)
// disable TLS certificate verification for all outbound API connections. A
// project `.env` must never enable either — that would let an untrusted repo
Expand Down
64 changes: 59 additions & 5 deletions packages/cli/src/serve/run-qwen-serve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1237,7 +1237,9 @@ describe('runQwenServe daemon logger wiring', () => {

// Point daemon logger at our temp debug dir
const origEnv = process.env['QWEN_RUNTIME_DIR'];
const originalScope = process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
process.env['QWEN_RUNTIME_DIR'] = tmpDir;
delete process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];

try {
const handle = await runQwenServe(
Expand Down Expand Up @@ -1271,6 +1273,10 @@ describe('runQwenServe daemon logger wiring', () => {
expect(logContent).toContain(
`workspace=${fs.realpathSync.native(workspace)}`,
);
expect(logContent).toContain('project memory scope resolved');
expect(logContent).toContain('projectMemoryScope=workspace');
expect(logContent).toContain('projectMemoryScopeSource=default');
Comment on lines +1276 to +1278

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复 + 验证证据:65139068f8;在外部导出 QWEN_CODE_MEMORY_PROJECT_SCOPE=git-root 时,定向日志测试 1/1 通过;完整 run-qwen-serve.test.ts 246/246 通过。

expect(logContent).toContain('projectMemoryScopeRaw=workspace');

await Promise.all(
Array.from({ length: 70 }, (_, index) =>
Expand Down Expand Up @@ -1303,6 +1309,11 @@ describe('runQwenServe daemon logger wiring', () => {
if (origEnv !== undefined) {
process.env['QWEN_RUNTIME_DIR'] = origEnv;
}
if (originalScope === undefined) {
delete process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
} else {
process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'] = originalScope;
}
}
}, 10_000);
});
Expand Down Expand Up @@ -3868,7 +3879,44 @@ describe('runQwenServe runtime startup failures', () => {
}
});

it('applies memoryProjectScope to every runtime without mutating process.env', async () => {
it.each([
[
'defaults every runtime to workspace project-memory scope',
undefined,
undefined,
'workspace',
],
[
'applies memoryProjectScope to every runtime without mutating process.env',
'workspace',
'git-root',
'git-root',
],
[
'preserves the launch environment scope when the option is omitted',
'git-root',
undefined,
'git-root',
],
Comment thread
qqqys marked this conversation as resolved.
Resolved
[
'treats a blank launch environment scope as unset',
'',
undefined,
'workspace',
],
[
'treats a whitespace-only launch environment scope as unset',
' ',
undefined,
'workspace',
],
[
'passes an unrecognized launch environment scope through unchanged',
'workspce',
undefined,
'workspce',
],
] as const)('%s', async (_name, launchScope, optionScope, expectedScope) => {
tmpDir = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'qws-memory-project-scope-')),
);
Expand All @@ -3877,7 +3925,11 @@ describe('runQwenServe runtime startup failures', () => {
fs.mkdirSync(primary);
fs.mkdirSync(secondary);
const originalScope = process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'] = 'workspace';
if (launchScope === undefined) {
delete process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
} else {
process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'] = launchScope;
}
vi.spyOn(qwenCore, 'resolveTelemetrySettings').mockResolvedValue({
enabled: false,
sensitiveSpanAttributeMaxLength: 1024 * 1024,
Expand Down Expand Up @@ -3915,7 +3967,9 @@ describe('runQwenServe runtime startup failures', () => {
hostname: '127.0.0.1',
mode: 'http-bridge',
workspace: [primary, secondary],
memoryProjectScope: 'git-root',
...(optionScope === undefined
? {}
: { memoryProjectScope: optionScope }),
maxSessions: 1,
serveWebShell: false,
},
Expand All @@ -3928,9 +3982,9 @@ describe('runQwenServe runtime startup failures', () => {
for (const runtime of workspaceRegistry?.list() ?? []) {
expect(
runtime.env.effectiveEnv?.['QWEN_CODE_MEMORY_PROJECT_SCOPE'],
).toBe('git-root');
).toBe(expectedScope);
}
expect(process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE']).toBe('workspace');
expect(process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE']).toBe(launchScope);
} finally {
if (originalScope === undefined) {
delete process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'];
Expand Down
Loading
Loading