Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
215e0db
refactor(serve): 1 daemon = 1 workspace (#3803 §02)
wenshao May 13, 2026
3152027
fix(serve): address review findings on the §02 refactor
wenshao May 13, 2026
2cd0ff8
fix(serve): BkUyD overwrite race + Windows-fragile test + doSpawn-fai…
wenshao May 13, 2026
cdaa760
docs(serve): update daemon-client-quickstart for §02 single-workspace
wenshao May 13, 2026
5e309a9
feat(sdk,test): align SDK types + integration tests with §02 single-w…
wenshao May 13, 2026
4d41fce
fix(serve): address /review feedback from gpt-5.5 + deepseek-v4-pro
wenshao May 13, 2026
adcb3f9
fix(serve/test): make /work/bound literals Windows-portable
wenshao May 13, 2026
c922a4e
fix(serve): address second /review round (gpt-5.5 + deepseek-v4-pro)
wenshao May 13, 2026
1c7f5f0
fix(serve): use const cwd in POST /session (prefer-const lint)
wenshao May 13, 2026
67a937a
fix(serve): address third /review round (gpt-5.5 + glm-5.1 + deepseek…
wenshao May 14, 2026
19650b5
fix(serve,sdk): address fourth /review round (deepseek-v4-pro x2)
wenshao May 14, 2026
3e544f3
fix(serve): address tanzhenxin REQUEST_CHANGES (cold-spawn + streamin…
wenshao May 14, 2026
b34a28b
fix(serve): address third /review round (gpt-5.5 + glm-5.1 + deepseek…
wenshao May 14, 2026
0c6e963
fix(serve): apply auto-fixes from /review (#4113)
wenshao May 15, 2026
d87c9fc
fix(serve/test): bind workspace explicitly in GET /workspace tests
wenshao May 15, 2026
594846d
fix(serve,sdk): address fourth /review round (deepseek-v4-pro x2)
wenshao May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions docs/developers/examples/daemon-client-quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ In one terminal:
```bash
cd your-project/
qwen serve --port 4170
# → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge)
# → qwen serve listening on http://127.0.0.1:4170 (mode=http-bridge, workspace=/path/to/your-project)
```

Per [#3803](https://github.com/QwenLM/qwen-code/issues/3803) §02 each daemon binds to one workspace at boot (the current `cwd`, or override with `--workspace /path/to/dir`). The daemon's bound path is advertised on `/capabilities.workspaceCwd` so clients can pre-flight check + omit `cwd` from `POST /session`.

In another:

```bash
Expand All @@ -28,13 +30,23 @@ const client = new DaemonClient({
// token: process.env.QWEN_SERVER_TOKEN, // required for non-loopback binds
});

// 1. Confirm we can reach the daemon and gate UI on its features.
// 1. Confirm we can reach the daemon, gate UI on its features, and
// read back the daemon's bound workspace (#3803 §02).
const caps = await client.capabilities();
console.log('Daemon features:', caps.features);

// 2. Spawn-or-attach a session for the current workspace.
console.log('Daemon workspace:', caps.workspaceCwd); // canonical bound path

// 2. Spawn-or-attach a session. Two equally-valid shapes:
// (a) pass `workspaceCwd: caps.workspaceCwd` to be explicit, or
// (b) omit `workspaceCwd` entirely — the SDK then sends no `cwd`
// field and the daemon route falls back to its bound
// workspace. The (b) shape is concise but assumes you trust
// `caps.workspaceCwd` to be whatever you intended.
// A non-empty `workspaceCwd` that doesn't canonicalize to the
// daemon's bound path yields `400 workspace_mismatch` (see
// "Workspace mismatch" below).
const session = await client.createOrAttachSession({
workspaceCwd: process.cwd(),
workspaceCwd: caps.workspaceCwd,
});
console.log(`session=${session.sessionId} attached=${session.attached}`);

Expand Down Expand Up @@ -139,9 +151,12 @@ case 'permission_request': {

## Shared-session collaboration

Two clients pointed at the same daemon and `cwd` end up on the same session:
Two clients pointed at the **same daemon** end up on the same session. Per #3803 §02 each daemon is bound to ONE workspace at boot, so the daemon launched as `qwen serve --workspace /work/repo` (or `cd /work/repo && qwen serve`) is what both clients connect to:

```ts
// Daemon was launched as `qwen serve --workspace /work/repo` so
// `caps.workspaceCwd === '/work/repo'` for both clients.

// Client A (e.g. an IDE plugin)
const a = await clientA.createOrAttachSession({ workspaceCwd: '/work/repo' });
console.log(a.attached); // false — A spawned the agent
Expand All @@ -154,6 +169,35 @@ console.log(a.sessionId === b.sessionId); // true

Both clients see the same `session_update` / `permission_request` stream. Either can send a prompt; they FIFO-queue per the agent's "one active prompt per session" guarantee.

## Workspace mismatch

If `workspaceCwd` doesn't match the daemon's bound workspace, `createOrAttachSession` rejects with `DaemonHttpError` carrying status `400` and a structured body:

```ts
import { DaemonHttpError } from '@qwen-code/sdk';

try {
await client.createOrAttachSession({ workspaceCwd: '/some/other/project' });
} catch (err) {
if (err instanceof DaemonHttpError && err.status === 400) {
const body = err.body as {
code?: string;
boundWorkspace?: string;
requestedWorkspace?: string;
};
if (body.code === 'workspace_mismatch') {
console.error(
`This daemon is bound to ${body.boundWorkspace}, ` +
`not ${body.requestedWorkspace}. Start a separate daemon ` +
`for that workspace, or route to the right one.`,
);
}
}
}
```

Multi-workspace deployments run one daemon per workspace on separate ports — there's no intra-daemon routing under §02. An orchestrator (or the user's launcher) picks the right daemon based on the project the client wants to talk to.

## Authentication

When the daemon was started with a token (any non-loopback bind requires one):
Expand Down
20 changes: 18 additions & 2 deletions docs/developers/qwen-serve-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ with status `400`.

with status `404`.

`WorkspaceMismatchError` for a `POST /session` whose `cwd` doesn't canonicalize to the daemon's bound workspace (#3803 §02 — 1 daemon = 1 workspace) returns `400` with:

```json
{
"error": "Workspace mismatch: daemon is bound to \"…\" but request asked for \"…\". …",
"code": "workspace_mismatch",
"boundWorkspace": "/path/the/daemon/binds",
"requestedWorkspace": "/path/in/the/request"
}
```

Use this to detect mismatch pre-flight: read `workspaceCwd` off `/capabilities` and omit `cwd` from `POST /session` (it falls back to the bound workspace), or route the request to a daemon bound to `requestedWorkspace`.

`POST /session` past the daemon's `--max-sessions` cap returns `503` with a `Retry-After: 5` header and:

```json
Expand Down Expand Up @@ -93,14 +106,17 @@ Pass `?deep=1` (also accepts `?deep=true` or bare `?deep`) for a probe that expo
"v": 1,
"mode": "http-bridge",
"features": ["health", "capabilities", "..."],
"modelServices": []
"modelServices": [],
"workspaceCwd": "/canonical/path/to/workspace"
}
```

Stable contract: when `v` increments the frame layout has changed in a backwards-incompatible way.

> **`modelServices` is always `[]` in Stage 1.** The agent uses its single default model service and doesn't enumerate it over the wire. Stage 2 will populate this from registered model adapters so SDK clients can build service-pickers; until then, do NOT rely on this field being non-empty.

> **`workspaceCwd`** is the canonical absolute path this daemon binds to (#3803 §02 — 1 daemon = 1 workspace). Use it to (a) detect mismatch before posting `/session` and (b) omit `cwd` on `POST /session` (the route falls back to this path). Multi-workspace deployments expose multiple daemons on different ports, each with its own `workspaceCwd`. Additive to v=1: pre-§02 v=1 daemons omit the field — clients that target older builds should null-check before consuming it.

### `POST /session`

Spawn a new agent or attach to an existing one (under `sessionScope: 'single'`, the default).
Expand All @@ -116,7 +132,7 @@ Request:

| Field | Required | Notes |
| ---------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `cwd` | yes | Absolute path. Relative paths return `400`. Workspace paths are canonicalized via `realpathSync.native` (with a resolve-only fallback for non-existent paths) so case-insensitive filesystems don't fork sessions per spelling. |
| `cwd` | no | Absolute path matching the daemon's bound workspace. If omitted, the route falls back to `boundWorkspace` (read it off `/capabilities.workspaceCwd`). A mismatched non-empty `cwd` returns `400 workspace_mismatch` (#3803 §02 — 1 daemon = 1 workspace). Workspace paths are canonicalized via `realpathSync.native` (with a resolve-only fallback for non-existent paths) so case-insensitive filesystems don't reject sessions per spelling. |
| `modelServiceId` | no | Selects which configured _model service_ the agent will route through (the back-end provider — Alibaba ModelStudio, OpenRouter, etc). If omitted the agent uses its default. If the workspace already has a session, this calls `setSessionModel` on the existing one and broadcasts `model_switched`. Distinct from `modelId` on `POST /session/:id/model`, which selects the model **within** an already-bound service. The `modelServices` array on `/capabilities` is reserved for advertising configured services; in Stage 1 it is always `[]` (the agent's default service is used and not enumerated over HTTP). |

Response:
Expand Down
Loading
Loading