From 59150c5038ccfe839d92c7e6a4106017cc5b411e Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Sun, 19 Jul 2026 17:45:44 +0800 Subject: [PATCH 1/6] fix(channels): exclude discrete messages from replies --- packages/channels/base/src/AcpBridge.test.ts | 38 +++++++++++++ packages/channels/base/src/AcpBridge.ts | 5 +- .../base/src/DaemonChannelBridge.test.ts | 54 +++++++++++++++++++ .../channels/base/src/DaemonChannelBridge.ts | 5 +- 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/packages/channels/base/src/AcpBridge.test.ts b/packages/channels/base/src/AcpBridge.test.ts index 8cd7cee64b3..8f0c0e5e68d 100644 --- a/packages/channels/base/src/AcpBridge.test.ts +++ b/packages/channels/base/src/AcpBridge.test.ts @@ -324,6 +324,44 @@ describe('AcpBridge', () => { ); }); + it('excludes discrete background notifications from the final response', async () => { + const bridge = new AcpBridge({ + cliEntryPath: '/tmp/qwen', + cwd: '/tmp', + }) as unknown as TestableAcpBridge; + bridge.child = { killed: false, exitCode: null }; + bridge.connection = { + extMethod: vi.fn(), + prompt: vi.fn(async () => { + bridge.handleSessionUpdate({ + sessionId: 's-1', + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'Final answer.' }, + }, + }); + bridge.handleSessionUpdate({ + sessionId: 's-1', + update: { + sessionUpdate: 'agent_message_chunk', + content: { + type: 'text', + text: 'Background agent "Explore" completed.', + }, + _meta: { + source: 'background_notification', + qwenDiscreteMessage: true, + }, + }, + }); + }), + }; + + await expect(bridge.prompt('s-1', 'question')).resolves.toBe( + 'Final answer.', + ); + }); + it('returns only the final slash-command output', async () => { const bridge = new AcpBridge({ cliEntryPath: '/tmp/qwen', diff --git a/packages/channels/base/src/AcpBridge.ts b/packages/channels/base/src/AcpBridge.ts index aaf530ef6d2..5c1e60e411c 100644 --- a/packages/channels/base/src/AcpBridge.ts +++ b/packages/channels/base/src/AcpBridge.ts @@ -314,7 +314,10 @@ export class AcpBridge extends EventEmitter implements ChannelAgentBridge { switch (type) { case 'agent_message_chunk': { const meta = update['_meta'] as Record | undefined; - if (typeof meta?.['parentToolCallId'] === 'string') { + if ( + typeof meta?.['parentToolCallId'] === 'string' || + meta?.['qwenDiscreteMessage'] === true + ) { break; } const content = update['content'] as diff --git a/packages/channels/base/src/DaemonChannelBridge.test.ts b/packages/channels/base/src/DaemonChannelBridge.test.ts index 1142dfa4e4e..649de326065 100644 --- a/packages/channels/base/src/DaemonChannelBridge.test.ts +++ b/packages/channels/base/src/DaemonChannelBridge.test.ts @@ -512,6 +512,60 @@ describe('DaemonChannelBridge', () => { bridge.stop(); }); + it('excludes discrete background notifications from the daemon response', async () => { + const events = new EventQueue(); + const session = createFakeSession(events); + session.prompt.mockImplementation(async () => { + events.push({ + id: 1, + v: 1, + type: 'session_update', + data: { + sessionId: 'session-1', + update: { + sessionUpdate: 'agent_message_chunk', + content: { type: 'text', text: 'Final answer.' }, + }, + }, + }); + events.push({ + id: 2, + v: 1, + type: 'session_update', + data: { + sessionId: 'session-1', + update: { + sessionUpdate: 'agent_message_chunk', + content: { + type: 'text', + text: 'Background agent "Explore" completed.', + }, + _meta: { + source: 'background_notification', + qwenDiscreteMessage: true, + }, + }, + }, + }); + events.push(turnCompleteEvent()); + return { stopReason: 'end_turn' }; + }); + const bridge = new DaemonChannelBridge({ + cwd: '/repo', + sessionFactory: vi.fn().mockResolvedValue(session), + }); + + await bridge.start(); + await bridge.newSession('/repo'); + + await expect(bridge.prompt('session-1', 'summarize')).resolves.toBe( + 'Final answer.', + ); + + events.close(); + bridge.stop(); + }); + it('returns only the final slash-command output from the daemon', async () => { const events = new EventQueue(); const session = createFakeSession(events); diff --git a/packages/channels/base/src/DaemonChannelBridge.ts b/packages/channels/base/src/DaemonChannelBridge.ts index fb7f2a226ea..d5fea4c7e58 100644 --- a/packages/channels/base/src/DaemonChannelBridge.ts +++ b/packages/channels/base/src/DaemonChannelBridge.ts @@ -649,7 +649,10 @@ export class DaemonChannelBridge switch (type) { case 'agent_message_chunk': { const meta = isRecord(update['_meta']) ? update['_meta'] : undefined; - if (typeof meta?.['parentToolCallId'] === 'string') { + if ( + typeof meta?.['parentToolCallId'] === 'string' || + meta?.['qwenDiscreteMessage'] === true + ) { break; } const text = getTextContent(update['content']); From 594126019aacce02debe6ffc30e1dd461d9ce35a Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Mon, 20 Jul 2026 00:38:09 +0800 Subject: [PATCH 2/6] feat(serve): make ACP initialize handshake timeout configurable Add --initialize-timeout-ms CLI flag to qwen serve, wiring it through to BridgeOptions.initializeTimeoutMs. The ACP initialize handshake defaults to 10 s (DEFAULT_INIT_TIMEOUT_MS); containerized deployments where the child process needs longer can now raise the ceiling without patching the source. Fixes #7244 --- packages/cli/src/commands/serve.test.ts | 10 +++ packages/cli/src/commands/serve.ts | 9 +++ packages/cli/src/serve/run-qwen-serve.test.ts | 74 +++++++++++++++++++ packages/cli/src/serve/run-qwen-serve.ts | 20 +++++ packages/cli/src/serve/types.ts | 5 ++ 5 files changed, 118 insertions(+) diff --git a/packages/cli/src/commands/serve.test.ts b/packages/cli/src/commands/serve.test.ts index 374f8ad2aab..8f905234a27 100644 --- a/packages/cli/src/commands/serve.test.ts +++ b/packages/cli/src/commands/serve.test.ts @@ -69,11 +69,21 @@ describe('serve command args', () => { expect(parsed['max-total-sessions']).toBe(42); }); + it('parses --initialize-timeout-ms as a number', () => { + const parsed = buildParser().parseSync('--initialize-timeout-ms 30000'); + expect(parsed['initialize-timeout-ms']).toBe(30000); + }); + it('leaves --permission-response-timeout-ms unset by default', () => { const parsed = buildParser().parseSync(''); expect(parsed['permission-response-timeout-ms']).toBeUndefined(); }); + it('leaves --initialize-timeout-ms unset by default', () => { + const parsed = buildParser().parseSync(''); + expect(parsed['initialize-timeout-ms']).toBeUndefined(); + }); + it('parses --experimental-lsp for daemon child opt-in', () => { const parsed = buildParser().parseSync('--experimental-lsp'); expect(parsed['experimentalLsp']).toBe(true); diff --git a/packages/cli/src/commands/serve.ts b/packages/cli/src/commands/serve.ts index dcbdbdb558e..7f44df17df3 100644 --- a/packages/cli/src/commands/serve.ts +++ b/packages/cli/src/commands/serve.ts @@ -120,6 +120,7 @@ interface ServeArgs { 'prompt-deadline-ms'?: number; 'writer-idle-timeout-ms'?: number; 'channel-idle-timeout-ms'?: number; + 'initialize-timeout-ms'?: number; 'session-reap-interval-ms'?: number; 'session-idle-timeout-ms'?: number; 'permission-response-timeout-ms'?: number; @@ -335,6 +336,11 @@ export const serveCommand: CommandModule = { 'Milliseconds to keep ACP child alive after last session closes. ' + '0 or unset = immediate kill (default).', }) + .option('initialize-timeout-ms', { + type: 'number', + description: + 'ACP initialize handshake timeout (ms). ' + 'Default: 10000 (10 s).', + }) .option('session-reap-interval-ms', { type: 'number', description: @@ -588,6 +594,9 @@ export const serveCommand: CommandModule = { ...(argv['channel-idle-timeout-ms'] !== undefined ? { channelIdleTimeoutMs: argv['channel-idle-timeout-ms'] } : {}), + ...(argv['initialize-timeout-ms'] !== undefined + ? { initializeTimeoutMs: argv['initialize-timeout-ms'] } + : {}), ...(argv['session-reap-interval-ms'] !== undefined ? { sessionReapIntervalMs: argv['session-reap-interval-ms'] } : {}), diff --git a/packages/cli/src/serve/run-qwen-serve.test.ts b/packages/cli/src/serve/run-qwen-serve.test.ts index 039fdd3ba63..f35810e6d65 100644 --- a/packages/cli/src/serve/run-qwen-serve.test.ts +++ b/packages/cli/src/serve/run-qwen-serve.test.ts @@ -1279,6 +1279,80 @@ describe('runQwenServe permissionResponseTimeoutMs validation', () => { }); }); +describe('runQwenServe initializeTimeoutMs validation', () => { + let tmpDir: string; + + afterEach(() => { + if (tmpDir) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } + }); + + it('rejects a non-positive initializeTimeoutMs', async () => { + tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'qws-it-'))); + const fakeBridge = { + spawnOrAttach: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + killAllSync: vi.fn(), + } as unknown as HttpAcpBridge; + + const origEnv = process.env['QWEN_RUNTIME_DIR']; + process.env['QWEN_RUNTIME_DIR'] = tmpDir; + try { + await expect( + runQwenServe( + { + port: 0, + hostname: '127.0.0.1', + mode: 'http-bridge', + workspace: tmpDir, + maxSessions: 1, + initializeTimeoutMs: 0, + }, + { bridge: fakeBridge }, + ), + ).rejects.toThrow(/initializeTimeoutMs/); + } finally { + delete process.env['QWEN_RUNTIME_DIR']; + if (origEnv !== undefined) { + process.env['QWEN_RUNTIME_DIR'] = origEnv; + } + } + }); + + it('rejects a non-finite initializeTimeoutMs', async () => { + tmpDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'qws-it-'))); + const fakeBridge = { + spawnOrAttach: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + killAllSync: vi.fn(), + } as unknown as HttpAcpBridge; + + const origEnv = process.env['QWEN_RUNTIME_DIR']; + process.env['QWEN_RUNTIME_DIR'] = tmpDir; + try { + await expect( + runQwenServe( + { + port: 0, + hostname: '127.0.0.1', + mode: 'http-bridge', + workspace: tmpDir, + maxSessions: 1, + initializeTimeoutMs: Number.NaN, + }, + { bridge: fakeBridge }, + ), + ).rejects.toThrow(/initializeTimeoutMs/); + } finally { + delete process.env['QWEN_RUNTIME_DIR']; + if (origEnv !== undefined) { + process.env['QWEN_RUNTIME_DIR'] = origEnv; + } + } + }); +}); + // Long-lived self-signed cert (CN=localhost, SAN IP:127.0.0.1) used only // to exercise the HTTPS listener path. Not a real secret. const TEST_TLS_CERT = `-----BEGIN CERTIFICATE----- diff --git a/packages/cli/src/serve/run-qwen-serve.ts b/packages/cli/src/serve/run-qwen-serve.ts index bb7bac9e01b..41e174b34fb 100644 --- a/packages/cli/src/serve/run-qwen-serve.ts +++ b/packages/cli/src/serve/run-qwen-serve.ts @@ -2393,6 +2393,17 @@ async function runQwenServeImpl( ); } } + if (opts.initializeTimeoutMs !== undefined) { + if ( + !Number.isFinite(opts.initializeTimeoutMs) || + !Number.isInteger(opts.initializeTimeoutMs) || + opts.initializeTimeoutMs <= 0 + ) { + throw new TypeError( + `Invalid initializeTimeoutMs: ${opts.initializeTimeoutMs}. Must be a positive integer (milliseconds).`, + ); + } + } // Validate here (not just in the yargs handler) so embedded callers of // `runQwenServe({ permissionResponseTimeoutMs })` also fail loud: the // bridge treats a non-finite / negative value as the "disabled" @@ -3339,6 +3350,9 @@ async function runQwenServeImpl( ...(opts.channelIdleTimeoutMs !== undefined ? { channelIdleTimeoutMs: opts.channelIdleTimeoutMs } : {}), + ...(opts.initializeTimeoutMs !== undefined + ? { initializeTimeoutMs: opts.initializeTimeoutMs } + : {}), ...(opts.sessionReapIntervalMs !== undefined ? { sessionReapIntervalMs: opts.sessionReapIntervalMs } : {}), @@ -3648,6 +3662,9 @@ async function runQwenServeImpl( ...(opts.channelIdleTimeoutMs !== undefined ? { channelIdleTimeoutMs: opts.channelIdleTimeoutMs } : {}), + ...(opts.initializeTimeoutMs !== undefined + ? { initializeTimeoutMs: opts.initializeTimeoutMs } + : {}), ...(opts.sessionReapIntervalMs !== undefined ? { sessionReapIntervalMs: opts.sessionReapIntervalMs } : {}), @@ -4016,6 +4033,9 @@ async function runQwenServeImpl( ...(opts.channelIdleTimeoutMs !== undefined ? { channelIdleTimeoutMs: opts.channelIdleTimeoutMs } : {}), + ...(opts.initializeTimeoutMs !== undefined + ? { initializeTimeoutMs: opts.initializeTimeoutMs } + : {}), ...(opts.sessionReapIntervalMs !== undefined ? { sessionReapIntervalMs: opts.sessionReapIntervalMs } : {}), diff --git a/packages/cli/src/serve/types.ts b/packages/cli/src/serve/types.ts index 58875b282c5..d7c330be7cc 100644 --- a/packages/cli/src/serve/types.ts +++ b/packages/cli/src/serve/types.ts @@ -221,6 +221,11 @@ export interface ServeOptions { sessionReapIntervalMs?: number; /** Session idle timeout in ms. 0 = disabled. Default: 1800000 (30 min). */ sessionIdleTimeoutMs?: number; + /** + * ACP `initialize` handshake timeout in ms. Must be a positive + * integer. Default: 10000 (10 s). + */ + initializeTimeoutMs?: number; /** * Wall-clock timeout in ms for a single human permission / * ask_user_question response in daemon (ACP) mode. 0 = disabled From cea08fb5749038c5f6029ce70912a2ac6c51cb03 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Mon, 20 Jul 2026 00:39:46 +0800 Subject: [PATCH 3/6] fix(serve): wire initializeTimeoutMs to fast-path parser and embed bridge Add the missing NUMBER_OPTIONS entry in fast-path.ts and forward initializeTimeoutMs in the server.ts inline createAcpSessionBridge call so the direct-embed / test path also respects the flag. --- packages/cli/src/serve/fast-path.ts | 1 + packages/cli/src/serve/server.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/cli/src/serve/fast-path.ts b/packages/cli/src/serve/fast-path.ts index cd13dcf0904..da3b321f889 100644 --- a/packages/cli/src/serve/fast-path.ts +++ b/packages/cli/src/serve/fast-path.ts @@ -45,6 +45,7 @@ const NUMBER_OPTIONS = new Map< ['promptDeadlineMs', 'prompt-deadline-ms'], ['writerIdleTimeoutMs', 'writer-idle-timeout-ms'], ['channelIdleTimeoutMs', 'channel-idle-timeout-ms'], + ['initializeTimeoutMs', 'initialize-timeout-ms'], ['sessionReapIntervalMs', 'session-reap-interval-ms'], ['sessionIdleTimeoutMs', 'session-idle-timeout-ms'], ['permissionResponseTimeoutMs', 'permission-response-timeout-ms'], diff --git a/packages/cli/src/serve/server.ts b/packages/cli/src/serve/server.ts index 41fa453b234..3e2bfcd20fd 100644 --- a/packages/cli/src/serve/server.ts +++ b/packages/cli/src/serve/server.ts @@ -766,6 +766,7 @@ export function createServeApp( maxPendingPromptsPerSession: opts.maxPendingPromptsPerSession, eventRingSize: opts.eventRingSize, compactedReplayMaxBytes: opts.compactedReplayMaxBytes, + initializeTimeoutMs: opts.initializeTimeoutMs, permissionResponseTimeoutMs: opts.permissionResponseTimeoutMs, boundWorkspace, sessionShellCommandEnabled, From 3d9e4c80016b94d2dd56ba07ab306320141b52d5 Mon Sep 17 00:00:00 2001 From: qwen-code-dev-bot Date: Mon, 20 Jul 2026 12:44:11 +0000 Subject: [PATCH 4/6] =?UTF-8?q?fix(serve):=20address=20review=20=E2=80=94?= =?UTF-8?q?=20fast-path=20test,=20timer=20upper=20bound,=20revert=20#7223,?= =?UTF-8?q?=20docs=20(#7246)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/developers/daemon/02-serve-runtime.md | 38 ++++++------- docs/developers/daemon/17-configuration.md | 2 + .../daemon/20-quickstart-operations.md | 2 + docs/users/qwen-serve.md | 1 + packages/channels/base/src/AcpBridge.test.ts | 38 ------------- packages/channels/base/src/AcpBridge.ts | 5 +- .../base/src/DaemonChannelBridge.test.ts | 54 ------------------- .../channels/base/src/DaemonChannelBridge.ts | 5 +- packages/cli/src/commands/serve.ts | 3 +- packages/cli/src/serve/fast-path.test.ts | 1 + packages/cli/src/serve/run-qwen-serve.test.ts | 32 +++++++++++ packages/cli/src/serve/run-qwen-serve.ts | 1 + packages/cli/src/serve/types.ts | 3 +- 13 files changed, 64 insertions(+), 121 deletions(-) diff --git a/docs/developers/daemon/02-serve-runtime.md b/docs/developers/daemon/02-serve-runtime.md index ddf703ba892..7fbee49bc1e 100644 --- a/docs/developers/daemon/02-serve-runtime.md +++ b/docs/developers/daemon/02-serve-runtime.md @@ -113,25 +113,25 @@ Calling `createServeApp` directly returns only an `Application`; the embedder ow ## Configuration -| Source | Key | Effect | -| --------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| Env | `QWEN_SERVER_TOKEN` | Bearer token after trim. | -| Env | `QWEN_SERVE_NO_MCP_POOL=1` | Forces `mcpPoolActive=false`. | -| ACP child env | `QWEN_SERVE_MCP_CLIENT_BUDGET` / `QWEN_SERVE_MCP_BUDGET_MODE` | Generated from `--mcp-client-budget` / `--mcp-budget-mode` and forwarded through `childEnvOverrides`. | -| Env | `QWEN_SERVE_PROMPT_DEADLINE_MS` / `QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS` | Default prompt / SSE idle timeouts. | -| Env | `QWEN_SERVE_RATE_LIMIT*` | Rate-limit switch, prompt / mutation / read caps, and window default. | -| Env | `QWEN_SERVE_DEBUG=1` | Verbose stderr logs. See [`19-observability.md`](./19-observability.md). | -| Flags | `--hostname`, `--port` | Listen binding. | -| Flags | `--token`, `--require-auth`, `--enable-session-shell` | Bearer token, loopback auth hardening, and explicit shell execution switch. | -| Flag | `--workspace` | Overrides `process.cwd()`; repeat to register additional isolated workspace runtimes. | -| Flags | `--max-sessions`, `--max-pending-prompts-per-session`, `--max-connections`, `--event-ring-size` | Bridge / Express caps. | -| Flags | `--mcp-client-budget=N`, `--mcp-budget-mode={off,warn,enforce}` | Forwarded to the ACP child. | -| Flags | `--allow-origin`, `--allow-private-auth-base-url` | Browser CORS allowlist and localhost/private auth provider installation switch. | -| Flags | `--prompt-deadline-ms`, `--writer-idle-timeout-ms`, `--channel-idle-timeout-ms` | Prompt, SSE writer, and ACP child idle lifecycle control. | -| Flags | `--session-reap-interval-ms`, `--session-idle-timeout-ms` | Disconnected-session reaping control. | -| Flags | `--rate-limit*` | Per-tier HTTP rate limit. | -| `settings.json` | `policy.permissionStrategy`, `policy.consensusQuorum` | `MultiClientPermissionMediator` policy and quorum. | -| `settings.json` | `context.fileName` | `getCurrentGeminiMdFilename` override for the bridge. | +| Source | Key | Effect | +| --------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Env | `QWEN_SERVER_TOKEN` | Bearer token after trim. | +| Env | `QWEN_SERVE_NO_MCP_POOL=1` | Forces `mcpPoolActive=false`. | +| ACP child env | `QWEN_SERVE_MCP_CLIENT_BUDGET` / `QWEN_SERVE_MCP_BUDGET_MODE` | Generated from `--mcp-client-budget` / `--mcp-budget-mode` and forwarded through `childEnvOverrides`. | +| Env | `QWEN_SERVE_PROMPT_DEADLINE_MS` / `QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS` | Default prompt / SSE idle timeouts. | +| Env | `QWEN_SERVE_RATE_LIMIT*` | Rate-limit switch, prompt / mutation / read caps, and window default. | +| Env | `QWEN_SERVE_DEBUG=1` | Verbose stderr logs. See [`19-observability.md`](./19-observability.md). | +| Flags | `--hostname`, `--port` | Listen binding. | +| Flags | `--token`, `--require-auth`, `--enable-session-shell` | Bearer token, loopback auth hardening, and explicit shell execution switch. | +| Flag | `--workspace` | Overrides `process.cwd()`; repeat to register additional isolated workspace runtimes. | +| Flags | `--max-sessions`, `--max-pending-prompts-per-session`, `--max-connections`, `--event-ring-size` | Bridge / Express caps. | +| Flags | `--mcp-client-budget=N`, `--mcp-budget-mode={off,warn,enforce}` | Forwarded to the ACP child. | +| Flags | `--allow-origin`, `--allow-private-auth-base-url` | Browser CORS allowlist and localhost/private auth provider installation switch. | +| Flags | `--prompt-deadline-ms`, `--writer-idle-timeout-ms`, `--channel-idle-timeout-ms`, `--initialize-timeout-ms` | Prompt, SSE writer, ACP child idle lifecycle, and ACP child request timeout control. | +| Flags | `--session-reap-interval-ms`, `--session-idle-timeout-ms` | Disconnected-session reaping control. | +| Flags | `--rate-limit*` | Per-tier HTTP rate limit. | +| `settings.json` | `policy.permissionStrategy`, `policy.consensusQuorum` | `MultiClientPermissionMediator` policy and quorum. | +| `settings.json` | `context.fileName` | `getCurrentGeminiMdFilename` override for the bridge. | See [`17-configuration.md`](./17-configuration.md) for the merged reference. diff --git a/docs/developers/daemon/17-configuration.md b/docs/developers/daemon/17-configuration.md index 5d4e33d3d14..9917dc85cb1 100644 --- a/docs/developers/daemon/17-configuration.md +++ b/docs/developers/daemon/17-configuration.md @@ -28,6 +28,7 @@ This page collects every setting that affects the `qwen serve` daemon and its ad | `--prompt-deadline-ms ` | positive integer | unset | Server-side prompt wallclock limit in ms. Timeout aborts and returns an error. | | `--writer-idle-timeout-ms ` | positive integer | unset | Per-SSE-connection idle timeout in ms. The daemon closes the SSE connection when no event is sent for this duration. | | `--channel-idle-timeout-ms ` | non-negative integer | `0` | How long to keep the ACP child alive after the last session closes. `0` means reclaim immediately. | +| `--initialize-timeout-ms ` | positive integer | `10000` | ACP child request timeout, including the initialize handshake (ms). | | `--session-reap-interval-ms ` | non-negative integer | `60000` | Session reaper scan interval; `0` disables it. | | `--session-idle-timeout-ms ` | non-negative integer | `1800000` | Disconnected-session idle reaping time; `0` disables it. | | `--rate-limit` / `--no-rate-limit` | boolean | env / off | Enables per-tier HTTP rate limiting for prompt, mutation, and read routes. | @@ -101,6 +102,7 @@ The daemon constructs each workspace runtime from that workspace's merged settin | `promptDeadlineMs` | Prompt wallclock limit. | | `writerIdleTimeoutMs` | SSE writer idle timeout. | | `channelIdleTimeoutMs` | How long to keep the ACP child warm after the last session closes. | +| `initializeTimeoutMs` | ACP child request timeout, including the initialize handshake. | | `sessionReapIntervalMs` | Session reaper scan interval. | | `sessionIdleTimeoutMs` | Disconnected-session idle reaping time. | | `rateLimit*` | Per-tier HTTP rate limit switch, thresholds, and window. | diff --git a/docs/developers/daemon/20-quickstart-operations.md b/docs/developers/daemon/20-quickstart-operations.md index 18546d22be6..0e6008b298d 100644 --- a/docs/developers/daemon/20-quickstart-operations.md +++ b/docs/developers/daemon/20-quickstart-operations.md @@ -94,6 +94,7 @@ The CLI is defined in **`packages/cli/src/commands/serve.ts`**: | `--prompt-deadline-ms ` | number | none | - | Server-side prompt wallclock limit in ms; timeout aborts the prompt. | | `--writer-idle-timeout-ms ` | number | none | - | Per-SSE-connection idle timeout in ms. | | `--channel-idle-timeout-ms ` | number | `0` | - | Keeps the ACP child alive after the last session closes. `0` means reclaim immediately. | +| `--initialize-timeout-ms ` | number | `10000` | - | ACP child request timeout, including the initialize handshake (ms). | | `--session-reap-interval-ms ` | number | `60000` | - | Session reaper scan interval. `0` disables it. | | `--session-idle-timeout-ms ` | number | `1800000` | - | Disconnected-session idle timeout. `0` disables it. | | `--rate-limit` / `--no-rate-limit` | boolean | env / off | - | Enables or disables per-tier HTTP rate limiting. | @@ -156,6 +157,7 @@ Settings I/O failure, such as malformed JSON, falls back to defaults. `InvalidPo | `--event-ring-size > 1_000_000` | Thrown during bridge construction | | `--allow-origin '*'` without token | `Refusing to start with --allow-origin '*' but no bearer token configured` | | `--prompt-deadline-ms` / `--writer-idle-timeout-ms` is not a positive integer | `Must be a positive integer` | +| `--initialize-timeout-ms` is not a positive integer or exceeds `2^31-1` | `Must be a positive integer` / `Exceeds maximum JS timer delay` | | Unknown `policy.permissionStrategy` or non-positive `policy.consensusQuorum` | `InvalidPolicyConfigError` | ## 7. Curl verification checklist diff --git a/docs/users/qwen-serve.md b/docs/users/qwen-serve.md index 91dd2c2a129..b2a623e94c4 100644 --- a/docs/users/qwen-serve.md +++ b/docs/users/qwen-serve.md @@ -397,6 +397,7 @@ Notes: | `--mcp-client-budget ` | — | Positive integer cap on live MCP clients. When `mcp_workspace_pool` is advertised, the cap and transports are shared per workspace runtime; when the tag is absent, the legacy per-session manager enforces it. Combine with `--mcp-budget-mode`. When unset, no accounting-driven enforcement (but `GET /workspace/mcp` still reports `clientCount`). Distinct from claude-code's `MCP_SERVER_CONNECTION_BATCH_SIZE`, which gates startup concurrency rather than total live clients. Pre-flight `caps.features.mcp_guardrails` and `caps.features.mcp_workspace_pool`. | | `--mcp-budget-mode ` | `warn` / `off` | How `--mcp-client-budget` is enforced. `warn` (default when budget set): no refusal, snapshot's `budgets[0].status` flips to `warning` at ≥75% of budget. `enforce`: connects past the cap are refused, per-server cell shows `disabledReason: 'budget'`, deterministic by `mcpServers` declaration order. `off` (default when budget unset): pure observability. Boot rejects `enforce` without a budget. | | `--http-bridge` | `true` | Stage 1 mode: production attempts to preheat one primary `qwen --acp` child for compatibility and retries on first use after failure, while each trusted secondary can start one child on demand. Sessions targeting a runtime multiplex onto its child via ACP `newSession()`; untrusted secondaries cannot start ACP. Stage 2 native in-process becomes available later. | +| `--initialize-timeout-ms ` | `10000` | ACP child request timeout, including the `initialize` handshake (ms). Must be a positive integer up to `2147483647`. Values above the JS timer ceiling (`2^31-1`) are rejected at boot because Node silently compresses them to 1 ms. Cold-container deployments that need extra headroom for child startup can raise this; the same value governs `newSession`, workspace-status polls, and other ACP ext-method deadlines. | | `--allow-origin ` | — | T2.4 ([#4514](https://github.com/QwenLM/qwen-code/issues/4514)). Cross-origin allowlist for browser webui clients. Repeatable. Each value is `*` (any origin — boot refuses if no bearer token is configured; `--require-auth` on loopback is recommended so `/health` and `/demo` are also bearer-gated, since both are pre-auth on loopback by default) or a canonical URL origin (`://[:]`, no trailing slash / path / userinfo / query). **Subdomain wildcards (`https://*.example.com`) are intentionally unsupported** — list each subdomain explicitly, or use `*` with a configured token (and `--require-auth` for full hardening). Matched origins receive CORS response headers (`Access-Control-Allow-Origin`, `Vary: Origin`, methods, headers, max-age, and exposed `Retry-After`); unmatched origins still get a 403 with the same envelope as today's wall. `Origin: null` (sandboxed iframes, file:// docs) is always rejected, even under `*`. Pre-flight via `caps.features.allow_origin`. Loopback self-origin hits are unaffected. | | `--web` / `--no-web` | `true` | Serve the built Web Shell SPA at the daemon root (`GET /`, `/assets/*`, and SPA deep-link fallback). The static shell is registered **before** the bearer-auth gate — a browser can't attach a token to a `