You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
> **Deprecated**: this document describes the early `DaemonTuiAdapter` spike. The legacy adapter still exists in `packages/cli/src/ui/daemon/`, but the reusable direction is now the SDK shared UI transcript layer. For the current architecture, see [`../daemon/14-cli-tui-adapter.md`](../daemon/14-cli-tui-adapter.md).
A`qwen serve`process is **one daemon = one workspace**. It hosts a single Express HTTP server, owns an `acp-bridge`instance, and spawns one ACP child process (`qwen --acp`) that runs the actual agent runtime. Multiple clients (CLI TUI, IDE companion, IM channel bots, web BFFs, custom scripts) connect over HTTP + SSE and either share one ACP session (`sessionScope: 'single'`, default) or split sessions by conversation thread (`sessionScope: 'thread'`).
Inside the ACP child, MCP servers are shared workspace-wide through `McpTransportPool` (F2): a single (server-name + config-fingerprint) tuple maps to one MCP transport, regardless of how many sessions discover it. The bridge's `MultiClientPermissionMediator` (F3) coordinates permission votes across all connected clients under one of four policies.
This doc gives the **system-level picture** that every other doc in this set hangs off. Each load-bearing flow is shown as a Mermaid sequence diagram; per-component implementation details live in the other 18 docs.
The daemon process and the ACP child are connected by an `AcpChannel` (default: a real subprocess pair-of-pipes; `inMemoryChannel` for tests). Everything the daemon does is shaped by this split: HTTP and SSE traffic terminate in the daemon; agent decisions and tool invocations happen in the child; the bridge is the seam.
Three trust boundaries to keep in mind: the HTTP edge (`serve/auth.ts` middleware chain), the bridge ↔ ACP-child seam (NDJSON over stdio, no auth — the child trusts the bridge implicitly), and the agent ↔ MCP server seam (the agent may invoke tools that touch the host).
150
136
151
-
## 流程 1:HTTP 请求生命周期
137
+
## Workflow 1: HTTP request lifecycle
152
138
153
139
```mermaid
154
140
sequenceDiagram
155
141
autonumber
156
142
participant C as Client (SDK)
157
-
participant MW as Middleware<br/>(CORS→host→log→bearer→rate-limit→JSON→telemetry)
143
+
participant MW as Middleware<br/>(bearer→host→CORS→mutationGate)
158
144
participant R as Route handler
159
145
participant BR as AcpBridge
160
146
participant BC as BridgeClient
161
147
participant CH as ACP child
162
148
163
149
C->>MW: POST /session/:id/prompt<br/>Authorization: Bearer …<br/>X-Qwen-Client-Id: …
Non-streaming routes (prompt, cancel, model switch, metadata, workspace CRUD) terminate as a single JSON reply. Streaming output is delivered out-of-band on the SSE channel, **not** as a chunked HTTP body on this connection. See workflow 2.
183
165
184
-
## 流程 2:SSE 事件投递与重放
166
+
## Workflow 2: SSE event delivery and replay
185
167
186
168
```mermaid
187
169
sequenceDiagram
@@ -203,13 +185,9 @@ sequenceDiagram
203
185
Note over EB,SR: If subscriber queue >= maxQueued,<br/>EventBus emits client_evicted terminal frame<br/>and closes subscriber.
204
186
```
205
187
206
-
要点:
188
+
The ring buffer is bounded (`eventRingSize`, default 1024). A reconnecting client whose `Last-Event-ID` is older than the ring's head receives a synthetic catch-up signal and must call `loadSession` / `resumeSession` to rebuild deeper state. Slow clients trigger `slow_client_warning` at 75% queue fill and `client_evicted` at the cap.
Cross-policy escape hatch: any client may vote `CANCEL_VOTE_SENTINEL` to short-circuit the request as `cancelled / agent_cancelled`. The bridge guards against wire callers smuggling the sentinel via the normal `optionId` field (`InvalidPermissionOptionError`).
`releaseSession(sessionId)` uses the reverse `sessionToEntries` index to release every entry the session holds in O(refs). On daemon shutdown, `drainAll()` sets the `draining` flag (refusing new acquires) and waits for every entry to close under a configurable timeout.
315
282
316
-
## 流程 5:生命周期 —— 启动与优雅退出
283
+
## Workflow 5: Lifecycle — startup and graceful shutdown
317
284
318
285
```mermaid
319
286
sequenceDiagram
@@ -342,35 +309,30 @@ sequenceDiagram
342
309
Note over Op,RQS: Second SIGTERM during shutdown →<br/>bridge.killAllSync() + process.exit(1) (orphan prevention)
The two-phase shutdown matters because in-flight HTTP requests, in-flight SSE subscribers, and the ACP child's in-flight tool calls all need bounded teardown windows. If anything blocks past those deadlines, the force-close path takes over so a stuck child can't keep the daemon process alive.
0 commit comments