docs(developers): add daemon-mode developer deep-dive documentation set#4412
docs(developers): add daemon-mode developer deep-dive documentation set#4412doudouOUC wants to merge 1 commit into
Conversation
📋 Review SummaryThis PR adds a comprehensive 20-file bilingual developer documentation set for qwen-code daemon mode, totaling ~7,130 lines across 22 files. The documentation covers the complete daemon architecture including 🔍 General Feedback
🎯 Specific Feedback🟡 High
🟢 Medium
🔵 Low
✅ Highlights
🔍 Verification Checklist (for maintainers)Before merging, verify:
Recommendation: This is a high-quality documentation PR that significantly improves the developer experience for contributing to the daemon codebase. The minor discrepancies noted above (diagram count in PR description, glossary completeness) are easily addressable and don't block the merge. Consider merging after the PR description diagram count is corrected from 6 to 7. |
There was a problem hiding this comment.
Pull request overview
This PR adds a new developer deep-dive documentation set under docs/developers/daemon/ (bilingual EN/ZH) intended to document the qwen serve daemon architecture, its protocol surface, SDK client, and adapters, plus wires it into the developer docs navigation.
Changes:
- Added a 20-file daemon-mode deep-dive documentation set (
docs/developers/daemon/*.md) with diagrams, workflows, and component references. - Added daemon sub-navigation (
docs/developers/daemon/_meta.ts) and a top-level nav entry (docs/developers/_meta.ts).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/developers/_meta.ts | Adds the top-level “Daemon Mode (Developer Deep Dive)” nav entry. |
| docs/developers/daemon/_meta.ts | Adds the daemon-docs sidebar ordering/titles. |
| docs/developers/daemon/00-index.md | Index, reading order, glossary, and scope notes for the doc set. |
| docs/developers/daemon/01-architecture.md | System overview + Mermaid diagrams for major daemon flows. |
| docs/developers/daemon/02-serve-runtime.md | Documents qwen serve bootstrap/runtime responsibilities and flows. |
| docs/developers/daemon/03-acp-bridge.md | Describes the bridge layer and how daemon↔ACP communication is structured. |
| docs/developers/daemon/04-permission-mediation.md | Documents multi-client permission mediation policies and flows. |
| docs/developers/daemon/05-mcp-transport-pool.md | Documents the workspace-scoped MCP transport pool design/operation. |
| docs/developers/daemon/06-mcp-budget-guardrails.md | Documents MCP budget controller behavior and emitted events. |
| docs/developers/daemon/07-workspace-filesystem.md | Documents workspace filesystem boundary rules and error handling. |
| docs/developers/daemon/08-session-lifecycle.md | Documents session lifecycle, identity, and terminal frames. |
| docs/developers/daemon/09-event-schema.md | Documents the typed daemon event schema and reducers. |
| docs/developers/daemon/10-event-bus.md | Documents the in-memory SSE EventBus and backpressure behavior. |
| docs/developers/daemon/11-capabilities-versioning.md | Documents /capabilities, feature tags, and versioning rules. |
| docs/developers/daemon/12-auth-security.md | Documents auth middleware, host/CORS defenses, and device-flow auth. |
| docs/developers/daemon/13-sdk-daemon-client.md | Documents the TypeScript daemon client SDK architecture/surfaces. |
| docs/developers/daemon/14-cli-tui-adapter.md | Documents the CLI TUI daemon adapter behavior and event mapping. |
| docs/developers/daemon/15-channel-adapters.md | Documents IM channel adapters and their daemon bridge layer. |
| docs/developers/daemon/16-vscode-ide-adapter.md | Documents VSCode companion daemon adapter and loopback constraint. |
| docs/developers/daemon/17-configuration.md | Consolidated daemon configuration reference (flags/env/settings). |
| docs/developers/daemon/18-error-taxonomy.md | Error taxonomy + remediation guidance across daemon layers. |
| docs/developers/daemon/19-observability.md | Observability/debugging surfaces and operational recipes. |
Comments suppressed due to low confidence (2)
docs/developers/daemon/19-observability.md:21
- The table references
PermissionAuditRingatpermissionAudit.ts:1-60, butpackages/cli/src/serve/permissionAudit.tsdoes not exist in this branch. Please update the reference to the actual implementation (if renamed/moved) or mark the audit ring as not yet implemented here.
| `/demo` debug console | `GET /demo` (`packages/cli/src/serve/demo.ts`) | Browser-accessible single-page console (chat + event log + workspace inspector + permission UX). Open `http://127.0.0.1:4170/demo` on loopback — the fastest way to exercise the daemon end-to-end without writing SDK code. See [`02-serve-runtime.md`](./02-serve-runtime.md) for the loopback-vs-auth registration rules. |
| `PermissionAuditRing` | `permissionAudit.ts:1-60` | In-memory FIFO (512 entries) of permission decisions. |
| `mediator`'s `decisionReason` audit trail | `permissionMediator.ts:80-100+` | Internal structured "why did this resolve like that" records. |
docs/developers/daemon/17-configuration.md:57
- This row also mentions
InvalidPolicyConfigErrorforpolicy.consensusQuorum, but that error type isn't defined in this branch. Please update the described validation/behavior to what the current implementation actually enforces, so operators know whether invalid values fail boot or are ignored.
| `policy.consensusQuorum` | positive integer | N for the `consensus` mediator policy. **Default:** `floor(M/2) + 1` of `votersAtIssue.size` (unanimity for M=2; supermajority for larger even M). Setting this under a non-`consensus` strategy is silently ignored — a stderr warning fires at boot. Non-positive-integer values throw `InvalidPolicyConfigError`. See [`04-permission-mediation.md`](./04-permission-mediation.md). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| subgraph daemon["qwen serve process (one workspace)"] | ||
| EXP["Express app<br/>(packages/cli/src/serve/server.ts)"] | ||
| BR["AcpBridge<br/>(packages/acp-bridge/src/bridge.ts)"] |
| Note over EB,SR: If subscriber queue >= maxQueued,<br/>EventBus emits client_evicted terminal frame<br/>and closes subscriber. | ||
| ``` | ||
|
|
||
| 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. |
| Every SSE frame the daemon emits on `GET /session/:id/events` carries `{ id, v, type, data, originatorClientId?, _meta? }`. `v: 1` is the current `EVENT_SCHEMA_VERSION`. `type` is a string from a closed, version-pinned set of **29 known types** declared in `DAEMON_KNOWN_EVENT_TYPE_VALUES` (`packages/sdk-typescript/src/daemon/events.ts:13-63`). The envelope's `_meta` field is stamped at the SSE write boundary (`formatSseFrame()` in `server.ts`) — see [Envelope-level metadata](#envelope-level-metadata) below. | ||
|
|
|
|
||
| ## Overview | ||
|
|
||
| `qwen serve` ships **today** with debug logging, structured preflight cells, and an in-memory permission audit ring. It does **not** today ship OpenTelemetry spans, Prometheus metrics, or a structured-log format — those land in Stage 1.5+. This doc is a pragmatic guide for the current surface plus the gaps to be aware of when triaging issues. |
| ### Ring-eviction → `state_resync_required` (the recovery flow) | ||
|
|
||
| When a consumer reconnects with `Last-Event-ID: N` and the ring's earliest surviving event has `id > N + 1`, the events in `[N+1, earliestInRing-1]` were evicted before the consumer reconnected. The naïve replay would silently succeed with a non-contiguous suffix, the SDK reducer would keep applying deltas as if the stream were contiguous, and its state would diverge from the daemon's truth — with no terminal signal. | ||
|
|
||
| Implemented at `packages/acp-bridge/src/eventBus.ts:359-402`: |
| The daemon loads `settings.json` once at boot (`runQwenServe.ts:496+`) via `loadSettings(boundWorkspace)`. Corruption falls back to defaults (try/catch wraps the load). | ||
|
|
||
| | Key | Type | Effect | | ||
| | --------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `policy.permissionStrategy` | `'first-responder' \| 'designated' \| 'consensus' \| 'local-only'` | Sets `BridgeOptions.permissionPolicy`. Active value surfaces in `/capabilities`'s `policy.permission`. **Boot-validated** by `validatePolicyConfig()` against `SERVE_CAPABILITY_REGISTRY.permission_mediation.modes`; an unknown literal throws `InvalidPolicyConfigError` (boot fails loudly). | | ||
| | `policy.consensusQuorum` | positive integer | N for the `consensus` mediator policy. **Default:** `floor(M/2) + 1` of `votersAtIssue.size` (unanimity for M=2; supermajority for larger even M). Setting this under a non-`consensus` strategy is silently ignored — a stderr warning fires at boot. Non-positive-integer values throw `InvalidPolicyConfigError`. See [`04-permission-mediation.md`](./04-permission-mediation.md). | | ||
| | `context.fileName` | string | Overrides `getCurrentGeminiMdFilename()`; used by `BridgeOptions.contextFilename`. | |
| | Surface | Where | Use | | ||
| | ------------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `QWEN_SERVE_DEBUG` stderr logging | `bridge.ts:287-295` and call sites | Set the env var to `1` / `true` / `on` / `yes` (case-insensitive) to get verbose `qwen serve debug: ...` lines on stderr. | | ||
| | `/health` | route in `server.ts` | Liveness probe. `?deep=1` returns extended info. | | ||
| | `/capabilities` | route in `server.ts` | Pre-flight feature surface (see [`11-capabilities-versioning.md`](./11-capabilities-versioning.md)). | | ||
| | `/workspace/preflight` | route → `DaemonStatusProvider` | Structured readiness cells (Node version, CLI entry, ripgrep, git, npm, ACP-level cells when child is alive). | | ||
| | `/workspace/env` | route → `DaemonStatusProvider` | Daemon process env snapshot (presence of secret env vars, never values; proxy URLs stripped of creds). | | ||
| | `/workspace/mcp` | route → bridge extMethod | Pool / budget / refusal snapshot. | | ||
| | `/workspace/skills`, `/workspace/providers` | routes | Live ACP-side snapshots (returns idle empty when no session). | | ||
| | Per-session SSE | `GET /session/:id/events` | Real-time event firehose. | | ||
| | `/demo` debug console | `GET /demo` (`packages/cli/src/serve/demo.ts`) | Browser-accessible single-page console (chat + event log + workspace inspector + permission UX). Open `http://127.0.0.1:4170/demo` on loopback — the fastest way to exercise the daemon end-to-end without writing SDK code. See [`02-serve-runtime.md`](./02-serve-runtime.md) for the loopback-vs-auth registration rules. | | ||
| | `PermissionAuditRing` | `permissionAudit.ts:1-60` | In-memory FIFO (512 entries) of permission decisions. | | ||
| | `mediator`'s `decisionReason` audit trail | `permissionMediator.ts:80-100+` | Internal structured "why did this resolve like that" records. | |
| ## What changed in this revision (F4 prereqs merged) | ||
|
|
||
| This doc set was originally pinned to `cb206da36`. After merging `origin/daemon_mode_b_main` (commit `a60c1c52a` plus prior F-series fold-ins), the F4-prereq surface is now in-tree and the docs reflect it directly: |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
| | `InvalidSessionScopeError` | 400 | Unknown `sessionScope` value. | Use `'single'` or `'per-client'`. | | ||
| | `RestoreInProgressError` | 409 | Concurrent `loadSession` / `resumeSession`. | Wait + retry. | | ||
| | `WorkspaceInitConflictError` | 409 | `POST /workspace/init` against an existing file without `force`. | Pass `force: true` or pick another path. | | ||
| | `WorkspaceInitPathEscapeError` | 400 | Init path leaves workspace. | Use a path inside `workspaceCwd`. | |
There was a problem hiding this comment.
[Critical] Six error classes listed in this table do not exist anywhere in packages/acp-bridge/src/bridgeErrors.ts:
WorkspaceInitPathEscapeError(line 50)WorkspaceInitSymlinkError(line 51)WorkspaceInitRaceError(line 52)PermissionForbiddenError(line 56)CancelSentinelCollisionError(line 57)PermissionPolicyNotImplementedError(line 8)
The actual file defines: SessionNotFoundError, RestoreInProgressError, InvalidSessionScopeError, SessionLimitExceededError, WorkspaceMismatchError, InvalidClientIdError, InvalidPermissionOptionError, InvalidSessionMetadataError, WorkspaceInitConflictError, McpServerNotFoundError, McpServerRestartFailedError.
Developers following these docs will instanceof-check or import classes that do not exist. The same phantom classes also appear in the ZH table (lines 206-214) and in 04-permission-mediation.md.
| | `WorkspaceInitPathEscapeError` | 400 | Init path leaves workspace. | Use a path inside `workspaceCwd`. | | |
| <!-- Remove or mark as planned: --> | |
| | `WorkspaceInitPathEscapeError` | 400 | Init path leaves workspace. *(planned, not yet implemented)* | Use a path inside `workspaceCwd`. | |
— qwen-latest-series-invite-beta-v36 via Qwen Code /review
|
|
||
| | Knob | Effect | | ||
| | ---------------------------------------- | ---------------------------------------------------------------- | | ||
| | `sessionScope` | `'per-sender'`, `'per-group'`, `'per-thread'` (channel-defined). | |
There was a problem hiding this comment.
[Critical] The ChannelConfig table lists field names and enum values that do not match the actual ChannelConfig interface in packages/channels/base/src/types.ts:27-57:
| Doc says | Actual interface |
|---|---|
sessionScope: 'per-sender', 'per-group', 'per-thread' |
SessionScope = 'user' | 'thread' | 'single' |
allowlist?: string[] |
allowedUsers: string[] |
denylist?: string[] |
(does not exist) |
chunkSize, chunkIntervalMs |
blockStreamingChunk?: { minChars?, maxChars? }, blockStreamingCoalesce?: { idleMs? } |
daemon: { baseUrl, token?, clientId? } |
(not a ChannelConfig field — handled by AcpBridge separately) |
The actual interface also has senderPolicy: SenderPolicy (line 32) which is not documented.
Additionally, the Envelope description (line 67) says { senderId, groupId?, text, media?, raw } but the actual Envelope interface (types.ts:66-93) has 14 fields: channelName, senderId, senderName, chatId, text, threadId?, messageId?, isGroup, isMentioned, isReplyToBot, referencedText?, imageBase64?, imageMimeType?, attachments?. No groupId, media, or raw fields exist.
— qwen-latest-series-invite-beta-v36 via Qwen Code /review
|
|
||
| Baseline tags (no Map entry) advertise unconditionally — that decision is encoded by **omission**, not by a separate Set the contributor could forget to update. | ||
|
|
||
| ### The 38 tags (v1, by domain) |
There was a problem hiding this comment.
[Critical] The heading says "The 38 tags" but SERVE_CAPABILITY_REGISTRY in packages/cli/src/serve/capabilities.ts:37-186 contains 39 tags.
Additionally, three tags listed in this doc do not exist in the registry:
permission_mediation(referenced on lines 52, 84) — not inSERVE_CAPABILITY_REGISTRYmcp_workspace_pool(referenced on lines 9, 62, 90) — not inSERVE_CAPABILITY_REGISTRYmcp_pool_restart(referenced on lines 9, 63, 90) — not inSERVE_CAPABILITY_REGISTRY
The actual registry has tags like workspace_file_read, workspace_file_bytes, workspace_file_write, session_approval_mode_control, workspace_tool_toggle, workspace_init, workspace_mcp_restart, auth_device_flow that are not mentioned in this doc.
| ### The 38 tags (v1, by domain) | |
| ### The 39 tags (v1, by domain) |
— qwen-latest-series-invite-beta-v36 via Qwen Code /review
|
|
||
| ## Overview | ||
|
|
||
| Every SSE frame the daemon emits on `GET /session/:id/events` carries `{ id, v, type, data, originatorClientId?, _meta? }`. `v: 1` is the current `EVENT_SCHEMA_VERSION`. `type` is a string from a closed, version-pinned set of **29 known types** declared in `DAEMON_KNOWN_EVENT_TYPE_VALUES` (`packages/sdk-typescript/src/daemon/events.ts:13-63`). The envelope's `_meta` field is stamped at the SSE write boundary (`formatSseFrame()` in `server.ts`) — see [Envelope-level metadata](#envelope-level-metadata) below. |
There was a problem hiding this comment.
[Suggestion] Internal contradiction within this document: line 5 says "29 known types declared in DAEMON_KNOWN_EVENT_TYPE_VALUES", but the Architecture table at line 93 says the same constant has "The closed list (length 28)".
The actual source array at packages/sdk-typescript/src/daemon/events.ts:13-52 contains 26 entries. Three event types documented as "known" (state_resync_required, permission_partial_vote, permission_forbidden) are not in DAEMON_KNOWN_EVENT_TYPE_VALUES — they are generated via EventBus.publish() with string type names but fall through to kind: 'unknown' in narrowDaemonEvent().
Suggested reconciliation: if the intent is 29 total on the wire, restructure as "28 known types in DAEMON_KNOWN_EVENT_TYPE_VALUES, plus one subscriber-level synthetic (state_resync_required) — 29 total on the wire." If permission_partial_vote and permission_forbidden are also planned additions, note them explicitly as pending.
— qwen-latest-series-invite-beta-v36 via Qwen Code /review
| | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | ||
| | `first-responder` | First valid vote wins; later voters get `permission_already_resolved`. | Live cross-client collaboration UX (default). | | ||
| | `designated` | Only the prompt's `originatorClientId` may resolve; others see `permission_forbidden{designated_mismatch}`. | Per-tenant SaaS where the UI surface must own its own approvals. | | ||
| | `consensus` | N-of-M quorum across pair-token-authenticated clients; intermediate `permission_partial_vote` events let UIs render progress. | Enterprise change review where two operators must agree. | |
There was a problem hiding this comment.
[Suggestion] The consensus policy overview says "N-of-M quorum across pair-token-authenticated clients", but pair-token authentication does not exist in v1. The Caveats section at line 212 correctly states: "A pair-token mechanism ... is planned for a future PR but not in v1."
This internal inconsistency is misleading — a developer reading only the Overview table would believe consensus provides real multi-party authorization with authenticated, distinct operators, when in reality any client that observed the SSE originatorClientId can spoof identity and inject votes.
| | `consensus` | N-of-M quorum across pair-token-authenticated clients; intermediate `permission_partial_vote` events let UIs render progress. | Enterprise change review where two operators must agree. | | |
| | `consensus` | N-of-M quorum across clients identified by self-declared `X-Qwen-Client-Id` (no proof-of-possession in v1); intermediate `permission_partial_vote` events let UIs render progress. | Enterprise change review where two operators must agree. | |
— qwen-latest-series-invite-beta-v36 via Qwen Code /review
|
|
||
| 长度保持(每个被剥码点替换为 `?` 而不是消失),operator 在那索引处仍能看出有东西曾经在。两层都用:`qwenDeviceFlowProvider` 净化 IdP 的 `oauthError`,registry 的 late-poll 观察者净化插值进 audit hint 的 provider 可控值(`latePollResult.kind` / `lateErr.name`)。 | ||
|
|
||
| **日志注入 / Trojan-Source 防御**:`sanitizeForStderr(value)`(`deviceFlow.ts:47-72`)剥掉 ASCII C0 / DEL / C1 控制字符**外加** Unicode 同形字符 —— 恶意 IdP 可能用它们伪造日志行或隐藏 payload: |
There was a problem hiding this comment.
[Suggestion] This entire "日志注入 / Trojan-Source 防御" paragraph (lines 365-377) is an exact duplicate of the identical section at lines 352-363. Other Chinese sections in this doc correctly use "见英文版" references instead of duplicating content.
| **日志注入 / Trojan-Source 防御**:`sanitizeForStderr(value)`(`deviceFlow.ts:47-72`)剥掉 ASCII C0 / DEL / C1 控制字符**外加** Unicode 同形字符 —— 恶意 IdP 可能用它们伪造日志行或隐藏 payload: | |
| > 见英文版「Log-injection / Trojan-Source defense」表格。 |
— qwen-latest-series-invite-beta-v36 via Qwen Code /review
| | { kind: 'permission_request'; value: PermissionRequestUpdate } | ||
| | { kind: 'tool_group_update'; value: ToolGroupUpdate } | ||
| | { kind: 'permission_resolved'; value: PermissionResolvedUpdate } | ||
| | { kind: 'model_switched'; value: ModelSwitchedUpdate } |
There was a problem hiding this comment.
[Critical] The DaemonTuiUpdate discriminated union code example uses kind as the discriminator and value as the payload field, but the actual type in packages/cli/src/ui/daemon/DaemonTuiAdapter.ts:54-88 uses type as the discriminator and per-variant field names:
// Actual (DaemonTuiAdapter.ts:54-88):
export type DaemonTuiUpdate =
| { type: 'history'; item: HistoryItemWithoutId; daemonEventId?: number }
| { type: 'permission_request'; requestId: string; request: RequestPermissionRequest; daemonEventId?: number }
| { type: 'tool_group_update'; item: HistoryItemToolGroup; daemonEventId?: number }
| { type: 'permission_resolved'; requestId: string; outcome?: unknown; daemonEventId?: number }
| { type: 'model_switched'; modelId: string; daemonEventId?: number }
| { type: 'disconnected'; reason: string; daemonEventId?: number };Additionally:
start()returnsvoid(line 632), notAsyncIterable<DaemonTuiUpdate>. Updates are delivered via theonUpdatecallback passed inDaemonTuiAdapterOptions(line 90).- The public prompt method is
sendPrompt(prompt)(line 671), notprompt(req). - The
--experimental-daemon-tuiCLI flag does not exist in the codebase.
— qwen-latest-series-invite-beta-v36 via Qwen Code /review
8e90746 to
fa42983
Compare
|
Force-push 重写说明 刚 force-push 把整个 PR 从「bilingual EN+ZH」改成「Chinese-only + 新增运维手册」。改动:
如果需要回到 EN 版本,git 历史里 |
| | [`../qwen-serve-protocol.md`](../qwen-serve-protocol.md) | 协议实现者 | HTTP 路由清单、请求/响应结构、错误码 | | ||
| | [`../examples/daemon-client-quickstart.md`](../examples/daemon-client-quickstart.md) | SDK 使用者 | TS 端到端示例 | | ||
| | [`../daemon-client-adapters/`](../daemon-client-adapters/) | 适配器作者(草案) | 每种客户端的设计草案 | | ||
| | [`../../design/f2-mcp-transport-pool.md`](../../design/f2-mcp-transport-pool.md) | F2 维护者 | 工作区共享 MCP transport 池设计 v2.2(32 条 review fold-in changelog) | |
There was a problem hiding this comment.
[Suggestion] This new docs set links to ../../design/f2-mcp-transport-pool.md from the daemon docs, but that target does not exist anywhere in the repository. I found the same broken relative link in four new files: 00-index.md, 01-architecture.md, 05-mcp-transport-pool.md, and 06-mcp-budget-guardrails.md. On the docs site these links will 404, and readers following the stated source-of-truth design reference will hit a dead end. Please either add the referenced design document or replace these links with an existing durable target such as the relevant issue/PR discussion.
— gpt-5.5 via Qwen Code /review
| - 一次性 **canonicalize** 绑定的 workspace(同一份规范形式同时供 `/capabilities`、`POST /session` 兜底和 bridge 使用)。 | ||
| - 拒绝以不安全的姿势启动:非 loopback 绑定无 token;`--require-auth` 无 token;`mcpBudgetMode='enforce'` 无正整数 `mcpClientBudget`;`--workspace` 不存在或不是目录。 | ||
| - 构造 `WorkspaceFileSystem` 工厂、权限审计 publisher、`DaemonStatusProvider`、`acp-bridge`。 | ||
| - 构造 Express 应用、装配中间件链(`bearerAuth` → `hostAllowlist` → `denyBrowserOriginCors` → 每路由 `mutationGate`)、挂载路由(session、workspace CRUD、文件、Device Flow auth、权限投票)。 |
There was a problem hiding this comment.
[Critical] Middleware chain order is reversed. The doc states bearerAuth → hostAllowlist → denyBrowserOriginCors, but the actual registration order in server.ts (lines 515-615) is denyBrowserOriginCors → hostAllowlist → bearerAuth. The code deliberately runs rejection guards (CORS / Host) before bearer auth to prevent unauthenticated clients from triggering a full 10 MB JSON.parse before the 401 fires (see server.ts:406-409 comment). The same reversal appears in 01-architecture.md:149.
| - 构造 Express 应用、装配中间件链(`bearerAuth` → `hostAllowlist` → `denyBrowserOriginCors` → 每路由 `mutationGate`)、挂载路由(session、workspace CRUD、文件、Device Flow auth、权限投票)。 | |
| - 构造 Express 应用、装配中间件链(`denyBrowserOriginCors` → `hostAllowlist` → `bearerAuth` → 每路由 `mutationGate`)、挂载路由(session、workspace CRUD、文件、Device Flow auth、权限投票)。 |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| sequenceDiagram | ||
| autonumber | ||
| participant C as Client (SDK) | ||
| participant MW as Middleware<br/>(bearer→host→CORS→mutationGate) |
There was a problem hiding this comment.
[Critical] Same middleware order reversal as 02-serve-runtime.md:12. The Mermaid participant label should reflect the actual code order: CORS → host → bearer → mutationGate.
| participant MW as Middleware<br/>(bearer→host→CORS→mutationGate) | |
| participant MW as Middleware<br/>(CORS→host→bearer→mutationGate) |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
|
|
||
| ```ts | ||
| class DaemonTuiAdapter { | ||
| constructor(session: DaemonTuiSessionClient, opts?: DaemonTuiAdapterOptions); |
There was a problem hiding this comment.
[Critical] The DaemonTuiAdapter API shape is fundamentally wrong. The doc describes a pull-based API (constructor(session, opts?) + start(): AsyncIterable<DaemonTuiUpdate>), but the actual code at DaemonTuiAdapter.ts:614-631 uses a push-based pattern: constructor(options: DaemonTuiAdapterOptions) where options contains {session, onUpdate}, and start(): void kicks off an internal pump that calls onUpdate. A developer following the doc would write for await (const u of adapter.start()) and get a compile error. The sequence diagram at line 96 also says "start() (returns AsyncIterable)" reinforcing the wrong model.
| constructor(session: DaemonTuiSessionClient, opts?: DaemonTuiAdapterOptions); | |
| constructor(options: DaemonTuiAdapterOptions); // { session, onUpdate } | |
| start(): void; // push-based via onUpdate callback |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| } | ||
| ``` | ||
|
|
||
| `defaultPoolEntryOptions(transport)`(`mcp-pool-entry.ts:58-70`):stdio/ws → `{fixed 5s, 3 次}`;http/sse → `{exponential 1s → 16s, 5 次}`。remote transport 给更长重试预算,因为它们的失败更多是 transient。 |
There was a problem hiding this comment.
[Critical] WebSocket reconnect strategy is wrong. The doc classifies websocket with stdio (fixed 5s, 3 次), but the actual code at mcp-pool-entry.ts:77 classifies websocket as remote alongside HTTP and SSE:
const isRemote = transport === 'http' || transport === 'sse' || transport === 'websocket';Websocket actually gets {exponential 1s → 16s, 5 attempts} — the opposite of what the doc claims.
| `defaultPoolEntryOptions(transport)`(`mcp-pool-entry.ts:58-70`):stdio/ws → `{fixed 5s, 3 次}`;http/sse → `{exponential 1s → 16s, 5 次}`。remote transport 给更长重试预算,因为它们的失败更多是 transient。 | |
| `defaultPoolEntryOptions(transport)`(`mcp-pool-entry.ts:58-70`):stdio → `{fixed 5s, 3 次}`;http/sse/websocket → `{exponential 1s → 16s, 5 次}`。remote transport 给更长重试预算,因为它们的失败更多是 transient。 |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
|
|
||
| ## Daemon-host 错误 kind(`packages/cli/src/serve/status.ts`) | ||
|
|
||
| `DaemonErrorKind` 枚举,给 `GET /workspace/preflight` 单元在 daemon-host check 失败时用: |
There was a problem hiding this comment.
[Critical] Two problems here:
- Wrong type name. The doc uses
DaemonErrorKind, but the actual exported type isServeErrorKind(fromSERVE_ERROR_KINDSinpackages/acp-bridge/src/status.ts:18-31). No symbol namedDaemonErrorKindexists. - Incomplete enumeration. The doc lists 7 kinds but the actual enum has 9: the 7 listed plus
stat_failedandbudget_exhausted. Both are actively used —budget_exhaustedis surfaced on the/workspace/mcpsnapshot and documented in05-mcp-transport-pool.md.
| `DaemonErrorKind` 枚举,给 `GET /workspace/preflight` 单元在 daemon-host check 失败时用: | |
| `ServeErrorKind` 枚举,给 `GET /workspace/preflight` 单元在 daemon-host check 失败时用: |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| qwen serve --max-sessions 0 --event-ring-size 32000 | ||
|
|
||
| # 6. 多客户端协作 + 严格预算 | ||
| QWEN_SERVER_TOKEN=secret \ |
There was a problem hiding this comment.
[Suggestion] Hardcoded weak token QWEN_SERVER_TOKEN=secret contradicts the secure generation pattern used in examples #3 and #4 ($(openssl rand -hex 32)). The --token parameter description later in this same file warns that the token appears in /proc/<pid>/cmdline and recommends env var usage. A user copying example #6 verbatim will run with a trivially guessable bearer token.
| QWEN_SERVER_TOKEN=secret \ | |
| QWEN_SERVER_TOKEN=$(openssl rand -hex 32) \ |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| | `denyBrowserOriginCors` | 拒绝任何带 `Origin` 头的请求 | CLI/SDK 永远不发 `Origin`,这是 CSRF 防护。 | | ||
| | `hostAllowlist(bind, getPort)` | Loopback 下校验 `Host` 头属于 `localhost`、`127.0.0.1`、`[::1]`、`host.docker.internal` 加端口的集合 | 防 DNS rebinding,按端口缓存,比较时大小写不敏感。 | | ||
| | `bearerAuth(token)` | 用 SHA-256 + `timingSafeEqual` 常量时间比较 | 无 token(loopback dev 默认)就 open passthrough,`Bearer` 大小写不敏感。 | | ||
| | `createMutationGate({tokenConfigured, requireAuth})` | 路由级 opt-in 闸门,对 Wave 4 修改类路由即便在 loopback 也强制 token | 返回 `401 { code: 'token_required' }`,区别于一般 `Unauthorized`。`/workspace/memory`、`/workspace/agents/*`、`/file/write`、`/workspace/tools/:name/enable`、`/workspace/mcp/:server/restart`、`/workspace/auth/device-flow`、`/workspace/init` 都调 `mutate({strict: true})`。 | |
There was a problem hiding this comment.
[Suggestion] The strict-route list is incomplete. It omits /file/edit and /session/:id/approval-mode, both of which are included in 12-auth-security.md (the authoritative security doc). An operator relying on this list would not know those routes also enforce the mutation gate.
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| - 用反向索引 `sessionToEntries` 让 `releaseSession(sessionId)` 是 O(refs) 而不是 O(entries)。 | ||
| - 按需重启条目(`restartByName`):单条目返回 `{restarted, durationMs}`,多条目返回 `{entries: RestartResult[]}`(F2 multi-entry 契约)。 | ||
| - daemon shutdown 时 `drainAll` 用可配置超时排空全池;drain 期间拒绝新 acquire。 | ||
| - 与 `WorkspaceMcpBudget`(见 [`06-mcp-budget-guardrays.md`](./06-mcp-budget-guardrails.md))联动在 `acquire` 上做 per-name 预留上限;条目 close 且同名无其他 entry 时释放 slot。 |
There was a problem hiding this comment.
[Suggestion] Typo in link text: 06-mcp-budget-guardrays.md is missing the 'l' in "guardrails". The href target is correct, but the visible text has the typo.
| - 与 `WorkspaceMcpBudget`(见 [`06-mcp-budget-guardrays.md`](./06-mcp-budget-guardrails.md))联动在 `acquire` 上做 per-name 预留上限;条目 close 且同名无其他 entry 时释放 slot。 | |
| - 与 `WorkspaceMcpBudget`(见 [`06-mcp-budget-guardrails.md`](./06-mcp-budget-guardrails.md))联动在 `acquire` 上做 per-name 预留上限;条目 close 且同名无其他 entry 时释放 slot。 |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| | `paths.ts` | `canonicalizeWorkspace`、`resolveWithinWorkspace`、`hasSuspiciousPathPattern`、branded `ResolvedPath`、`Intent`(`read \| write \| list \| stat \| glob`) | | ||
| | `policy.ts` | `MAX_READ_BYTES`、`MAX_WRITE_BYTES`、`BINARY_PROBE_BYTES`、`assertTrustedForIntent`、`detectBinary`、`enforceReadBytesSize`、`enforceReadSize`、`enforceWriteSize`、`shouldIgnore` | | ||
| | `audit.ts` | `FS_ACCESS_EVENT_TYPE`、`FS_DENIED_EVENT_TYPE`、`createAuditPublisher`、audit payload 类型 | | ||
| | `errors.ts` | `FsError` 类、`isFsError`、`FsErrorKind`(13 种)、`FsErrorStatus`(`400 / 403 / 404 / 409 / 413 / 422 / 500 / 503`) | |
There was a problem hiding this comment.
[Suggestion] FsErrorKind count is wrong: says "13 种" but the actual union type in errors.ts:17-54 has 14 members (including parse_error). The doc's own detailed table below correctly lists all 14 rows, contradicting the summary number. Same issue in 18-error-taxonomy.md:14.
| | `errors.ts` | `FsError` 类、`isFsError`、`FsErrorKind`(13 种)、`FsErrorStatus`(`400 / 403 / 404 / 409 / 413 / 422 / 500 / 503`) | | |
| | `errors.ts` | `FsError` 类、`isFsError`、`FsErrorKind`(14 种)、`FsErrorStatus`(`400 / 403 / 404 / 409 / 413 / 422 / 500 / 503`) | |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| 'channel-plugins': 'Channel Plugin Guide', | ||
| tools: 'Tools', | ||
| 'qwen-serve-protocol': 'qwen serve HTTP protocol', | ||
| daemon: 'Daemon 模式 · 开发者深度指南', |
There was a problem hiding this comment.
[Suggestion] This nav label is in Chinese ('Daemon 模式 · 开发者深度指南') while every other entry in this _meta.ts uses English ('Architecture', 'Tools', 'Channel Plugin Guide', etc.). Consider aligning with the parent meta's language for sidebar consistency, e.g., 'Daemon Mode Developer Guide'.
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
|
|
||
| 实现:`packages/cli/src/serve/auth/deviceFlow.ts` + `qwenDeviceFlowProvider.ts`。 | ||
|
|
||
| **日志注入 / Trojan-Source 防御**:`sanitizeForStderr(value)`(`deviceFlow.ts:47-72`)剥掉 ASCII C0 / DEL / C1 控制字符**外加** Unicode 同形字符 —— 恶意 IdP 可能用它们伪造日志行或隐藏 payload: |
There was a problem hiding this comment.
[Critical] sanitizeForStderr 描述的 Unicode 防御实际未实现。
文档声称 sanitizeForStderr() 剥离 U+200B-U+200F、U+2028-U+2029、U+202A-U+202E、U+2066-U+2069 (CVE-2021-42574 Trojan Source) 和 U+FEFF。但实际代码 deviceFlow.ts:69-71 只剥离 ASCII C0/C1 控制字符 (\x00-\x1f\x7f-\x9f):
function sanitizeForStderr(value: string): string {
return value.replace(/[\x00-\x1f\x7f-\x9f]/g, '?');
}运维人员基于文档信任 Unicode 日志注入已被防御,但实际上并没有。建议:要么实现文档描述的 Unicode 剥离,要么将文档描述改为只覆盖 ASCII C0/C1。
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| | --------------------------- | --------- | ------------------------------------- | | ||
| | `--experimental-daemon-tui` | CLI 参数 | 选这条适配器路径而不是进程内默认 | | ||
| | `QWEN_DAEMON_URL` | Env / CLI | daemon base URL | | ||
| | `QWEN_DAEMON_TOKEN` | Env / CLI | Bearer token(透传给 `DaemonClient`) | |
There was a problem hiding this comment.
[Critical] QWEN_DAEMON_TOKEN 环境变量在代码中不存在。
14-cli-tui-adapter.md:157 和 17-configuration.md:46 引用 QWEN_DAEMON_TOKEN 作为 daemon Bearer token 环境变量。但在整个 packages/ 源代码树中搜索,该变量零匹配。代码只识别 QWEN_SERVER_TOKEN。
运维人员按文档配置 QWEN_DAEMON_TOKEN=my-secret 启动 daemon,daemon 完全不读取该变量,结果在无 token 状态下运行——运维人员却以为已启用认证。
| | `QWEN_DAEMON_TOKEN` | Env / CLI | Bearer token(透传给 `DaemonClient`) | | |
| | `QWEN_SERVER_TOKEN` | Env / CLI | Bearer token(透传给 `DaemonClient`) | |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| | Env | 作用 | | ||
| | ----------------------- | ---------------------------------------------------------- | | ||
| | `QWEN_DAEMON_URL` | daemon base URL(CLI TUI 适配器、channels、IDE companion) | | ||
| | `QWEN_DAEMON_TOKEN` | Bearer token | |
There was a problem hiding this comment.
[Critical] QWEN_DAEMON_TOKEN — 同上(见 14-cli-tui-adapter.md:157)。
| | `QWEN_DAEMON_TOKEN` | Bearer token | | |
| | `QWEN_SERVER_TOKEN` | Bearer token | |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
|
|
||
| 要点: | ||
|
|
||
| - 环形缓冲有上限(`eventRingSize`,默认 1024)。 |
There was a problem hiding this comment.
[Suggestion] eventRingSize 默认值错误:文档写 1024,实际 eventBus.ts:76 为 DEFAULT_RING_SIZE = 8000。本文档集其他文件(10-event-bus.md、17-configuration.md、20-quickstart-operations.md)均正确引用 8000——此处是孤立的旧草稿值。
| - 环形缓冲有上限(`eventRingSize`,默认 1024)。 | |
| - 环形缓冲有上限(`eventRingSize`,默认 8000)。从 ring 头 replay 所有 SSE 帧。 |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
|
|
||
| subgraph daemon["qwen serve process (one workspace)"] | ||
| EXP["Express app<br/>(packages/cli/src/serve/server.ts)"] | ||
| BR["AcpBridge<br/>(packages/acp-bridge/src/bridge.ts)"] |
There was a problem hiding this comment.
[Suggestion] 文档集中 11 处引用 packages/acp-bridge/src/bridge.ts,但该文件不存在。Bridge 实现实际在 packages/cli/src/serve/httpAcpBridge.ts。
受影响位置:
01-architecture.md:23,76,347(Mermaid 图 + 关键文件表)03-acp-bridge.md:23,224(公开入口 + 参考)08-session-lifecycle.md:24,28,177(SessionEntry 定义 + ClientId 校验)10-event-bus.md:29,174,196(常量上限 + publish 站点)17-configuration.md:90-106(8 个常量文件列)
| BR["AcpBridge<br/>(packages/acp-bridge/src/bridge.ts)"] | |
| BR["AcpBridge<br/>(packages/cli/src/serve/httpAcpBridge.ts)"] |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| 6. **MCP 预算校验**:必须正整数;`enforce` 必须配 budget。 | ||
| 7. **MCP pool 开关推断**:父进程 env 里 `QWEN_SERVE_NO_MCP_POOL=1` 时,`mcpPoolActive` 默认 `false`,capabilities 也会诚实地不广播 `mcp_workspace_pool` + `mcp_pool_restart`。 | ||
| 8. **per-handle `childEnvOverrides`**:把 `QWEN_SERVE_MCP_CLIENT_BUDGET` 和 `QWEN_SERVE_MCP_BUDGET_MODE` 通过 `BridgeOptions.childEnvOverrides` 传给 ACP 子进程,**不**改 `process.env`(同进程跑两个 daemon 会出 race)。 | ||
| 9. **boot 一次 `settings.json`**:取 `context.fileName`、`policy.permissionStrategy`、`policy.consensusQuorum`;损坏文件 try/catch 走默认值。之后 **`validatePolicyConfig()`**(`runQwenServe.ts:89+`)解析 `policy.*`,未知 strategy(按 `SERVE_CAPABILITY_REGISTRY.permission_mediation.modes` 单一事实源校验)或非正整数 `consensusQuorum` 时抛 `InvalidPolicyConfigError`。`consensusQuorum` 设了但策略非 `consensus` 时打 stderr 警告(默认会被静默忽略,浮出来防 operator 误以为它生效)。settings 读 I/O 失败回退默认;`InvalidPolicyConfigError` 重抛让 boot 显式失败。 |
There was a problem hiding this comment.
[Suggestion] validatePolicyConfig() 函数在代码中不存在。
02-serve-runtime.md:64、04-permission-mediation.md:192,199、18-error-taxonomy.md:65、17-configuration.md:55-56 和 20-quickstart-operations.md:95,101,119,201 共 5 个文件描述了此函数。
若属于 daemon_mode_b_main 前向引用,请在所有引用处标注,例如:
| 9. **boot 一次 `settings.json`**:取 `context.fileName`、`policy.permissionStrategy`、`policy.consensusQuorum`;损坏文件 try/catch 走默认值。之后 **`validatePolicyConfig()`**(`runQwenServe.ts:89+`)解析 `policy.*`,未知 strategy(按 `SERVE_CAPABILITY_REGISTRY.permission_mediation.modes` 单一事实源校验)或非正整数 `consensusQuorum` 时抛 `InvalidPolicyConfigError`。`consensusQuorum` 设了但策略非 `consensus` 时打 stderr 警告(默认会被静默忽略,浮出来防 operator 误以为它生效)。settings 读 I/O 失败回退默认;`InvalidPolicyConfigError` 重抛让 boot 显式失败。 | |
| **`validatePolicyConfig()`**(待合入 `daemon_mode_b_main`,`runQwenServe.ts:89+`)解析... |
— DeepSeek/deepseek-v4-pro via Qwen Code /review
| | 键 | 默认 | 作用 | | ||
| | --------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------ | | ||
| | `boundWorkspace` | (必填) | bridge 强制的规范 workspace 路径 | | ||
| | `sessionScope` | `'single'` | `'single'` 所有客户端共享一个 session;`'per-client'` 每客户端一个 | |
There was a problem hiding this comment.
[Suggestion] sessionScope 枚举值 'per-client' 在代码中不存在。实际类型为 'single' | 'thread'(bridgeOptions.ts:99, bridgeTypes.ts:45)。同样出现在 08-session-lifecycle.md:53、17-configuration.md:77、18-error-taxonomy.md:46、01-architecture.md:4、00-index.md:80。
| | `sessionScope` | `'single'` | `'single'` 所有客户端共享一个 session;`'per-client'` 每客户端一个 | | |
| | `sessionScope` | `'single'` | `'single'` 所有客户端共享一个 session;`'thread'` 每客户端一个 | |
— qwen3.7-max via Qwen Code /review
| | `policy.permissionStrategy` | `'first-responder' \| 'designated' \| 'consensus' \| 'local-only'` | 设 `BridgeOptions.permissionPolicy`;激活值出现在 `/capabilities` 的 `policy.permission`。**boot 校验**通过 `validatePolicyConfig()`,对照 `SERVE_CAPABILITY_REGISTRY.permission_mediation.modes`;未知字面量抛 `InvalidPolicyConfigError`,boot 显式失败 | | ||
| | `policy.consensusQuorum` | 正整数 | `consensus` 策略的 N。**默认**:`votersAtIssue.size` 的 `floor(M/2) + 1`(M=2 一致同意;更大偶数 M 超过半数)。非 `consensus` 策略下设它会被静默忽略,boot 会打 stderr 警告。非正整数抛 `InvalidPolicyConfigError`。详见 [`04-permission-mediation.md`](./04-permission-mediation.md) | | ||
| | `context.fileName` | string | 覆盖 `getCurrentGeminiMdFilename()`;走 `BridgeOptions.contextFilename` | | ||
| | `tools.disabled` | string[] | 下次 ACP child spawn 时被禁的 tool;通过 `normalizeDisabledToolList()`(`packages/cli/src/config/normalizeDisabledTools.ts`)归一化:非数组 → `[]`;非字符串项跳过;trim 空白;trim 后空串丢弃;去重(保留首次出现顺序)。boot 路径与 `restartMcpServer` settings 刷新都过这函数,`ToolRegistry.has(name)` 精确匹配才一致。**不**做大小写折叠 —— Stage 1 工具名在 registry 全程大小写敏感。`POST /workspace/tools/:name/enable` 与 `tool_toggled` 事件改这里 | |
There was a problem hiding this comment.
[Suggestion] normalizeDisabledToolList() 函数和文件 packages/cli/src/config/normalizeDisabledTools.ts 在代码中不存在。实际归一化逻辑内联在 packages/cli/src/config/config.ts:1429-1436(trim, filter non-strings, deduplicate)。
— qwen3.7-max via Qwen Code /review
| P["publish({type, data, originatorClientId?})"] --> C{"bus closed?"} | ||
| C -->|yes| RU["return undefined"] | ||
| C -->|no| AID["assign id = nextId++, v = 1"] | ||
| AID --> PR["push to ring (shift if > ringSize)"] |
There was a problem hiding this comment.
[Suggestion] Mermaid flowchart 中使用 > / < HTML 实体。Mermaid 不解析 HTML 实体,会渲染为字面文本 > / <。同文件其他 Mermaid 图均直接使用 > / <。
| AID --> PR["push to ring (shift if > ringSize)"] | |
| AID --> PR["push to ring (shift if > ringSize)"] |
(line 80 和 82 同理:>= → >=,<= → <=)
— qwen3.7-max via Qwen Code /review
|
|
||
| ### `_meta.serverTimestamp` —— daemon 时钟 | ||
|
|
||
| 在 `formatSseFrame()`(`packages/cli/src/serve/server.ts:2602+`)的 SSE 写边界盖,**不**在 `EventBus.publish`。这样内存里的 `BridgeEvent` 类型不变,内部 daemon 消费方看不到 `_meta`,只有 wire 上的 SSE 帧带。 |
There was a problem hiding this comment.
[Suggestion] formatSseFrame() 行号引用为 server.ts:2602+,但实际定义在 server.ts:2320(文件共 2590 行,2602 不可能存在)。
| 在 `formatSseFrame()`(`packages/cli/src/serve/server.ts:2602+`)的 SSE 写边界盖,**不**在 `EventBus.publish`。这样内存里的 `BridgeEvent` 类型不变,内部 daemon 消费方看不到 `_meta`,只有 wire 上的 SSE 帧带。 | |
| 在 `formatSseFrame()`(`packages/cli/src/serve/server.ts:2320`)的 SSE 写边界盖,**不**在 `EventBus.publish`。这样内存里的 `BridgeEvent` 类型不变,内部 daemon 消费方看不到 `_meta`,只有 wire 上的 SSE 帧带。 |
— qwen3.7-max via Qwen Code /review
|
|
||
| ## 注意 & 已知局限 | ||
|
|
||
| - 三种合成帧故意无 `id`,SDK 代码不能假设每个事件都有 id。 |
There was a problem hiding this comment.
[Suggestion] "三种合成帧故意无 id" — 但文档自身的事件表列出了 4 个 无 id 的事件类型:client_evicted、slow_client_warning、stream_error、和 state_resync_required。每个在表中都标注了"无 id"。
| - 三种合成帧故意无 `id`,SDK 代码不能假设每个事件都有 id。 | |
| - 四种合成帧故意无 `id`,SDK 代码不能假设每个事件都有 id。 |
— qwen3.7-max via Qwen Code /review
| | 键 | 默认 | 作用 | | ||
| | --------------------------------------------- | ------------------------------------------------- | ------------------------------------------------------------------ | | ||
| | `boundWorkspace` | (必填) | bridge 强制的规范 workspace 路径 | | ||
| | `sessionScope` | `'single'` | `'single'` 所有客户端共享一个 session;`'per-client'` 每客户端一个 | |
There was a problem hiding this comment.
[Suggestion] sessionScope 枚举值 'per-client' 在代码中不存在。实际类型为 'single' | 'thread'(bridgeOptions.ts:99, bridgeTypes.ts:45)。同样出现在 08-session-lifecycle.md:53、17-configuration.md:77、18-error-taxonomy.md:46、01-architecture.md:4、00-index.md:80。
| | `sessionScope` | `'single'` | `'single'` 所有客户端共享一个 session;`'per-client'` 每客户端一个 | | |
| | `sessionScope` | `'single'` | `'single'` 所有客户端共享一个 session;`'thread'` 每客户端一个 | |
— qwen3.7-max via Qwen Code /review
| | `policy.permissionStrategy` | `'first-responder' \| 'designated' \| 'consensus' \| 'local-only'` | 设 `BridgeOptions.permissionPolicy`;激活值出现在 `/capabilities` 的 `policy.permission`。**boot 校验**通过 `validatePolicyConfig()`,对照 `SERVE_CAPABILITY_REGISTRY.permission_mediation.modes`;未知字面量抛 `InvalidPolicyConfigError`,boot 显式失败 | | ||
| | `policy.consensusQuorum` | 正整数 | `consensus` 策略的 N。**默认**:`votersAtIssue.size` 的 `floor(M/2) + 1`(M=2 一致同意;更大偶数 M 超过半数)。非 `consensus` 策略下设它会被静默忽略,boot 会打 stderr 警告。非正整数抛 `InvalidPolicyConfigError`。详见 [`04-permission-mediation.md`](./04-permission-mediation.md) | | ||
| | `context.fileName` | string | 覆盖 `getCurrentGeminiMdFilename()`;走 `BridgeOptions.contextFilename` | | ||
| | `tools.disabled` | string[] | 下次 ACP child spawn 时被禁的 tool;通过 `normalizeDisabledToolList()`(`packages/cli/src/config/normalizeDisabledTools.ts`)归一化:非数组 → `[]`;非字符串项跳过;trim 空白;trim 后空串丢弃;去重(保留首次出现顺序)。boot 路径与 `restartMcpServer` settings 刷新都过这函数,`ToolRegistry.has(name)` 精确匹配才一致。**不**做大小写折叠 —— Stage 1 工具名在 registry 全程大小写敏感。`POST /workspace/tools/:name/enable` 与 `tool_toggled` 事件改这里 | |
There was a problem hiding this comment.
[Suggestion] normalizeDisabledToolList() 函数和文件 packages/cli/src/config/normalizeDisabledTools.ts 在代码中不存在。实际归一化逻辑内联在 packages/cli/src/config/config.ts:1429-1436(trim, filter non-strings, deduplicate)。
— qwen3.7-max via Qwen Code /review
| P["publish({type, data, originatorClientId?})"] --> C{"bus closed?"} | ||
| C -->|yes| RU["return undefined"] | ||
| C -->|no| AID["assign id = nextId++, v = 1"] | ||
| AID --> PR["push to ring (shift if > ringSize)"] |
There was a problem hiding this comment.
[Suggestion] Mermaid flowchart 中使用 > / < HTML 实体。Mermaid 不解析 HTML 实体,会渲染为字面文本。同文件其他 Mermaid 图均直接使用 > / <。
| AID --> PR["push to ring (shift if > ringSize)"] | |
| AID --> PR["push to ring (shift if > ringSize)"] |
(line 80 和 82 同理:>= → >=,<= → <=)
— qwen3.7-max via Qwen Code /review
|
|
||
| ### `_meta.serverTimestamp` —— daemon 时钟 | ||
|
|
||
| 在 `formatSseFrame()`(`packages/cli/src/serve/server.ts:2602+`)的 SSE 写边界盖,**不**在 `EventBus.publish`。这样内存里的 `BridgeEvent` 类型不变,内部 daemon 消费方看不到 `_meta`,只有 wire 上的 SSE 帧带。 |
There was a problem hiding this comment.
[Suggestion] formatSseFrame() 行号引用为 server.ts:2602+,但实际定义在 server.ts:2320(文件共 2590 行,2602 不可能存在)。
| 在 `formatSseFrame()`(`packages/cli/src/serve/server.ts:2602+`)的 SSE 写边界盖,**不**在 `EventBus.publish`。这样内存里的 `BridgeEvent` 类型不变,内部 daemon 消费方看不到 `_meta`,只有 wire 上的 SSE 帧带。 | |
| 在 `formatSseFrame()`(`packages/cli/src/serve/server.ts:2320`)的 SSE 写边界盖,**不**在 `EventBus.publish`。这样内存里的 `BridgeEvent` 类型不变,内部 daemon 消费方看不到 `_meta`,只有 wire 上的 SSE 帧带。 |
— qwen3.7-max via Qwen Code /review
|
|
||
| ## 注意 & 已知局限 | ||
|
|
||
| - 三种合成帧故意无 `id`,SDK 代码不能假设每个事件都有 id。 |
There was a problem hiding this comment.
[Suggestion] "三种合成帧故意无 id" — 但文档自身的事件表列出了 4 个 无 id 的事件类型:client_evicted、slow_client_warning、stream_error、和 state_resync_required。每个在表中都标注了"无 id"。
| - 三种合成帧故意无 `id`,SDK 代码不能假设每个事件都有 id。 | |
| - 四种合成帧故意无 `id`,SDK 代码不能假设每个事件都有 id。 |
— qwen3.7-max via Qwen Code /review
| # SSE 事件总线与反压 | ||
| ## 概览 | ||
|
|
||
| `EventBus`(`packages/acp-bridge/src/eventBus.ts`)是每 session 一份的内存 pub/sub,喂给 daemon 的 `GET /session/:id/events` SSE 路由。它给每个事件分配单调 id、用有界环形缓冲缓存最近事件给 `Last-Event-ID` 重放、把 publish 扇出到所有订阅者、对订阅者实施反压(队列 75% 满时发警告、达到上限时驱逐),还会合成两种终态帧(`client_evicted`、`slow_client_warning`),SDK 把它们当一等事件,但 bus 故意**不**给它们分配 `id`,防止它们占掉本 session 的序列号让其他订阅者看到断档。 |
There was a problem hiding this comment.
[Suggestion] slow_client_warning 被分类为"终态帧",但实际行为是非终态的。
本文写道"还会合成两种终态帧(client_evicted、slow_client_warning)",但 eventBus.ts:288-306 实现了 warned flag + warnResetThreshold 滞回重臂机制(队列降到 37.5% 后 warned = false,下次溢出重新发 warning)。测试 'rearms slow_client_warning after queue drains' 也证实了可重复发送。
client_evicted 是真正的终态(关闭订阅者),slow_client_warning 不是。SDK 实现者如果把 slow_client_warning 当终态处理,会过早停止事件处理。
| `EventBus`(`packages/acp-bridge/src/eventBus.ts`)是每 session 一份的内存 pub/sub,喂给 daemon 的 `GET /session/:id/events` SSE 路由。它给每个事件分配单调 id、用有界环形缓冲缓存最近事件给 `Last-Event-ID` 重放、把 publish 扇出到所有订阅者、对订阅者实施反压(队列 75% 满时发警告、达到上限时驱逐),还会合成两种终态帧(`client_evicted`、`slow_client_warning`),SDK 把它们当一等事件,但 bus 故意**不**给它们分配 `id`,防止它们占掉本 session 的序列号让其他订阅者看到断档。 | |
| 还会合成终态帧 `client_evicted` 与警告帧 `slow_client_warning`(37.5% 滞回重臂),SDK 把它们当一等事件 |
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
| - **流式端点(`subscribeEvents`)绕过超时** —— 长 SSE 不能被它杀。 | ||
|
|
||
| ### `DaemonSessionClient`(`DaemonSessionClient.ts:61-385`) | ||
|
|
There was a problem hiding this comment.
[Suggestion] MCP_RESTART_DEFAULT_TIMEOUT_MS = 330_000 常量在 SDK 代码中不存在。
整个 packages/sdk-typescript/ grep 零命中。实际的 restartMcpServer()(DaemonClient.ts:941-965)签名为 (serverName, opts?),没有 per-call timeout 参数,使用的是 client 级 fetchTimeoutMs 默认值 30s。
文档声称的 330s 超时 vs 实际的 30s 是 10 倍差距。开发者如果依赖这个超时覆盖来安全地重启 MCP server,会在 30s 时收到假阳性 TimeoutError(daemon 允许重启最多 300s)。
建议:删除常量名和值,改为建议调用方在构造 DaemonClient 时设置 fetchTimeoutMs: 330_000 或在文档中标注该特性尚未实现。
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
|
|
||
| | Type | 方向 | 触发 | Payload 关键字段 | | ||
| | ----------------------------- | ---- | -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | `permission_request` | S→C | agent 调 `requestPermission` | `requestId, sessionId, toolCall, options[]`;envelope 盖 `originatorClientId`(= prompt originator,F3 N3) | |
There was a problem hiding this comment.
[Suggestion] awaitingResync reducer flag 和 RESYNC_PASSTHROUGH_TYPES 常量在代码中不存在。
整个 packages/ grep 零命中。文档描述的完整 SDK reducer resync 流程(events.ts:870-905, 1120-1140 处的实现位置、events.ts:896-902 的 passthrough 集合)均无对应代码。state_resync_required 事件本身也不在 DAEMON_KNOWN_EVENT_TYPE_VALUES 中。
如属 forward reference,建议标注 ⚠ 尚未实现 / planned,与 00-index.md 的 F4 prereq 表对齐。
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
| ### Models | ||
|
|
||
| | Type | 方向 | Payload | | ||
| | --------------------- | ---- | -------------------------------------------- | |
There was a problem hiding this comment.
[Suggestion] permission_partial_vote 和 permission_forbidden 不在 DAEMON_KNOWN_EVENT_TYPE_VALUES 中。
这两个事件类型仅出现在 packages/acp-bridge/src/permission.ts 的 JSDoc 注释中,从未注册到 SDK 的已知事件数组。narrowDaemonEvent() 对它们会返回 kind: 'unknown',typed dispatch handler 永远不会触发。
如果属于 mediator 内部概念(不通过 SSE 暴露),应从词汇表中移除并注明;如果确实会通过 SSE 暴露,则应加入 DAEMON_KNOWN_EVENT_TYPE_VALUES 并添加对应的 DaemonXxxData 接口。
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
| | `serverId` | `string`(仅 `provenance === 'mcp'` 时设) | 从 `mcp__<serverId>__<tool>` 命名启发提取 | | ||
|
|
||
| 加上原本就有的 `_meta.toolName`(显示名)。 | ||
|
|
There was a problem hiding this comment.
[Suggestion] ToolCallEmitter.resolveToolProvenance() 在代码中不存在。
ToolCallEmitter.ts:201-237 的实际方法名为 resolveToolMetadata(),处理 tool displayName / locations / kind 解析。整个 packages/ 中 resolveToolProvenance 和 provenance 均零命中。
文档声称的 provenance: 'builtin' | 'mcp' | 'subagent' 和 serverId 字段从未被 emit。adapter 开发者如果依赖这些字段会永远拿不到数据。
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
| err.name === 'FsError' && | ||
| typeof (err as { kind?: unknown }).kind === 'string' | ||
| ); | ||
| } |
There was a problem hiding this comment.
[Suggestion] isFsErrorShape() 和 preserveFsErrorOverAcp() 在代码中不存在。
整个 packages/ grep 零命中。文档展示了完整的 TypeScript 实现代码(包括类型守卫和 throw 逻辑),但这两个函数是虚构的。当前 FsError 在 ACP wire 上传输时 kind 信息会丢失,文档描述的 thin guardrail 尚未实现。
建议:如果是 planned feature,标注 ⚠ 尚未实现;如果不打算实现,删除此节。
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
| port: 0, // ephemeral | ||
| hostname: '127.0.0.1', | ||
| mode: 'http-bridge', | ||
| maxSessions: 20, |
There was a problem hiding this comment.
[Suggestion] @qwen-code/qwen-code/serve 子路径导出不存在。
packages/cli/package.json 的 exports 字段仅声明 . 和 ./export,没有 ./serve。开发者复制第 265 行和第 282 行的 import 会失败:ERR_PACKAGE_PATH_NOT_EXPORTED。
第 12 节"嵌入式调用(绕过 CLI)"的两段代码示例(runQwenServe 和 createServeApp)作为文档完全不可运行。
建议:要么在 package.json 添加 ./serve 导出,要么改为内部相对路径并注明 programmatic embedding 尚非受支持的公开 API。
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
| ``` | ||
|
|
||
| `Map` 形状把「predicate 判断」和「集合成员」收成一条记录。加一个新条件 tag 要**两处协调修改**: | ||
|
|
There was a problem hiding this comment.
[Suggestion] mcp_workspace_pool / mcp_pool_restart capability tags 和 mcpPoolActive toggle 字段均不存在。
验证结果:
SERVE_CAPABILITY_REGISTRY(capabilities.ts:37-183)无此二 tagAdvertiseFeatureToggles(capabilities.ts:190-193)仅有requireAuth?: boolean,无mcpPoolActiveCONDITIONAL_SERVE_FEATURES实际只有一条require_auth条目
文档的 CONDITIONAL_SERVE_FEATURES 代码块(含 3 条 Map 条目)和 AdvertiseFeatureToggles 接口形状均与代码不符。SDK client 调用 caps.features.includes('mcp_workspace_pool') 永远返回 false。
— qwen-latest-series-invite-beta-v38 via Qwen Code /review
056a38c to
4147aa8
Compare
|
根据 daemon_mode_b_main 最新代码同步(force-push at 6 个新 commit 进了
最重要的:#4328 删除了 本次文档变化:
23 files / +4,428 lines(前次 4,366)。无 stale |
4147aa8 to
b4810d1
Compare
|
Round 2 review fold-in(force-push at 逐 commit review 时发现两处描述不准 / 一处漏: 1.
|
b4810d1 to
88c66de
Compare
|
Round 3 重构:从「review fold-in 切分」改成「功能视角」(force-push at 上一轮把 PR #4411 拆成 R9 / W11 / W12 / R10 / R2 / R3 T3 / WR4 / WR5 / WR6 一一列,太按 PR review 内部结构组织 —— 读者要的是「daemon mode b 是个什么完整功能」,不是「这一轮 review 改了哪 6 件事」。本轮把 daemon_mode_b 作为完整产品重新组织。 Doc 05 (
|
| | `ui/types.ts` | `DaemonUiEventType`、`DaemonUiEvent*`(按 type 一类一 interface)、`DaemonTranscriptBlock`、`DaemonTranscriptState`、`DaemonUiToolProvenance`、`DAEMON_PLAN_TOOL_CALL_ID` | 全部类型 | | ||
| | `ui/normalizer.ts` | `normalizeDaemonEvent(evt) → DaemonUiEvent`、`getSessionUpdatePayload(evt)` | wire → UI 词汇映射 | | ||
| | `ui/transcript.ts` | `createDaemonTranscriptState()`、`appendLocalUserTranscriptMessage()`、`reduceDaemonTranscriptEvents()`、`rebuildDaemonTranscriptBlockIndex()`、selectors(见下) | 状态机 + 选择器 | | ||
| | `ui/store.ts` | `createDaemonTranscriptStore(initial?)` | 可订阅 store 封装 reducer | |
There was a problem hiding this comment.
[Critical] normalizeDaemonEvent 的签名有两个错误:
- 返回类型是
DaemonUiEvent[](数组),不是DaemonUiEvent。 一条 wire event 可以扇出到 0..N 条 UI event(例如包含 tool call 的session_update会产生多条)。 - 缺少第二个参数
opts?: NormalizeDaemonEventOptions(承载clientId、suppressOwnUserEcho、includeRawEvent)。
实际签名(normalizer.ts:52-55):
export function normalizeDaemonEvent(
event: DaemonEvent,
opts: NormalizeDaemonEventOptions = {},
): DaemonUiEvent[]| | `ui/store.ts` | `createDaemonTranscriptStore(initial?)` | 可订阅 store 封装 reducer | | |
| | `ui/normalizer.ts` | `normalizeDaemonEvent(evt, opts?) → DaemonUiEvent[]`、`getSessionUpdatePayload(evt)` | wire → UI 词汇映射(1:N 扇出) | |
— qwen3.7-max via Qwen Code /review
| selectCurrentTool(state); | ||
| selectApprovalMode(state); | ||
| selectToolProgress(state, toolCallId); | ||
| selectSubagentChildBlocks(state, parentBlockId); |
There was a problem hiding this comment.
[Suggestion] selectSubagentChildBlocks(state, parentBlockId) 的参数名不对。实际参数名是 parentToolCallId(transcript.ts:1168),指父级委派 tool call 的 toolCallId,不是 block 自身的 id。传入 block id 会静默返回空数组,无报错。
| selectSubagentChildBlocks(state, parentBlockId); | |
| selectSubagentChildBlocks(state, parentToolCallId); |
— qwen3.7-max via Qwen Code /review
| selectToolProgress(state, toolCallId); | ||
| selectSubagentChildBlocks(state, parentBlockId); | ||
| isSubagentChildBlock(block); | ||
| formatBlockTimestamp(block.clientReceivedAt); |
There was a problem hiding this comment.
[Critical] formatBlockTimestamp(block.clientReceivedAt) 签名错误。实际签名(transcript.ts:1234)是:
formatBlockTimestamp(block: DaemonTranscriptBlock, opts?: TimestampFormatOptions): string函数接收整个 block 对象,内部优先读 block.serverTimestamp,回退到 block.clientReceivedAt。直接传 timestamp 数字会编译失败,且跳过了 server-first 的时间戳优先逻辑。
| formatBlockTimestamp(block.clientReceivedAt); | |
| formatBlockTimestamp(block); // 默认格式 | |
| formatBlockTimestamp(block, { locale: 'zh-CN', timeStyle: 'short' }); // 自定义格式 |
— qwen3.7-max via Qwen Code /review
| selectSubagentChildBlocks(state, parentBlockId); | ||
| isSubagentChildBlock(block); | ||
| formatBlockTimestamp(block.clientReceivedAt); | ||
| formatMissedRange(state); // state_resync_required 后的 "you missed X" 文案 |
There was a problem hiding this comment.
[Critical] formatMissedRange(state) 签名错误。实际签名(transcript.ts:303)是:
formatMissedRange(lastDeliveredId: number, earliestAvailableId: number): string函数是纯字符串格式化器,不读 state。需要传入两个 SSE cursor id。
| formatMissedRange(state); // state_resync_required 后的 "you missed X" 文案 | |
| formatMissedRange(state.lastDeliveredId, state.earliestAvailableId); // → "missed daemon events 42–58" |
— qwen3.7-max via Qwen Code /review
| `createDaemonTranscriptStore()` 提供订阅 / 派发: | ||
|
|
||
| ```ts | ||
| const store = createDaemonTranscriptStore(); |
There was a problem hiding this comment.
[Critical] store.getState() 方法不存在。DaemonTranscriptStore 接口(store.ts:45)暴露的是 getSnapshot():
export interface DaemonTranscriptStore {
getSnapshot(): DaemonTranscriptState;
subscribe(listener: () => void): () => void;
dispatch(event: DaemonUiEvent | DaemonUiEvent[]): void;
}| const store = createDaemonTranscriptStore(); | |
| const store = createDaemonTranscriptStore(); | |
| const unsubscribe = store.subscribe(() => render(store.getSnapshot())); | |
| store.dispatch(uiEvents); // 内部走 reducer | |
| // 销毁时: unsubscribe(); |
— qwen3.7-max via Qwen Code /review
|
|
||
| ### 与 `state_resync_required` 的配合 | ||
|
|
||
| `session.state_resync_required` 在 reducer 里被映射成 transcript 的 "missed range" 标记,UI 用 `formatMissedRange(state)` 拿到 "missed events X–Y" 文案。reducer 之后**继续 apply 后续事件**,但会标 block 为 `resyncRecovery: true`,渲染层可加视觉提示。具体语义见 [`10-event-bus.md`](./10-event-bus.md) 的「环驱逐 → state_resync_required」一节。 |
There was a problem hiding this comment.
[Critical] resyncRecovery: true 字段在 DaemonTranscriptBlock 中不存在。搜索 types.ts 和 transcript.ts 均无 resyncRecovery 匹配。
Reducer 处理 session.state_resync_required 时实际使用的是 state 级别的字段:awaitingResync、resyncRequiredCount、lastResyncRequired(transcript.ts:291),并在 transcript 中追加一条 status block(不是给现有 block 打标签)。
建议修改为:
`session.state_resync_required` 在 reducer 里被映射成 transcript 的 "missed range" status block,UI 用 `formatMissedRange(state.lastDeliveredId, state.earliestAvailableId)` 拿到 "missed events X–Y" 文案。state 的 `awaitingResync` 标志位会被设置,渲染层可据此加视觉提示。— qwen3.7-max via Qwen Code /review
| - 无运行时配置 —— 全部 reducer / selectors 是纯函数。 | ||
| - 宿主自选渲染层:HTML(`render.ts`)/ 终端(`terminal.ts`)/ 自实现。 | ||
| - 调试用:`render.ts` 的选项支持 `includeRawEvent: true` 把原始 wire frame 一起放进渲染输出。 | ||
|
|
There was a problem hiding this comment.
[Suggestion] includeRawEvent 不是 render.ts 的选项 — 它是 NormalizeDaemonEventOptions(types.ts:384)的字段,传给 normalizeDaemonEvent(evt, { includeRawEvent: true })。DaemonRenderOptions 只有 sanitizeUrls、locale、maxFieldLength。
此外缺少安全提示:raw event 包含完整工具入参(文件内容、shell 命令、env 等敏感数据),多用户部署不应开启。
| - 调试用:`normalizeDaemonEvent(evt, { includeRawEvent: true })` 把原始 wire frame 一起放进 UI 事件。**安全注意**:raw event 含完整工具入参(文件内容、shell 命令、env 等),多用户部署**不要**开启。 |
— qwen3.7-max via Qwen Code /review
…et (Chinese)
Adds 21-file Chinese developer documentation set under
docs/developers/daemon/ for qwen serve daemon mode. Plus one
nav entry in docs/developers/_meta.ts and a sub-nav under
docs/developers/daemon/_meta.ts.
The set complements (not replaces) the existing daemon docs:
- docs/users/qwen-serve.md — operator quickstart, flags, threat model
- docs/developers/qwen-serve-protocol.md — wire-level HTTP routes
- docs/developers/examples/daemon-client-quickstart.md — TS walkthrough
- docs/developers/daemon-client-adapters/* — adapter design drafts
- docs/design/f2-mcp-transport-pool.md — F2 design v2.2
It is the developer architecture reference that's been missing — a new
contributor's path through the system, with one architectural diagram
doc + per-component deep-dives + a quickstart/operations chapter.
Document set:
Foundation
- 00-index.md navigation, glossary, reading paths
- 01-architecture.md system architecture + 6 Mermaid diagrams
Server core
- 02-serve-runtime.md runQwenServe bootstrap, Express, lifecycle,
validatePolicyConfig + InvalidPolicyConfigError
- 03-acp-bridge.md @qwen-code/acp-bridge package internals
- 04-permission-mediation.md MultiClientPermissionMediator — four
policies, N=floor(M/2)+1 quorum + M=2
unanimity edge case, X-Qwen-Client-Id
self-declaration security caveat
- 05-mcp-transport-pool.md McpTransportPool (F2) including pool-aware
snapshot fields (entryCount / entrySummary),
MCPCallInterruptedError on silent transport
drop, canonicalOAuth fingerprint
normalization, extension-uninstall orphan
reap via MAX_IDLE_MS, IDE-close drain path,
/mcp refresh pool gate
- 06-mcp-budget-guardrails.md WorkspaceMcpBudget — modes, hysteresis,
refused-batch coalescing, scope: 'pool'
future reservation
- 07-workspace-filesystem.md WorkspaceFileSystem sandbox + FsError
preservation over ACP wire
- 08-session-lifecycle.md create/attach/load/resume, identity,
heartbeat, eviction
- 09-event-schema.md typed event schema v1 (29 known event types)
with payloads, reducers, envelope-level
metadata (_meta.serverTimestamp), tool-call
_meta (provenance + serverId), SDK reducer
behavior (awaitingResync flag,
RESYNC_PASSTHROUGH_TYPES)
- 10-event-bus.md EventBus + Last-Event-ID replay +
ring-eviction-on-resume state_resync_required
recovery flow
- 11-capabilities-versioning.md capability registry, protocol
version, conditional advertisement
- 12-auth-security.md bearer, host allowlist, mutation gate,
--require-auth, /health exemption,
device-flow, X-Qwen-Client-Id identity
caveat, sanitizeForStderr CVE-2021-42574
defense
Clients
- 13-sdk-daemon-client.md TypeScript SDK — DaemonClient,
DaemonSessionClient, DaemonAuthFlow, SSE,
MCP_RESTART_DEFAULT_TIMEOUT_MS 330s
rationale
- 14-cli-tui-adapter.md DaemonTuiAdapter
- 15-channel-adapters.md DaemonChannelBridge + DingTalk / WeChat /
Telegram
- 16-vscode-ide-adapter.md DaemonIdeConnection (loopback-only)
Reference appendices
- 17-configuration.md env vars + CLI flags + settings.json keys
- 18-error-taxonomy.md typed errors per layer (filesystem boundary,
bridge errors, daemon-host error kinds,
boot-time config errors, device-flow)
- 19-observability.md QWEN_SERVE_DEBUG, debug recipes, /demo
console
Quickstart / operations
- 20-quickstart-operations.md 9 startup recipes; full CLI flag /
env / settings.json reference; boot
fail-loud scenarios; curl validation
checklist; /demo console usage; full call
chain from `qwen serve` to listening
server with line numbers; embedded-mode
examples; graceful vs force shutdown
Each topic doc follows: 概览 / 职责 / 架构 / 流程 (with Mermaid
diagrams) / 状态与生命周期 / 依赖 / 配置 / 注意 & 已知局限 / 参考.
Pinned to the daemon_mode_b_main code surface (F2 pool, F3 mediator,
F4 prereqs). Some referenced files (McpTransportPool, state_resync_
required, validatePolicyConfig, FsError-over-wire, sanitizeForStderr)
land on main only when daemon_mode_b_main next batches up — the doc
set arrives ahead of the feature merges by design.
History: prior revision of this PR was bilingual EN+ZH; switched to
Chinese-only per maintainer preference and added 20-quickstart-
operations.md to consolidate startup / validation / call-chain
content that surfaced during review.
88c66de to
a72ec6a
Compare
Round 4 verification fixes — code drift cleanupRe-verified all
Cross-checked and verified correct (no change needed): Force-pushed as |
|
|
||
| per-session 过滤 / 元数据字段(`includeTools`、`excludeTools`、`trust`、`description`、`extensionName`、`discoveryTimeoutMs`)**被排除**,不同 session 用不同过滤共享同一 entry。 | ||
|
|
||
| OAuth 这一格,`canonicalOAuth(o)`(`mcp-pool-key.ts:75-110`)哈希**每一个** `MCPOAuthConfig` 字段 —— `clientId`、`clientSecret`、`scopes`(排序后)、`audiences`(排序后)、`authorizationUrl`、`tokenUrl`、`redirectUri`、`tokenParamName`、`registrationUrl`。**这是凭证隔离的关键**:仅在 `clientSecret` / `audiences` / `redirectUri` 等字段上有差异的两个 session config 会被正确视作不同 fingerprint,不会共享一条 entry。confidential client(带 `clientSecret`)和 multi-audience token 部署最依赖这条契约。 |
There was a problem hiding this comment.
[Suggestion] Fingerprint hash field list omits 3 fields that fingerprint() (mcp-pool-key.ts:128-145) actually hashes:
authProviderType: cfg.authProviderType ?? null,
targetAudience: cfg.targetAudience ?? null,
targetServiceAccount: cfg.targetServiceAccount ?? null,The doc frames this section as an exhaustive enumeration of what goes into the key ("哈希每一个 MCPOAuthConfig 字段"), but the parent fingerprint() also hashes these three MCPServerConfig-level fields independently of the OAuth sub-object. Two configs that differ only in targetAudience or targetServiceAccount (common in GCP service-account-per-workspace setups) produce distinct fingerprints and never share a pool entry — a real isolation property that the doc silently drops.
— qwen3.7-max via Qwen Code /review
| restartByName( | ||
| name, | ||
| opts?, | ||
| ): Promise<RestartResult | { entries: RestartResult[] }>; |
There was a problem hiding this comment.
[Suggestion] restartByName return type presents a dual-shape contract (RestartResult | { entries: RestartResult[] }) that doesn't exist at the pool level. McpTransportPool.restartByName() (mcp-transport-pool.ts:549-610) always returns an array:
Promise<Array<{
entryIndex: number;
restarted: boolean;
durationMs?: number;
reason?: string;
}>>There is no single-vs-multi shape distinction, and RestartResult is not an exported type anywhere in the codebase (grep returns zero hits). The bridge layer has its own three-shape union, but that's a different contract in a different package.
| ): Promise<RestartResult | { entries: RestartResult[] }>; | |
| ): Promise< | |
| Array<{ | |
| entryIndex: number; | |
| restarted: boolean; | |
| durationMs?: number; | |
| reason?: string; | |
| }> | |
| >; |
— qwen3.7-max via Qwen Code /review
Summary
What changed: adds
docs/developers/daemon/— a 21-file Chinese developer documentation set covering qwen-code daemon mode end-to-end (qwen serveruntime,acp-bridgepackage internals, multi-client permission mediation F3, MCP transport pool F2, MCP budget guardrails, workspace filesystem boundary, session lifecycle + identity, typed event schema v1 with all 29 known events, SSE event bus, capabilities + protocol versioning, auth + security, TypeScript SDK, CLI TUI / channel / VSCode IDE adapters, configuration, error taxonomy, observability, quickstart/operations). Plus a one-line nav entry indocs/developers/_meta.tsand a sub-nav underdocs/developers/daemon/_meta.ts. 23 files, ~4,366 lines.Why it changed: the existing daemon docs are user-facing (
users/qwen-serve.md), wire-level (developers/qwen-serve-protocol.md), or draft adapter notes (daemon-client-adapters/*.md). There is no developer architecture reference that walks a new contributor through every load-bearing piece of the daemon stack. New contributors had to read the source. This set fills the gap with one architectural-diagram doc + per-component deep-dives + a consolidated quickstart/operations chapter.Reviewer focus:
01-architecture.md(process topology, package map, HTTP request, SSE delivery, permission mediation, MCP pool).09-event-schema.mdagainstDAEMON_KNOWN_EVENT_TYPE_VALUESinpackages/sdk-typescript/src/daemon/events.ts:13-63.04-permission-mediation.mdagainst thePermissionPolicyunion inpackages/acp-bridge/src/permission.ts.20-quickstart-operations.md— the new "怎么把 daemon 跑起来 + 验证 + 调用链" chapter. It consolidates 9 startup recipes, full CLI/env/settings tables, 11 boot fail-loud scenarios, a curl validation checklist,/demousage, and the full call chain fromqwen serve→main()→ yargs →serveCommand.handler→runQwenServe()→createServeApp()→app.listen()with source line numbers at each step.mainonly whendaemon_mode_b_mainnext batches up (F2McpTransportPool, F3MultiClientPermissionMediator, F4 prereqs likestate_resync_required/_meta.serverTimestamp/FsError-over-wire /validatePolicyConfig/sanitizeForStderr). The doc set deliberately arrives ahead of those merges so it's available the moment they land. Reviewers greppingmainfor the referenced file paths will see misses until then — those are not broken references, they're forward references against thedaemon_mode_b_maincode surface.Validation
Static checks
End-to-end runtime validation
Booted
qwen serve --port 4171against this worktree's source and hit every documented endpoint:All endpoints documented in
20-quickstart-operations.md§ 7 (curl validation checklist) returned the expected shapes.daemon_mode_b_main; sidebar surfaces a new "Daemon 模式 · 开发者深度指南" entry under "Dive Into Qwen Code".docs/developers/daemon/00-index.mdfor the navigation + glossary + reading order.docs/developers/daemon/20-quickstart-operations.mdfor the "怎么跑起来 + 怎么验证 + 调用链" reference — this is the fastest path to confirm the docs match the running daemon.docs/developers/daemon/01-architecture.mdto inspect the 6 Mermaid system diagrams.file:linereferences againstorigin/daemon_mode_b_main(e.g.,git show origin/daemon_mode_b_main:packages/acp-bridge/src/permissionMediator.ts | head -200).docs/. Markdown-only — no runtime, build, or public-API surface touched. The only non-docs/developers/daemon/change is the single line indocs/developers/_meta.tsadding the nav entry.Scope / Risk
daemon_mode_b_maincode surface (HEAD46f8d48f1at PR creation time). When that branch next batches tomain, references will resolve cleanly; in the interim, reviewers greppingmainfor paths likepackages/core/src/tools/mcp-transport-pool.tsorpackages/acp-bridge/src/permissionMediator.tswill see misses. Either (a) merge after the nextdaemon_mode_b_main → mainbatch, or (b) merge now to land the docs ahead.00-index.md):packages/webui/— component library that renders host-relayed messages, not itself a daemon HTTP client.packages/zed-extension/— uses stdio ACP directly, bypasses the daemon.qwen --serveco-host, protocol completion) — surface not yet stable.Testing Matrix
Testing matrix notes:
docs/developers/daemon/is the one-line nav entry indocs/developers/_meta.ts./demoin browser) was run on 🍏 macOS 25 (Darwin 25.4.0) against this worktree's HEAD, confirming the daemon documented in this PR matches reality.Linked Issues / Bugs
Related: #3803 (daemon design), #4175 (F-series milestones — F1 acp-bridge lift, F2 MCP transport pool, F3 multi-client permission mediation, F4 in progress). No closing keyword — this is supplementary developer documentation that doesn't close the implementation issues.
🤖 Generated with Qwen Code