Skip to Content
Developer GuideDaemon Mode (Developer Deep Dive)20 · Quickstart & Operations

Quickstart & Operations

This page focuses on how to start qwen serve, how to verify that it is working, and what the internal call chain looks like from qwen serve to the listening server. Architecture, components, and wire protocol details live in the other daemon deep-dive pages.

1. Shortest path

qwen serve

Output:

qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/your/cwd) qwen serve: bound to workspace "/your/cwd" qwen serve: trusted loopback mode; local callers have full API access without bearer authentication, including code execution as the daemon user. Use --require-auth with QWEN_SERVER_TOKEN on shared or untrusted hosts.

Open http://127.0.0.1:4170/ in a browser to get the Web Shell UI: chat, session list, and workspace inspection. This token-less loopback primary listener grants local callers full operator API authority, while preserving workspace trust, session ownership, client-id, permission, feature, validation, and resource checks. createServeApp() mounts the bundled Web Shell assets (packages/cli/src/serve/web-shell-static.ts) before bearerAuth, so the shell itself loads without a token; its own API calls carry the bearer when one is configured — start the daemon with --open (which puts the token in the URL fragment, never sent to the server) or append #token=… manually when auth is enabled. --no-web opts out and leaves the daemon API-only.

2. Launch recipes

# 1. Local dev default (loopback, no token) qwen serve # 2. Explicit workspace + ephemeral port qwen serve --workspace /path/to/repo --port 0 # 3. Hardened loopback development (force bearer even on loopback) QWEN_SERVER_TOKEN=$(openssl rand -hex 32) qwen serve --require-auth # 4. Expose to LAN (a stable token; without one the daemon generates # an ephemeral bearer and prints it once) QWEN_SERVER_TOKEN=$(openssl rand -hex 32) \ qwen serve --hostname 0.0.0.0 --port 4170 # 4b. Expose to LAN with the generated ephemeral bearer qwen serve --hostname 0.0.0.0 --port 4170 # 5. Tune for many sessions and a larger replay ring qwen serve --max-sessions 0 --event-ring-size 32000 # 6. Multi-client collaboration + strict MCP budget QWEN_SERVER_TOKEN=secret \ qwen serve --require-auth \ --mcp-client-budget 10 \ --mcp-budget-mode enforce # 7. Start with a consensus policy configured in settings.json # settings.json: { "policy": { "permissionStrategy": "consensus", "consensusQuorum": 2 } } qwen serve # 8. Debug logging QWEN_SERVE_DEBUG=1 qwen serve # 9. Disable the F2 pool (fallback to per-session MCP clients) QWEN_SERVE_NO_MCP_POOL=1 qwen serve # 10. Allow browser web UI cross-origin access QWEN_SERVER_TOKEN=secret \ qwen serve --allow-origin 'http://localhost:3000' # 11. Prompt deadline + SSE idle timeout qwen serve --prompt-deadline-ms 300000 --writer-idle-timeout-ms 600000 # 12. Keep an idle ACP child reusable for at least 60s after work drains qwen serve --channel-idle-timeout-ms 60000 # 13. Enable HTTP rate limiting QWEN_SERVE_RATE_LIMIT=1 qwen serve

With the hardened loopback recipe (3), /health is registered after bearerAuth, so probes must carry the token like every other API route (the Web Shell static surface stays pre-auth by design; pass --no-web for an API-only daemon).

3. Full startup flags

The CLI is defined in packages/cli/src/commands/serve.ts:

FlagTypeDefaultRequired whenEffect
--port <n>number4170-TCP port; 0 means OS-assigned ephemeral port.
--hostname <host>string127.0.0.1Non-loopback refuses on an empty token source or a localhost resolving off-loopback with no token sourceBind address. Loopback values: any 127.0.0.0/8 address, localhost, ::1, [::1]; localhost is resolved once and pinned, and trusted mode verifies the actual listener address before startup completes. [::1] brackets are stripped automatically; host:port input is rejected with guidance to use --port. A non-loopback result always carries a bearer: with neither --token nor QWEN_SERVER_TOKEN, boot generates an ephemeral one instead of refusing. Carve-out: generation keys on the spelling, so a localhost that resolves off-loopback never generates, and boots only when a token source resolved (the refusal reads the resolved address), while a non-literal name that resolves to loopback generates and prints its bearer token-only.
--token <s>stringenv / noneNo valid source needed on a non-loopback spelling (generated instead — but a blank value refuses there); on loopback: --require-auth, --allow-origin '*', or a non-loopback HTTP(S) --allow-originBearer token; trimmed once. An explicitly empty value is a supplied source, so it shadows QWEN_SERVER_TOKEN and resolves to no token — a non-loopback bind then refuses to boot, as does a localhost bind whose resolution lands off-loopback with no other token source (generation never runs for it). It appears in /proc/<pid>/cmdline, so prefer QWEN_SERVER_TOKEN. Boot stderr also warns about this.
--max-sessions <n>number32-Per-workspace active session cap. Excess spawn returns 503. 0 means unlimited. NaN / negative values throw.
--max-total-sessions <n>number800, or derived at capacity 25 or less-Daemon-wide active session cap. When omitted, it is 800 at registration capacities above 25 (including the default 256), even with one workspace; at capacities of 25 or less it is derived once from the per-workspace cap and the startup/restored workspace count, and one such workspace is unlimited. Dynamic registration does not recompute it. 0 means unlimited.
--memory-budget-mb <n>integer in [1024, 1048576]50% of cgroup/host memory-Total memory budget for the daemon process tree, capped at resolved available memory. No child is sized from it; the one consumer today is the adaptive live-journal growth pool (see --max-journal-bytes). Reported under limits.memory, including a modeled per-child partition.
--max-journal-events <n>positive safe integer10000-Per-session baseline cap on in-flight liveJournal replay entries. Adaptive growth can raise it (see --max-journal-bytes); pinning either journal flag disables growth.
--max-journal-bytes <n>positive safe integer8388608-Per-session baseline byte cap on the in-flight liveJournal. Breaching turns grow the caps on demand (toward double, limited by remaining pool headroom) within one daemon-wide pool of 5% of the effective --memory-budget-mb (capped at 1024 MB; 0 — growth disabled — when the effective budget falls below the 1024 MB minimum), never past a 256 MiB per-session hard cap; pinning either journal flag disables growth.
--memory-pressure-mode <mode>off | observeobserveObservation onlyReports runtime.memory.pressure in both modes; only observe raises the daemon_memory_pressure issue. Root process only.
--child-heap-mode <mode>off | observeobserveObservation onlyUnder observe, reports the modeled partition under limits.memory.childHeap; applies nothing and refuses nothing. Under off, that block’s two figures are null.
--max-pending-prompts-per-session <n>number5-Accepted but pending/running prompt cap per session. Excess prompt returns 503. 0 / Infinity means unlimited. Negative or non-integer values throw.
--workspace <dir>string / repeatableprocess.cwd()-Startup workspace runtime; repeat to register additional isolated runtimes. The first is primary. Each value must be an absolute path, must exist, and must be a directory. Boot canonicalizes every value via canonicalizeWorkspace. POST /session with a mismatched cwd returns 400 workspace_mismatch.
--max-connections <n>number256-Listener-level server.maxConnections. 0 / Infinity means unlimited. NaN / negative values fail boot to avoid fail-open behavior.
--require-authbooleanfalseToken source on loopbackExtends bearer auth to loopback and /health. Boot refuses to start when no token source resolves — a loopback-only fail-fast, because on a non-loopback bind the generated ephemeral token satisfies the flag. Loopback sources are --token, QWEN_SERVER_TOKEN, and --open-with-auth (which installs its own generated token before boot, so --require-auth --open-with-auth starts).
--enable-session-shellbooleanfalseBearer or trusted loopbackEnables direct POST /session/:id/shell execution. Callers must also send a session-bound X-Qwen-Client-Id.
--event-ring-size <n>number8000-Per-session SSE replay ring depth. Soft cap is MAX_EVENT_RING_SIZE = 1_000_000; out-of-range values throw during bridge construction.
--http-bridgebooleantrue-Bridge mode: production attempts to preheat one primary qwen --acp child and retries on first use after failure; trusted secondaries start one on demand, while untrusted secondaries cannot start ACP. Stage 2 in-process mode is not implemented yet; --no-http-bridge falls back and prints to stderr.
--mcp-client-budget <n>numbernoneRequired for mcp-budget-mode=enforceWorkspace MCP client cap. Must be a positive integer.
--mcp-budget-mode <m>'enforce' | 'warn' | 'off'warn when a budget is set, otherwise offenforce requires --mcp-client-budgetenforce refuses, warn only warns at 75%, off is observation only.
--allow-origin <pattern>repeatable stringnone-CORS allowlist that replaces the default Origin denial. Wildcard and non-loopback HTTP(S) origins require a resolved token; these guards read the token after generation, so on a non-loopback bind the ephemeral bearer satisfies them and the refusals are loopback-only.
--allow-private-auth-base-urlbooleanfalse-Allows localhost / private-network auth provider baseUrl installation. Use only for trusted local development.
--prompt-deadline-ms <n>numbernone-Server-side prompt wallclock limit in ms; timeout aborts the prompt.
--writer-idle-timeout-ms <n>numbernone-Per-SSE-connection idle timeout in ms.
--channel-idle-timeout-ms <n>non-negative integer0-ACP child auto-reap delay after runtime work drains. Plain preheat is preserved for first use. Active keepalive windows may extend the configured delay; the longer remaining delay wins.
--initialize-timeout-ms <n>number10000-ACP child startup deadline (channel factory + initialize handshake) and default request timeout (ms).
--session-reap-interval-ms <n>number60000-Session reaper scan interval. 0 disables it.
--session-idle-timeout-ms <n>number1800000-Disconnected-session idle timeout. 0 disables it.
--rate-limit / --no-rate-limitbooleanenv / off-Enables or disables per-tier HTTP rate limiting.
--rate-limit-prompt <n>number10--rate-limitPrompt requests per window.
--rate-limit-mutation <n>number30--rate-limitMutation requests per window.
--rate-limit-read <n>number120--rate-limitRead requests per window.
--rate-limit-window-ms <n>number60000--rate-limitRate limit window length; must be >= 1000.

4. Environment variables

EnvEquivalent flag / effect
QWEN_SERVER_TOKENEquivalent to --token; --token wins. Trimmed once at boot to avoid a trailing newline from cat token.txt. When neither source is set, a non-loopback bind generates an ephemeral bearer instead; the daemon never sets this variable itself. A supplied but blank source is never “absent”, so it always suppresses that generation — but blankness decides the resolved token in one direction only: a blank --token shadows a set value here and resolves to no token (a non-loopback bind then refuses), whereas a blank value here resolves to no token only when --token is not passed — a non-blank --token still wins, and the daemon boots on it. A localhost bind whose resolution lands off-loopback likewise never generates, and boots only when a token source resolved (see --hostname).
QWEN_SERVE_DEBUG1 / true / on / yes (case-insensitive) enables verbose stderr logs.
QWEN_SERVE_NO_MCP_POOL1 disables the workspace MCP pool entirely and falls back to per-session McpClientManager. Capabilities stop advertising mcp_workspace_pool / mcp_pool_restart.
QWEN_SERVE_MCP_CLIENT_BUDGETACP-child internal budget input. The CLI generates it from --mcp-client-budget through childEnvOverrides; it is not a parent-process env fallback.
QWEN_SERVE_MCP_BUDGET_MODEACP-child internal budget mode. The CLI generates it from --mcp-budget-mode through childEnvOverrides; it is not a parent-process env fallback.
QWEN_SERVE_PROMPT_DEADLINE_MSEnv fallback for --prompt-deadline-ms.
QWEN_SERVE_WRITER_IDLE_TIMEOUT_MSEnv fallback for --writer-idle-timeout-ms.
QWEN_SERVE_MCP_POOL_TRANSPORTSRead by the ACP child. Comma-separated pooled transport allowlist; default is stdio,websocket.
QWEN_SERVE_MCP_POOL_DRAIN_MSRead by the ACP child. Pool entry idle drain delay; default is 30000, clamped to 1000..600000 ms.
QWEN_SERVE_RATE_LIMIT1 / true enables rate limiting; CLI flag wins.
QWEN_SERVE_RATE_LIMIT_PROMPTEnv fallback for --rate-limit-prompt.
QWEN_SERVE_RATE_LIMIT_MUTATIONEnv fallback for --rate-limit-mutation.
QWEN_SERVE_RATE_LIMIT_READEnv fallback for --rate-limit-read.
QWEN_SERVE_RATE_LIMIT_WINDOW_MSEnv fallback for --rate-limit-window-ms.

Per-handle env overrides are intentional: two daemons running in the same process do not race on process.env. defaultSpawnChannelFactory snapshots env at spawn time.

5. settings.json is also read

Boot calls loadSettings(boundWorkspace) once:

KeyTypeBehavior
policy.permissionStrategy'first-responder' | 'designated' | 'consensus' | 'local-only'Sets BridgeOptions.permissionPolicy. Boot validates with validatePolicyConfig; unknown values throw InvalidPolicyConfigError instead of falling back silently.
policy.consensusQuorumpositive integerN for the consensus policy. Default is floor(M/2)+1. If set under a non-consensus policy, it is ignored and boot logs a stderr warning.
context.fileNamestringControls which file POST /workspace/init writes through the workspace-service contextFilename.
tools.disabledstring[]Normalized through normalizeDisabledToolList() (trim, drop empty entries, dedupe) before affecting the next ACP child spawn.
tools.approvalModestringDefault session approval mode.
telemetryobjectOTel configuration: enabled, otlpEndpoint, otlpProtocol, per-signal endpoints, and more. See 17-configuration.md.

Settings I/O failure, such as malformed JSON, falls back to defaults. InvalidPolicyConfigError is the exception: policy misconfiguration fails boot explicitly.

6. Boot refusal scenarios (explicit failures)

run-qwen-serve.ts intentionally throws instead of falling back in these cases:

A non-loopback bind with NO token source at all is not a refusal: the daemon generates an ephemeral 128-bit bearer (16 random bytes, 22 base64url characters), prints it once at startup, and rotates it on every restart (see the remote quickstart in docs/users/qwen-serve.md). Loopback spellings never generate. The --open-with-auth generator is separate and larger — 32 bytes, a 256-bit bearer — and only runs on loopback.

Every token guard below reads the resolved token, i.e. after that generation step. That is what makes the --require-auth and --allow-origin refusals loopback-only: on a non-loopback bind a generated bearer is already in place.

ScenarioError prefix
Non-loopback bind with an explicitly empty token source (--token '' / QWEN_SERVER_TOKEN='')Refusing to bind ... without a bearer token
--require-auth on a loopback bind with no token source (--token, QWEN_SERVER_TOKEN, --open-with-auth)Refusing to start with --require-auth set but no bearer token
--workspace does not exist, is not a directory, or is not absoluteInvalid --workspace ...
--workspace stat permission deniedInvalid --workspace ...: permission denied
--mcp-client-budget is not a positive integerMust be a positive integer
--mcp-budget-mode=enforce without budgetrequires a positive mcpClientBudget
--hostname is written as localhost:4170looks like a "host:port" combination. Use --port
--hostname [::1]:8080Invalid --hostname ... brackets indicate an IPv6 literal but the value is not a clean [addr] form
--max-connections is NaN or negativeMust be >= 0
--event-ring-size > 1_000_000Thrown during bridge construction
--allow-origin '*' on a token-less loopback bindRefusing to start with --allow-origin '*' but no bearer token configured
Non-loopback HTTP(S) --allow-origin on a token-less loopback bindRefusing to start with --allow-origin ... but no bearer token configured
--prompt-deadline-ms / --writer-idle-timeout-ms is not a positive integerMust be a positive integer
--initialize-timeout-ms is not a positive integer or exceeds 2^31-1Must be a positive integer / Exceeds maximum JS timer delay
Unknown policy.permissionStrategy or non-positive policy.consensusQuorumInvalidPolicyConfigError

7. Curl verification checklist

# 1. Liveness curl http://127.0.0.1:4170/health # -> {"status":"ok"} # 1.1 Deep health curl -s 'http://127.0.0.1:4170/health?deep=1' | jq # 2. Capabilities curl -s http://127.0.0.1:4170/capabilities | jq # 3. Preflight readiness curl -s http://127.0.0.1:4170/workspace/preflight | jq # 4. Env snapshot (secrets only report presence) curl -s http://127.0.0.1:4170/workspace/env | jq # 5. MCP pool / budget snapshot curl -s http://127.0.0.1:4170/workspace/mcp | jq # 6. Create a session curl -s -X POST http://127.0.0.1:4170/session \ -H 'Content-Type: application/json' \ -H 'X-Qwen-Client-Id: curl-debug' \ -d '{}' | jq # 7. Tail SSE (replace <sid>) curl -N \ -H 'Accept: text/event-stream' \ -H 'X-Qwen-Client-Id: curl-debug' \ -H 'Last-Event-ID: 0' \ 'http://127.0.0.1:4170/session/<sid>/events' # 8. Web Shell UI open http://127.0.0.1:4170/

When bearer auth is enabled, add -H "Authorization: Bearer $QWEN_SERVER_TOKEN" to every request. Under the generated-token flow that variable is never set — the daemon prints the bearer once at startup and does not export it — so paste the printed value into your own shell first:

TOKEN='<printed bearer>' curl -s -H "Authorization: Bearer $TOKEN" http://<bind>:4170/capabilities | jq

8. Is there a browser UI?

Yes — the Web Shell. resolveWebShellDir() finds the built assets (bundled next to the CLI bundle in a release, packages/web-shell/dist in a checkout) and mountWebShellAssets() serves them at /, /assets, and /session/:id document navigations (browser deep links — a plain curl /session/<id> gets the API’s 401/404, not the shell). When the assets are missing the daemon degrades to API-only instead of crashing; --no-web opts out explicitly.

The static shell is mounted before bearerAuth in every launch mode — a browser cannot attach an Authorization header to an address-bar navigation or a <script src> subresource, so gating it would just break the UI. API calls use trusted-loopback authority on the token-less primary listener or attach the configured bearer in authenticated modes. On a non-loopback bind with a token the shell’s same-origin HTTP requests need no allowlist: an Origin equal to the direct socket scheme plus the normalized Host authority is bearer-authenticated and then stripped ahead of the CORS wall, so its POSTs go through. Three cases still need an explicit --allow-origin <origin> — the WebSocket upgrades behind the terminal and voice features; browsers reaching the daemon through a TLS-terminating proxy, whose https origin never matches the plain socket; and any plain-HTTP intermediary that rewrites the Host header (nginx’s default proxy_set_header Host $proxy_host, k8s Ingress), whose forwarded Host then no longer matches the browser’s Origin. Forwarded headers are never trusted to establish same-origin, so the fix is the allowlist entry or a proxy that forwards Host verbatim; port translation alone (docker -p 8080:4170) needs nothing on a non-loopback bind, because only the forwarded Host is compared — on the default loopback bind the Host gate rejects the translated port with 403 Invalid Host header for every request including the shell document, and --allow-origin cannot override it, so forward the same port (ssh -L 4170:localhost:4170) or bind non-loopback.

CSP is built by buildWebShellCsp() and is deliberately looser than a static page’s ('unsafe-inline' for the inline performance.measure patch, eval/wasm/blob workers for shiki and mermaid, data: for katex fonts, connect-src 'self' for SSE). frame-ancestors 'none' plus X-Frame-Options: DENY block clickjacking, except when an extension origin is explicitly allowed via --allow-origin so the UI can be hosted in a Chrome side panel (#5626).

For raw protocol inspection, subscribe to the SSE stream directly (routes/sse-events.ts) — see the curl recipes in section 7.

9. Call chain from qwen serve to the listening server

qwen serve | v (process) packages/cli/index.ts main() | v llm.tsx main() - parseArguments() | v (yargs assembly) config/config.ts import { serveCommand } ... config/config.ts .command(serveCommand) config/config.ts await yargsInstance.parse() | v (handler) commands/serve.ts handler(argv) - boot pre-checks commands/serve.ts const { runQwenServe } = await import('../serve/index.js') # lazy load commands/serve.ts await runQwenServe({...}) | v serve/run-qwen-serve.ts runQwenServe(opts, deps) | |- resolve token (trim / env fallback / non-loopback ephemeral generation) | |- hostname mismatch fallback | |- auth preflight (reads the resolved token) | |- workspace validation + canonicalization | |- MCP budget validation + childEnvOverrides | |- loadSettings + validatePolicyConfig | |- PermissionAuditRing + publisher | |- resolveBridgeFsFactory | `- createHttpAcpBridge({...}) | v serve/run-qwen-serve.ts const app = createServeApp(opts, () => actualPort, {...}) | v serve/server.ts createServeApp() - builds Express app (**does not listen**) | |- middleware chain (loopback Origin strip / access log + trace id / Host allowlist / remote same-origin strip / CORS / pre-auth health + shell / webhooks / bearerAuth / rate limit / JSON / telemetry / per-route mutation gate) | |- route mounting (health / web-shell static / capabilities / workspace / session / SSE / ACP HTTP) | `- return app | v serve/run-qwen-serve.ts server = createServer(app) / https.createServer(..., app) | |- lifecycle.bindServer(server, { startupReady, drainHost }) | |- server.listen(port, hostname) | |- server.maxConnections = cap | |- actualPort = server.address().port | |- write "qwen serve listening on ..." | |- register SIGINT / SIGTERM (onSignal) | `- resolve(handle: RunHandle) | v commands/serve.ts await blockForever() // block forever until signal

Key facts:

  • createServeApp only builds; it does not listen. It returns an express() instance with middleware and routes mounted. Ordinary-only embedders may continue to own app.listen(). Embedders that use Live/Conversations must bind the actual Node server to the exported app lifecycle before listening and await that lifecycle during shutdown.
  • () => actualPort is a lazy closure. actualPort is assigned in the server.listen callback. The hostAllowlist middleware reads it on demand, so ephemeral ports (--port 0) still gate the Host header correctly.
  • await blockForever() is intentional. If yargs.parse() resolves, the CLI top level falls through into the interactive TUI entrypoint (llm.tsx). SIGINT / SIGTERM exit through runQwenServe’s onSignal path.

10. HTTP route file split

The main assembly happens in createServeApp() in server.ts, which wires middleware and mounts focused route modules:

RoutesFileMounting entry
/healthpackages/cli/src/serve/routes/health.tshealthRoutes.register()
/daemon/statuspackages/cli/src/serve/routes/daemon-status.tsregisterDaemonStatusRoutes()
/capabilities, workspace init/tool/MCP mutation routes, ACP HTTP bridgepackages/cli/src/serve/server.tsRegistered directly inside createServeApp()
Workspace status, env, preflight, MCP/tool/provider/skill summariespackages/cli/src/serve/routes/workspace-status.tsregisterWorkspaceStatusRoutes(), registerWorkspaceDiagnosticStatusRoutes()
Workspace extensions and extension operationspackages/cli/src/serve/routes/workspace-extensions.tsregisterWorkspaceExtensionRoutes()
/workspace/memory (GET/POST)packages/cli/src/serve/workspace-memory.tsmountWorkspaceMemoryRoutes()
All /workspace/agents CRUD routespackages/cli/src/serve/workspace-agents.tsmountWorkspaceAgentsRoutes()
GET /file, /file/bytes, /list, /glob, /statpackages/cli/src/serve/routes/workspace-file-read.tsregisterWorkspaceFileReadRoutes()
POST /file/write, /file/editpackages/cli/src/serve/routes/workspace-file-write.tsregisterWorkspaceFileWriteRoutes()
Workspace setup, trust, settings, permissions, and voice routespackages/cli/src/serve/routes/workspace-*.tsregisterWorkspaceSetupGithubRoutes(), registerWorkspaceTrustRoutes(), etc.
Workspace auth provider and device-flow routespackages/cli/src/serve/routes/workspace-auth.tsregisterWorkspaceAuthRoutes()
Session lifecycle, prompt, metadata, language, shell, recap, rewind, branch, and list routespackages/cli/src/serve/routes/session.tsregisterSessionRoutes()
GET /session/:id/events SSE streampackages/cli/src/serve/routes/sse-events.tsregisterSseEventsRoutes()
Permission response routespackages/cli/src/serve/routes/permission.tsregisterPermissionRoutes()

For the complete route and wire protocol reference, see ../qwen-serve-protocol.md. For architecture, see 01-architecture.md.

11. Graceful vs hard shutdown

  • First SIGINT / SIGTERM -> runQwenServe onSignal -> two-phase graceful shutdown:
    1. bridge.shutdown(): each channel gets KILL_HARD_DEADLINE_MS (10s), then channel.kill().
    2. server.close(): in-flight requests drain, SHUTDOWN_FORCE_CLOSE_MS (5s) triggers closeAllConnections(), then a second 2s deadline applies.
  • Second SIGINT / SIGTERM while already exiting -> bridge.killAllSync() synchronously SIGKILLs all ACP children and calls process.exit(1) to avoid orphan processes.

RunHandle.close() returned by runQwenServe is the programmatic equivalent used by repository-internal hosts and tests.

12. Embedding boundary

runQwenServe, createServeApp, and their lifecycle helpers are internal implementation APIs; the published @qwen-code/qwen-code package does not export a ./serve subpath. External integrations should start qwen serve --no-web and use the documented HTTP/SSE protocol or @qwen-code/sdk. Repository code and tests may import the source modules directly, but those imports are not a supported integration contract.

For repository-internal callers of createServeApp, the default fsFactory.trusted = false. Agent-side ACP writeTextFile is rejected as untrusted_workspace, and a stderr warning is printed once. Either inject deps.fsFactory with explicit trust, inject deps.bridge, or accept the trust-gated default behavior.

13. Debugging recipes

See the debugging section in 19-observability.md. The common commands are:

# Is the daemon alive? curl http://127.0.0.1:4170/health # Which capabilities are advertised? curl -s http://127.0.0.1:4170/capabilities | jq # Daemon-host readiness curl -s http://127.0.0.1:4170/workspace/preflight | jq # Tail live SSE curl -N -H 'Accept: text/event-stream' \ -H 'Last-Event-ID: 0' \ 'http://127.0.0.1:4170/session/<sid>/events' # Verbose logs QWEN_SERVE_DEBUG=1 qwen serve

References

  • CLI entry: packages/cli/src/commands/serve.ts
  • Bootstrap: packages/cli/src/serve/run-qwen-serve.ts
  • Express factory: packages/cli/src/serve/server.ts
  • Middleware: packages/cli/src/serve/auth.ts
  • Bridge factory: packages/acp-bridge/src/bridge.ts
  • Web Shell static mount: packages/cli/src/serve/web-shell-static.ts
  • User docs: ../../users/qwen-serve.md
  • Wire protocol: ../qwen-serve-protocol.md
Last updated on