#12116·qwen-code

bug(daemon): intermittent POST /session 504 — AcpSessionBridge newSession times out after 10000ms on shared ACP child

Author: samuelhsinCreated Sep 17, 2026Updated Sep 17, 2026
Labelspriority/P2type/bugcategory/corescope/session-managementstatus/ready-for-humandaemon

What happened?

Creating a new session intermittently fails in a running qwen serve deployment. The UI reports an error when the user submits the first message because POST /session fails before that message can be submitted to the new session.

The daemon reports an ACP newSession timeout, not a model response timeout:

qwen serve: newSession timed out after 10000ms on channel <channel-id>; decision=fence_shared

[WARN] [DAEMON] route=POST /session
  errorType=BridgeTimeoutError
  AcpSessionBridge newSession timed out after 10000ms

[WARN] [DAEMON] route=POST /session
  clientId=webui_<client-id> durationMs=10010 status=504

The forwarding service recorded the same request as HTTP 504 after 10014ms, consistent with the inner daemon timeout being propagated. Increasing an outer HTTP/proxy timeout alone would not remove this inner deadline.

Identifiers in the excerpts are placeholders. Error strings and measured durations are preserved from the observed incident.

Supporting evidence

The same running ACP child had intermittent responsiveness problems around the incident:

Observation Measured result
ACP event-loop stall, earlier in the same runtime 4127.195135ms
ACP event-loop stall, later in the same runtime 11785.994239ms
Existing-session load requests around the incident 21662ms and 27936ms, both eventually HTTP 200
Extension refresh Multiple POST /workspace/extensions/refresh failures: extension refresh timed out after 30000ms
New-session creation BridgeTimeoutError, HTTP 504 after 10010ms

The event-loop stall observations are not samples taken inside the exact failing newSession call. They establish concurrent responsiveness issues, but do not identify the function responsible for this specific timeout.

Inspection of the installed bridge bundle confirmed that its newSession request uses initTimeoutMs and rejects with BridgeTimeoutError("newSession", initTimeoutMs). The incident's effective deadline was 10000ms.

Reproduction status

Observed in a downstream deployment; not yet reproduced on an unmodified upstream release, and no deterministic minimal reproducer is available.

Observed workflow:

  1. Run a daemon with a shared ACP child and existing sessions.
  2. Use the web UI to create another session and submit its first message.
  3. Intermittently, POST /session returns 504 at the 10-second internal deadline.
  4. Check daemon logs for newSession timed out and decision=fence_shared.

No user prompts, session transcripts, credentials, internal endpoints, or raw diagnostic bundles are attached.

What did you expect to happen?

  • New-session creation should remain usable under transient load, with a clearly defined and configurable creation budget if it differs from the ACP handshake budget.
  • When the deadline expires, clients should be able to distinguish an unstarted request from an operation that may still complete in the ACP child, so they can avoid duplicate sessions or unsafe automatic retries.
  • Diagnostics should identify the slow creation phase (for example, queued work, configuration/extension refresh, or initialization) and expose the settlement outcome after the public timeout.
  • Existing sessions sharing the child should remain usable when a new-session operation times out.

Client information

  • Linux container, CPU quota equivalent to 2 CPUs.
  • Installed Qwen Code package version: 0.24.0-dataworks.0 (downstream distribution; upstream commit mapping not established).
  • qwen serve daemon with an ACP child and embedded web UI.
  • Full /about output was not captured; the version above was read from the installed package metadata.

Login information

Authentication mode was not collected. Authenticated instance access succeeded; the observed failure was the daemon's BridgeTimeoutError. Credentials are intentionally omitted.

Additional context / possible confounders

Earlier, this container exhausted its inotify watch quota (8192 watches). To restore a separate MCP controller's configuration watcher, Qwen's chokidar watchers were switched to polling and Qwen was restarted:

CHOKIDAR_USEPOLLING=1
CHOKIDAR_INTERVAL=1000
CHOKIDAR_BINARY_INTERVAL=1000

The new-session timeout above occurred after that change. Polling may contribute overhead and should be considered when reproducing. However, an ACP event-loop stall of 3592.421375ms and an extension-refresh timeout were also logged before the change. We have not established polling as the cause.

A later 5-second CPU sample showed no increase in cgroup CPU throttling counters. This was not the failure interval, so it cannot rule out resource pressure during the incident.

Related: #12021. That report concerns existing-session load / channel.initialize timeout. This report concerns POST /session / newSession timeout on a shared child, with decision=fence_shared. We are not claiming the same cause or a child SIGKILL in this incident.

Requested investigation

  1. Confirm the intended newSession deadline and its relationship to the initialization timeout.
  2. Add or identify phase-level diagnostics for creation latency and event-loop stalls.
  3. Verify that late completion / fencing / cleanup after a public timeout preserves existing sessions and does not leak or duplicate the requested session.
  4. Add a regression scenario where a shared child's newSession response exceeds 10 seconds; document the expected client recovery behavior.

Expanded incident timeline: daemon startup → failed creation → later successful creation

This section correlates the daemon's own log, its captured stderr, and the forwarding service's access log. T+00:00.000 is the new daemon's HTTP-listening event; calendar times, process IDs, ports, workspace paths, and all request/session/channel identifiers have been removed. Relative order and measured durations are retained. S-A and S-B represent different existing sessions; R-fail and R-success represent different creation requests. Times from the forwarding service have second precision and are marked approximately.

This is the complete relevant lifecycle timeline available from these logs, not an unfiltered raw-log dump or a claim that every internal phase was logged. The daemon also reports suppressed access-log entries; exact ACP spawn/handshake and extension-scan start/end timestamps are not present in the captured evidence.

Relative time Component / event Result and interpretation
T−00:05.199 Previous daemon daemon stopped during the operator-requested restart to apply watcher polling. Existing SSE streams close; these closures precede the incident and belong to the restart.
Approximately T−00:01.5 Forwarding service Logs spawning the replacement daemon.
T+00:00.000 New daemon qwen serve listening ... (mode=http-bridge, workspace=<workspace>). Startup timing: processToListenMs=1062, runQwenServeToListenMs=696.
T+00:00.001 New daemon deferred runtime: scheduling fallback start in 1000ms. HTTP listening does not mean deferred runtime/session initialization has finished.
T+00:00.211 New daemon deferred runtime: health served, scheduling start in 50ms.
T+00:00.719 New daemon Workspace roots initialized.
T+00:00.761 / 00:00.991 New daemon Session reaper starts; ACP WebSocket endpoint enabled. These messages do not establish when the ACP child handshake completed.
Approximately T+00:00.5 Forwarding service Normal daemon restart API reports success.
Approximately T+00:01.5 and 00:03.5 Host integration Two POST /extension-sync requests begin. This is a host endpoint; it must not be conflated with Qwen's POST /workspace/extensions/refresh.
T+00:02.941 Daemon extension catalog GET /workspace/extensions, HTTP 304, 830ms.
T+00:03.785 Daemon extension catalog Another GET /workspace/extensions, HTTP 304, 13ms. A catalog response is not evidence of a full rescan on every GET.
T+00:04.622 Daemon skill catalog GET /workspace/skills, HTTP 200, 1296ms.
T+00:12.153 MCP lifecycle POST /workspaces/<workspace>/mcp/reload, HTTP 202, 5ms. This is acceptance of discovery work, not proof of completion by itself. Later independent checks confirmed the MCP was connected with 9 valid tools.
Approximately T+01:16–01:37 Host integration Update checks run for skills, extensions, and combined scope. Extension results report no available updates. These are additional host-side activities, not evidence that Qwen downloads extensions on every startup.
T+01:46.351 / 01:46.396 Existing session S-A Two load requests complete successfully in 2628ms / 35ms.
T+01:51.232 Existing session S-A POST /session/<S-A>/prompt, HTTP 202, 21ms. The child was capable of accepting work before the later failure.
T+01:55.718 Same ACP child Event-loop stall reported: 4127.195135ms.
T+03:25.570 Existing session S-B Prompt accepted: HTTP 202, 8ms.
T+03:38.556 Same ACP child Event-loop stall reported: 11785.994239ms.
T+03:39.526 Daemon / S-B AcpSessionBridge qwen/status/session/agents timed out after 10000ms. Another operation is affected before the failing creation.
T+04:50–04:52 Existing-session loads Requests eventually return HTTP 200 after 15964ms, 27570ms, and 11232ms.
Approximately T+05:24.5 Host integration Another POST /extension-sync begins. No matching evidence establishes that this request caused R-fail.
Approximately T+06:03.5 Forwarding service / R-fail POST /session starts.
T+06:14.051 Daemon / R-fail newSession timed out after 10000ms ... decision=fence_shared.
T+06:14.052–06:14.054 Daemon / R-fail BridgeTimeoutError, AcpSessionBridge newSession timed out after 10000ms; HTTP 504, 10010ms. Forwarder records the matching 504 after 10014ms.
T+06:14.266 ACP child Logs Session initialization deadline exceeded, errorKind: session_initialization_timeout. The associated initialization metadata refers to R-fail. This is child-side evidence about the same failed initialization, approximately 0.21 seconds after the public timeout, not merely an outer proxy timeout.
T+06:52.137 / 06:52.827 / 06:55.498 Daemon extension refresh Three POST /workspace/extensions/refresh errors: extension refresh timed out after 30000ms. One access record is HTTP 500, 30018ms. These errors occur after R-fail. Inferring their request starts by subtracting 30 seconds still places them after the creation failure; they cannot establish that these exact refresh calls blocked R-fail.
T+06:56.502–06:56.681 Existing session S-B Load requests complete with HTTP 200 after 21662ms, 9371ms, and 27936ms.
T+08:20.205–08:20.210 Daemon / R-success A different new session is spawned; POST /session returns HTTP 200 after 5890ms. Do not interpret this as late success of R-fail.
T+08:21.641 / 08:22.237 New session from R-success Model selection returns HTTP 200 (326ms), then prompt submission is accepted with HTTP 202 (45ms). This confirms subsequent creation/submission recovery, not completion of the model answer.

Does daemon startup scan extensions?

The installed runtime does perform awaited extension discovery during configuration initialization, but HTTP startup, configuration initialization, catalog reads, and explicit refresh requests are different events.

The installed downstream bundle contains this sequence in Config.initializeInternal (equivalent source location: packages/core/src/config/config.ts):

typescript
recordStartupEvent("config_initialize_extensions_initial_start");
if (!this.isSafeMode() && !this.getBareMode()) {
  await this.extensionManager.refreshCache();
} else if (!this.isSafeMode() && explicitExtensionNames.length > 0) {
  await this.extensionManager.refreshCache({ names: explicitExtensionNames });
}
recordStartupEvent("config_initialize_extensions_initial_end");
options?.signal?.throwIfAborted();
// Hook initialization follows.

The installed ExtensionManager.refreshCache() delegates to refreshCacheWithSnapshot(). Without a name filter, that method calls:

typescript
await this.extensionStore.readConsistent(async () => {
  const loaded = await this.loadExtensionsFromExtensionsDir(
    this.configDir,
    this.workspaceDir,
  );
  // Build the next extension cache and apply activation state.
});

The implementation distinguishes a cheap directory fingerprint from a full refresh; its comment describes refreshCache() as parsing manifests and re-listing extension skill directories. Thus the awaited refresh includes disk discovery, rather than only reading an already-populated in-memory map. This alone does not mean all extensions are activated, all MCPs are started, or remote updates are downloaded.

Separately, the installed bridge exposes refreshExtensionsForAllSessions(): it enumerates live sessions, issues the workspace-extension-refresh control method to their ACP connections, and tracks in-flight refreshes by session and refresh scope. Please investigate how this work is scheduled relative to newSession on a shared child. We are not claiming there is no refresh deduplication.

What is still missing: per-phase timestamps for the failed initialization, extension count/scan duration for that operation, lock/queue wait duration, and the final settlement/cleanup of R-fail. The later successful request does not answer the settlement question. Please correlate the startup markers (extensionsInitialStart/End, hooks, skills, final extensions, etc.) with session-level initialization deadlines so a future occurrence identifies the actual slow phase.

Additional attribution caution

Host event-loop lag warnings also occurred during this window (including 3015ms, 3815ms, and later 9087ms). These are from a different process than the ACP child's 11785.994239ms stall and should not be merged into a single measurement. The combination supports investigating shared runtime load and scheduling, but the current evidence does not prove extension scanning, polling, or host integration activity individually caused R-fail.

Startup and session-initialization work inventory (extensions, skills, MCP, JSONL, and other phases)

This inventory is based on the installed downstream JavaScript bundle, in addition to the observed timeline above. It describes execution paths and their conditions; it is not a claim that every conditional branch ran during R-fail. Per-phase timings were not captured for that request. In particular, daemon HTTP startup and per-session Config.initialize are separate lifecycle boundaries.

1. Daemon boot and deferred runtime

The observed restart proceeds through process launch → HTTP listening → health response → deferred runtime scheduling → workspace roots / reaper / ACP endpoint setup. The installed run-qwen-serve implementation guards startRuntime() against duplicate starts, clears the scheduling timers, calls buildRuntime(), retains the bridge/app, optionally starts bridge preheat, then completes runtime startup. Runtime construction failure follows its startup-failure cleanup path.

When preheat is enabled, bridge.preheat() runs asynchronously; on success it schedules workspace MCP discovery/runtime setup. Its failure message says the ACP preheat will be retried on the first session. Thus HTTP listening, mounted runtime, ACP preheat, and MCP readiness are distinct signals. Exact preheat/handshake completion times and whether all optional boot work ran are unavailable in the incident logs.

Host integration separately requests extension synchronization, extension/skill catalogs, MCP reload, and update checks, as shown in the timeline. These requests can overlap runtime/session activity; they are not all intrinsic daemon boot operations.

2. New-session path and awaited configuration work

The installed ACP newSession() validates/reserves a requested session ID, creates the session-initialization deadline, and profiles these phases in order:

settings_load
  → config_setup (newSessionConfig / Config initialization)
  → auth (non-provisional session)
  → file_system_setup (non-provisional session)
  → session_register (createAndStoreSession)
  → response_build

The deadline signal is checked between stages and passed into configuration/session creation. Failure has configuration cleanup and deadline/reservation disposal paths. The following work exists inside configuration initialization:

Phase Installed behavior Blocking / interpretation
Transcript writer activation When recording and writer leasing are enabled, acquire SessionWriterLease, check the selected session's location, assert ownership, and activate its recorder. Existing transcript/restore projection is loaded when applicable. Register the session's project directory afterward. Awaited before initializeInternal(); writer ownership and transcript I/O deserve their own timing, not only extension timing.
Proxy and basic services Await proxy-dispatcher readiness; conditionally validate Omni dependencies; initialize file service for non-provisional workspaces and prompt/resource registries. Conditions depend on configuration; not evidence that Omni was enabled here.
Initial extensions Normal mode awaits extensionManager.refreshCache(); bare/safe behavior is gated. Disk discovery/cache refresh, including manifests and extension skill directories.
Hooks and subagents Initialize the hook system when enabled, register message-bus dispatch, and construct the subagent manager. Hook-system initialization does not mean every registered hook executes. Session-start hook execution should be separately timed where triggered.
Skills Unless skipped, construct SkillManager. Normal mode awaits startWatching(), which ensures the user skill directory, refreshes the cache, and updates watchers. Bare/safe paths refresh without the same watcher setup. Cache loading covers project, user, extension, and bundled skill levels in normal mode. The optional trusted-workspace auto-skill curator precedes this when enabled. This is additional filesystem work beyond extension scanning.
Final extensions Normal non-bare/non-safe mode awaits extensionManager.refreshCache() again. There are initial and final extension refresh stages in the installed path; their actual cost needs measurement.
Hierarchical instructions / memory Non-provisional initialization awaits refreshHierarchicalMemory("session_start", signal). Instruction-file discovery/loading is another initialization stage, separate from chat-history JSONL.
Tools, LLM client, warmup Create the tool registry; initialize the LLM client unless skipped/provisional; detect runtime model; non-provisional tool warmAll() and Omni policy initialization. These are additional awaited stages. Client initialization must not be confused with a successful model inference.
MCP discovery Default configuration skips inline discovery and can start discoverAllMcpToolsIncremental() in the background; readiness can be awaited later before model invocation. Do not assume all MCP connections block POST /session. QWEN_CODE_LEGACY_MCP_BLOCKING=1, bare/safe/provisional mode, and explicit skipMcpDiscovery change the path. ACP also derives a defer-discovery option from the request. The incident's exact effective branch was not captured.
Post-initialization background work Normal non-provisional path can launch a stale agent-worktree sweep asynchronously. Source-backed conditional work; not establishe