From 31e3061d9bb03956fe198c60070768dd5c79b5bb Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:09:28 +0800 Subject: [PATCH 1/5] refactor(serve): default project memory to workspace scope --- docs/design/auto-memory/memory-system.md | 2 +- docs/developers/daemon/17-configuration.md | 30 ++++++++-------- docs/users/qwen-serve.md | 2 +- packages/cli/src/commands/serve.ts | 2 +- packages/cli/src/serve/run-qwen-serve.test.ts | 35 ++++++++++++++++--- packages/cli/src/serve/run-qwen-serve.ts | 9 +++-- packages/cli/src/serve/types.ts | 3 +- 7 files changed, 54 insertions(+), 29 deletions(-) diff --git a/docs/design/auto-memory/memory-system.md b/docs/design/auto-memory/memory-system.md index 93b63f84d6b..f3ebc50cd51 100644 --- a/docs/design/auto-memory/memory-system.md +++ b/docs/design/auto-memory/memory-system.md @@ -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` 分区下这类命名会共享记忆,命名时需避开。 diff --git a/docs/developers/daemon/17-configuration.md b/docs/developers/daemon/17-configuration.md index d42ca257c3a..c3d6bed3b6f 100644 --- a/docs/developers/daemon/17-configuration.md +++ b/docs/developers/daemon/17-configuration.md @@ -13,7 +13,7 @@ This page collects every setting that affects the `qwen serve` daemon and its ad | `--token ` | string | env | Bearer token. Overrides `QWEN_SERVER_TOKEN` and is trimmed at boot. It appears in the process command line, so prefer env in deployments. | | `--require-auth` | boolean | `false` | Extends bearer auth to loopback and `/health`; boot refuses to start without a token. | | `--workspace ` | absolute path / repeatable | `process.cwd()` | Startup workspace runtime; repeat to register additional isolated runtimes. The first is primary. Every value must be absolute and a directory; canonicalized at boot. | -| `--memory-project-scope ` | `git-root` / `workspace` | `git-root` | Project-memory partitioning. `git-root` shares memory among workspaces at the same Git root; `workspace` isolates by exact workspace directory. Overrides `QWEN_CODE_MEMORY_PROJECT_SCOPE`. | +| `--memory-project-scope ` | `git-root` / `workspace` | `workspace` | Project-memory partitioning. `workspace` isolates by exact workspace directory; `git-root` is the legacy compatibility scope shared by workspaces at the same Git root. Overrides `QWEN_CODE_MEMORY_PROJECT_SCOPE`. | | `--max-sessions ` | number | `32` | Per-workspace active session cap. `0` / `Infinity` means unlimited; `NaN` / negative values throw. | | `--max-total-sessions ` | number | derived for multiple startup/restored workspaces | Daemon-wide active session cap. When omitted, a finite default is derived once from the per-workspace cap and startup/restored workspace count. `0` / `Infinity` means unlimited. | | `--max-pending-prompts-per-session ` | number | `5` | Accepted but pending/running prompt cap per session. Excess prompt returns 503. `0` / `Infinity` means unlimited; negative or non-integer values throw. | @@ -50,19 +50,19 @@ This page collects every setting that affects the `qwen serve` daemon and its ad ### Read by `runQwenServe` / Express middleware -| Env | Effect | -| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `QWEN_SERVER_TOKEN` | Bearer token; trimmed at boot. | -| `QWEN_SERVE_DEBUG` | `1` / `true` / `on` / `yes` (case-insensitive) enables verbose stderr logs. See [`19-observability.md`](./19-observability.md). | -| `QWEN_SERVE_NO_MCP_POOL` | `1` disables the workspace MCP transport pool and falls back to per-session `McpClientManager`; capabilities stop advertising `mcp_workspace_pool` / `mcp_pool_restart`. | -| `QWEN_SERVE_PROMPT_DEADLINE_MS` | Env fallback for `--prompt-deadline-ms`. | -| `QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS` | Env fallback for `--writer-idle-timeout-ms`. | -| `QWEN_SERVE_RATE_LIMIT` | `1` / `true` enables per-tier HTTP rate limiting; CLI `--rate-limit` / `--no-rate-limit` wins. | -| `QWEN_SERVE_RATE_LIMIT_PROMPT` | Env fallback for `--rate-limit-prompt`. | -| `QWEN_SERVE_RATE_LIMIT_MUTATION` | Env fallback for `--rate-limit-mutation`. | -| `QWEN_SERVE_RATE_LIMIT_READ` | Env fallback for `--rate-limit-read`. | -| `QWEN_SERVE_RATE_LIMIT_WINDOW_MS` | Env fallback for `--rate-limit-window-ms`. | -| `QWEN_CODE_MEMORY_PROJECT_SCOPE` | `workspace` keys project memory by the exact workspace dir; any other value keeps the `git-root` scope (unrecognized values warn once). Propagates via the runtime base env, not `childEnvOverrides`; `--memory-project-scope` wins. Each workspace remember/forget/dream lane caps pending tasks at `MAX_PENDING = 16`; N workspaces allow up to 16·N queued tasks with no daemon-wide cap. | +| Env | Effect | +| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `QWEN_SERVER_TOKEN` | Bearer token; trimmed at boot. | +| `QWEN_SERVE_DEBUG` | `1` / `true` / `on` / `yes` (case-insensitive) enables verbose stderr logs. See [`19-observability.md`](./19-observability.md). | +| `QWEN_SERVE_NO_MCP_POOL` | `1` disables the workspace MCP transport pool and falls back to per-session `McpClientManager`; capabilities stop advertising `mcp_workspace_pool` / `mcp_pool_restart`. | +| `QWEN_SERVE_PROMPT_DEADLINE_MS` | Env fallback for `--prompt-deadline-ms`. | +| `QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS` | Env fallback for `--writer-idle-timeout-ms`. | +| `QWEN_SERVE_RATE_LIMIT` | `1` / `true` enables per-tier HTTP rate limiting; CLI `--rate-limit` / `--no-rate-limit` wins. | +| `QWEN_SERVE_RATE_LIMIT_PROMPT` | Env fallback for `--rate-limit-prompt`. | +| `QWEN_SERVE_RATE_LIMIT_MUTATION` | Env fallback for `--rate-limit-mutation`. | +| `QWEN_SERVE_RATE_LIMIT_READ` | Env fallback for `--rate-limit-read`. | +| `QWEN_SERVE_RATE_LIMIT_WINDOW_MS` | Env fallback for `--rate-limit-window-ms`. | +| `QWEN_CODE_MEMORY_PROJECT_SCOPE` | `workspace` keys project memory by the exact workspace dir; `git-root` selects the legacy shared scope. When unset, the daemon injects `workspace`; unrecognized values warn once and retain the legacy `git-root` behavior. Propagates via the runtime base env, not `childEnvOverrides`; `--memory-project-scope` wins. Each workspace remember/forget/dream lane caps pending tasks at `MAX_PENDING = 16`; N workspaces allow up to 16·N queued tasks with no daemon-wide cap. | ### Read by the `qwen serve` CLI wrapper @@ -109,7 +109,7 @@ The daemon constructs each workspace runtime from that workspace's merged settin | Field | Effect | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `eventRingSize` | Overrides the default per-session ring size. | -| `memoryProjectScope` | `'git-root' \| 'workspace'` project-memory partitioning; falls back to `QWEN_CODE_MEMORY_PROJECT_SCOPE`. | +| `memoryProjectScope` | `'git-root' \| 'workspace'` project-memory partitioning; falls back to `QWEN_CODE_MEMORY_PROJECT_SCOPE`, then `workspace`. | | `maxPendingPromptsPerSession` | Pending prompt cap per session; `0` / `Infinity` means unlimited. | | `mcpPoolActive` | Programmatic switch, defaulting from `QWEN_SERVE_NO_MCP_POOL`. | | `externalToolGuard` | Optional `{mode:'required', endpoint, token, timeoutMs?}`. Omission is fully off; required mode performs the provider handshake before listening. | diff --git a/docs/users/qwen-serve.md b/docs/users/qwen-serve.md index 32726428d99..40882471a56 100644 --- a/docs/users/qwen-serve.md +++ b/docs/users/qwen-serve.md @@ -393,7 +393,7 @@ Notes: | `--max-total-sessions ` | 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 ` | `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 ` | `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 ` | `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 ` | `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; an unrecognized env 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 ` | — | 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 ` | `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 ` | 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]`. | diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index dc79029301b..e3be49bafad 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -272,7 +272,7 @@ export const serveCommand: CommandModule = { 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. ' + 'Overrides QWEN_CODE_MEMORY_PROJECT_SCOPE when provided.', }) .option('max-connections', { diff --git a/packages/cli/src/serve/run-qwen-serve.test.ts b/packages/cli/src/serve/run-qwen-serve.test.ts index 4e6fbfc0d22..ce51b4c2b5a 100644 --- a/packages/cli/src/serve/run-qwen-serve.test.ts +++ b/packages/cli/src/serve/run-qwen-serve.test.ts @@ -3732,7 +3732,26 @@ 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', + ], + ] as const)('%s', async (_name, launchScope, optionScope, expectedScope) => { tmpDir = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'qws-memory-project-scope-')), ); @@ -3741,7 +3760,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, @@ -3779,7 +3802,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, }, @@ -3792,9 +3817,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']; diff --git a/packages/cli/src/serve/run-qwen-serve.ts b/packages/cli/src/serve/run-qwen-serve.ts index 8dd0b731907..9fef1688efe 100644 --- a/packages/cli/src/serve/run-qwen-serve.ts +++ b/packages/cli/src/serve/run-qwen-serve.ts @@ -2107,11 +2107,10 @@ async function runQwenServeImpl( const launchEnv = { ...process.env }; const baseEnv: NodeJS.ProcessEnv = { ...process.env, - ...(optsIn.memoryProjectScope !== undefined - ? { - QWEN_CODE_MEMORY_PROJECT_SCOPE: optsIn.memoryProjectScope, - } - : {}), + QWEN_CODE_MEMORY_PROJECT_SCOPE: + optsIn.memoryProjectScope ?? + process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'] ?? + 'workspace', }; // The dev harness (scripts/dev.js) stamps DEV=true into the same env that // carries the tsx loader's NODE_OPTIONS, so only then does the base env diff --git a/packages/cli/src/serve/types.ts b/packages/cli/src/serve/types.ts index 83c98e1c9a5..7879e8e36a6 100644 --- a/packages/cli/src/serve/types.ts +++ b/packages/cli/src/serve/types.ts @@ -157,7 +157,8 @@ export interface ServeOptions { * `workspace` keys memory by the exact registered workspace; `git-root` * preserves the legacy behavior that shares memory among workspaces * resolved to the same Git root. When omitted, - * `QWEN_CODE_MEMORY_PROJECT_SCOPE` is read from the environment. + * `QWEN_CODE_MEMORY_PROJECT_SCOPE` is read from the environment before + * defaulting to `workspace`. */ memoryProjectScope?: MemoryProjectScope; /** From 9a41641f8b46374f0265ed1c7c18e102c2318ece Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:30:32 +0800 Subject: [PATCH 2/5] fix(serve): preserve launch env access guard --- packages/cli/src/serve/run-qwen-serve.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/serve/run-qwen-serve.ts b/packages/cli/src/serve/run-qwen-serve.ts index 9fef1688efe..aeafbc01110 100644 --- a/packages/cli/src/serve/run-qwen-serve.ts +++ b/packages/cli/src/serve/run-qwen-serve.ts @@ -2109,7 +2109,7 @@ async function runQwenServeImpl( ...process.env, QWEN_CODE_MEMORY_PROJECT_SCOPE: optsIn.memoryProjectScope ?? - process.env['QWEN_CODE_MEMORY_PROJECT_SCOPE'] ?? + launchEnv['QWEN_CODE_MEMORY_PROJECT_SCOPE'] ?? 'workspace', }; // The dev harness (scripts/dev.js) stamps DEV=true into the same env that From 71a7171ce93f325c6428a153559e1b36e0f6e984 Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:35:12 +0800 Subject: [PATCH 3/5] fix(serve): harden project memory scope resolution --- docs/design/auto-memory/memory-system.md | 2 +- docs/developers/daemon/17-configuration.md | 6 +++-- docs/users/qwen-serve.md | 14 +++++----- packages/cli/src/commands/serve.test.ts | 6 ++--- packages/cli/src/serve/run-qwen-serve.test.ts | 21 +++++++++++++++ packages/cli/src/serve/run-qwen-serve.ts | 26 ++++++++++++++++--- packages/cli/src/serve/types.ts | 5 ++-- 7 files changed, 60 insertions(+), 20 deletions(-) diff --git a/docs/design/auto-memory/memory-system.md b/docs/design/auto-memory/memory-system.md index f3ebc50cd51..b8b81c38fa7 100644 --- a/docs/design/auto-memory/memory-system.md +++ b/docs/design/auto-memory/memory-system.md @@ -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 目录分区项目记忆。`qwen serve` 未显式设置时会注入 `workspace`;standalone CLI 仍默认按 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` 分区下这类命名会共享记忆,命名时需避开。 diff --git a/docs/developers/daemon/17-configuration.md b/docs/developers/daemon/17-configuration.md index c3d6bed3b6f..3e0351d1133 100644 --- a/docs/developers/daemon/17-configuration.md +++ b/docs/developers/daemon/17-configuration.md @@ -64,6 +64,8 @@ This page collects every setting that affects the `qwen serve` daemon and its ad | `QWEN_SERVE_RATE_LIMIT_WINDOW_MS` | Env fallback for `--rate-limit-window-ms`. | | `QWEN_CODE_MEMORY_PROJECT_SCOPE` | `workspace` keys project memory by the exact workspace dir; `git-root` selects the legacy shared scope. When unset, the daemon injects `workspace`; unrecognized values warn once and retain the legacy `git-root` behavior. Propagates via the runtime base env, not `childEnvOverrides`; `--memory-project-scope` wins. Each workspace remember/forget/dream lane caps pending tasks at `MAX_PENDING = 16`; N workspaces allow up to 16·N queued tasks with no daemon-wide cap. | +Blank `QWEN_CODE_MEMORY_PROJECT_SCOPE` values are treated as unset and therefore default to `workspace`; unrecognized non-empty values still warn once and retain the legacy `git-root` behavior. + ### Read by the `qwen serve` CLI wrapper | Env | Effect | @@ -104,12 +106,12 @@ The daemon constructs each workspace runtime from that workspace's merged settin ## `ServeOptions` (programmatic embedding) -`packages/cli/src/serve/types.ts` defines the typed options object accepted by both `runQwenServe` and `createServeApp`. It mirrors the CLI flags above and adds: +`packages/cli/src/serve/types.ts` defines the typed options passed through the public serve APIs. It mirrors the CLI flags above and adds: | Field | Effect | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `eventRingSize` | Overrides the default per-session ring size. | -| `memoryProjectScope` | `'git-root' \| 'workspace'` project-memory partitioning; falls back to `QWEN_CODE_MEMORY_PROJECT_SCOPE`, then `workspace`. | +| `memoryProjectScope` | `runQwenServe` only; precedence is option, launch env, then `workspace`. Direct `createServeApp` callers use `deps.daemonEnv`. | | `maxPendingPromptsPerSession` | Pending prompt cap per session; `0` / `Infinity` means unlimited. | | `mcpPoolActive` | Programmatic switch, defaulting from `QWEN_SERVE_NO_MCP_POOL`. | | `externalToolGuard` | Optional `{mode:'required', endpoint, token, timeoutMs?}`. Omission is fully off; required mode performs the provider handshake before listening. | diff --git a/docs/users/qwen-serve.md b/docs/users/qwen-serve.md index 40882471a56..a8d3bc70887 100644 --- a/docs/users/qwen-serve.md +++ b/docs/users/qwen-serve.md @@ -393,7 +393,7 @@ Notes: | `--max-total-sessions ` | 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 ` | `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 ` | `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 ` | `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; an unrecognized env 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. | +| `--memory-project-scope ` | `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 ` | — | 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 ` | `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 ` | 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]`. | @@ -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. diff --git a/packages/cli/src/commands/serve.test.ts b/packages/cli/src/commands/serve.test.ts index 2677c4b18da..0731bda4b5f 100644 --- a/packages/cli/src/commands/serve.test.ts +++ b/packages/cli/src/commands/serve.test.ts @@ -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' }), ); }); diff --git a/packages/cli/src/serve/run-qwen-serve.test.ts b/packages/cli/src/serve/run-qwen-serve.test.ts index ce51b4c2b5a..644a0e7bbda 100644 --- a/packages/cli/src/serve/run-qwen-serve.test.ts +++ b/packages/cli/src/serve/run-qwen-serve.test.ts @@ -1270,6 +1270,9 @@ 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'); await Promise.all( Array.from({ length: 70 }, (_, index) => @@ -3751,6 +3754,24 @@ describe('runQwenServe runtime startup failures', () => { undefined, 'git-root', ], + [ + '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-')), diff --git a/packages/cli/src/serve/run-qwen-serve.ts b/packages/cli/src/serve/run-qwen-serve.ts index aeafbc01110..0430d637636 100644 --- a/packages/cli/src/serve/run-qwen-serve.ts +++ b/packages/cli/src/serve/run-qwen-serve.ts @@ -2105,12 +2105,26 @@ async function runQwenServeImpl( // Snapshot before any scrub: close() restores the host's launch // environment from this copy, not from the (possibly scrubbed) base env. const launchEnv = { ...process.env }; + const launchMemoryProjectScopeValue = + launchEnv['QWEN_CODE_MEMORY_PROJECT_SCOPE']; + const launchMemoryProjectScope = launchMemoryProjectScopeValue?.trim() + ? launchMemoryProjectScopeValue + : undefined; + const memoryProjectScopeValue = + optsIn.memoryProjectScope ?? launchMemoryProjectScope ?? 'workspace'; + const memoryProjectScopeSource = + optsIn.memoryProjectScope !== undefined + ? 'option' + : launchMemoryProjectScope !== undefined + ? 'environment' + : 'default'; + const resolvedMemoryProjectScope = + memoryProjectScopeValue.trim().toLowerCase() === 'workspace' + ? 'workspace' + : 'git-root'; const baseEnv: NodeJS.ProcessEnv = { ...process.env, - QWEN_CODE_MEMORY_PROJECT_SCOPE: - optsIn.memoryProjectScope ?? - launchEnv['QWEN_CODE_MEMORY_PROJECT_SCOPE'] ?? - 'workspace', + QWEN_CODE_MEMORY_PROJECT_SCOPE: memoryProjectScopeValue, }; // The dev harness (scripts/dev.js) stamps DEV=true into the same env that // carries the tsx loader's NODE_OPTIONS, so only then does the base env @@ -2661,6 +2675,10 @@ async function runQwenServeImpl( baseDir: daemonLogBaseDir, }); loggerLifecycle.initialized(daemonLog); + daemonLog.info('project memory scope resolved', { + projectMemoryScope: resolvedMemoryProjectScope, + projectMemoryScopeSource: memoryProjectScopeSource, + }); // Per-workspace .env loads keep running after boot (skill status, voice // capability checks, settings reloads); boot stderr is long gone by then, // so fresh loader-key rejections must land in the durable daemon log or diff --git a/packages/cli/src/serve/types.ts b/packages/cli/src/serve/types.ts index 7879e8e36a6..9ff4e2f6052 100644 --- a/packages/cli/src/serve/types.ts +++ b/packages/cli/src/serve/types.ts @@ -153,12 +153,13 @@ export interface ServeOptions { */ workspace?: string; /** - * Project-memory partitioning for every runtime owned by this daemon. + * Project-memory partitioning for every runtime owned by `runQwenServe`. * `workspace` keys memory by the exact registered workspace; `git-root` * preserves the legacy behavior that shares memory among workspaces * resolved to the same Git root. When omitted, * `QWEN_CODE_MEMORY_PROJECT_SCOPE` is read from the environment before - * defaulting to `workspace`. + * defaulting to `workspace`. Direct `createServeApp` callers must instead + * provide the scope through `deps.daemonEnv`. */ memoryProjectScope?: MemoryProjectScope; /** From 65139068f8298f988bebcb80a19dfb54ae9da407 Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:11:47 +0800 Subject: [PATCH 4/5] fix(serve): harden project memory scope diagnostics --- packages/cli/src/serve/run-qwen-serve.test.ts | 8 ++++++++ packages/cli/src/serve/run-qwen-serve.ts | 1 + 2 files changed, 9 insertions(+) diff --git a/packages/cli/src/serve/run-qwen-serve.test.ts b/packages/cli/src/serve/run-qwen-serve.test.ts index 644a0e7bbda..c50b3adaa3d 100644 --- a/packages/cli/src/serve/run-qwen-serve.test.ts +++ b/packages/cli/src/serve/run-qwen-serve.test.ts @@ -1236,7 +1236,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( @@ -1273,6 +1275,7 @@ describe('runQwenServe daemon logger wiring', () => { expect(logContent).toContain('project memory scope resolved'); expect(logContent).toContain('projectMemoryScope=workspace'); expect(logContent).toContain('projectMemoryScopeSource=default'); + expect(logContent).toContain('projectMemoryScopeRaw=workspace'); await Promise.all( Array.from({ length: 70 }, (_, index) => @@ -1305,6 +1308,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); }); diff --git a/packages/cli/src/serve/run-qwen-serve.ts b/packages/cli/src/serve/run-qwen-serve.ts index 974ee7f2195..bf3358257a8 100644 --- a/packages/cli/src/serve/run-qwen-serve.ts +++ b/packages/cli/src/serve/run-qwen-serve.ts @@ -2685,6 +2685,7 @@ async function runQwenServeImpl( daemonLog.info('project memory scope resolved', { projectMemoryScope: resolvedMemoryProjectScope, projectMemoryScopeSource: memoryProjectScopeSource, + projectMemoryScopeRaw: memoryProjectScopeValue, }); // Per-workspace .env loads keep running after boot (skill status, voice // capability checks, settings reloads); boot stderr is long gone by then, From c6ed41cf6c5109109380024a8e39161d0d9ae25d Mon Sep 17 00:00:00 2001 From: qqqys <266654365+qqqys@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:14:37 +0800 Subject: [PATCH 5/5] fix(serve): keep memory scope operator-owned --- packages/cli/src/config/shared-env-keys.test.ts | 6 ++++++ packages/cli/src/config/shared-env-keys.ts | 3 +++ 2 files changed, 9 insertions(+) diff --git a/packages/cli/src/config/shared-env-keys.test.ts b/packages/cli/src/config/shared-env-keys.test.ts index 9504c1b3278..4199fdff84e 100644 --- a/packages/cli/src/config/shared-env-keys.test.ts +++ b/packages/cli/src/config/shared-env-keys.test.ts @@ -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'); diff --git a/packages/cli/src/config/shared-env-keys.ts b/packages/cli/src/config/shared-env-keys.ts index 30c1792715f..0dd7e641b16 100644 --- a/packages/cli/src/config/shared-env-keys.ts +++ b/packages/cli/src/config/shared-env-keys.ts @@ -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