qwen-code/docs/developers/daemon/08-session-lifecycle.md
jinye 38384ae7b9
feat(serve): Add cursor-paged transcript replay endpoint (#6525)
* feat(serve): Add cursor-paged transcript replay endpoint

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): Bound transcript replay indexing

Limit transcript index builds to bounded snapshots and surface oversized transcript errors as 413 responses. Give transcript status calls a dedicated timeout and update the capabilities integration baseline.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Validate transcript cursors

Sign transcript cursors so forged snapshot sizes cannot bypass the index cache, and keep hasMore tied to persisted record availability when replay conversion returns a partial page.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Lazy-init transcript cursor secret

Avoid generating the transcript cursor HMAC key while importing the core barrel so unrelated tests with narrow crypto mocks can load core without requiring randomBytes. Keep the VS Code companion crypto mock partial so it only replaces the auth-token UUID behavior it asserts on.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): Address transcript replay review suggestions

Mark bounded replay truncation frames as having a transcript endpoint, sanitize paged transcript replay conversion errors, and remove the core reader's incomplete pre-encoded cursor field so cursors are only emitted after replay continuation state is merged.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): Stabilize transcript replay pagination

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Avoid quadratic transcript line scanning

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Mark transcript history gaps

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Address transcript reader review comments

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): Address transcript replay review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): align transcript cursor preflight errors

Return transcript snapshot conflicts for cursor pagination when the active JSONL can no longer be found during route preflight. Add route-level and integration coverage for full transcript paging, and document the boolean fullTranscriptAvailable SDK contract.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(cli): Cover paged dangling tool call replay

Add a HistoryReplayer.replayPage regression test that carries a dangling tool call through pendingToolCalls and finalizes it on a later page.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Bound transcript index cache bytes

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: Address transcript replay review follow-ups

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): Preserve pending tool calls on transcript replay errors

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6525)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix CI failure on PR #6525

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(serve): warm transcript-replay tools leniently

The read-only transcript-replay Config sets skipSkillManager, but Config.initialize() still runs toolRegistry.warmAll({ strict: true }), which constructs SkillTool whose constructor throws when no SkillManager exists. The throw escaped the replay try/catch and surfaced as JSON-RPC -32603, so GET /session/:id/transcript returned HTTP 500 for every persisted session.

Add a lenientToolWarmup initialize option and set it for the replay Config so tools that cannot construct under the deliberately-skipped subsystems are logged and skipped instead of aborting initialize(). Replay only needs optional tool_call metadata and ToolCallEmitter already falls back to the recorded tool name, so buildable tools keep full title/kind. This supersedes the narrower excludeTools:[Skill] guard, which is removed.

* fix(core): invalidate transcript index cache on in-place rewrites

An in-place transcript rewrite that keeps the inode and byte length (e.g. rsync --inplace or a redaction pass) reused a stale cached index, because makeCacheKey() keyed only on path:dev:ino:size. readSegmentRecords then found each recorded offset parsing to a different uuid and dropped it, so GET /session/:id/transcript answered 200 with an empty events array instead of the documented 409.

Include the file mtime in the index cache key so a fresh read after a same-size rewrite rebuilds the index, and raise SessionTranscriptSnapshotUnavailableError (-> 409) on a uuid mismatch or missing fragment instead of silently returning a short/empty transcript. Also make the qwen-serve docs explicit that at the default --channel-idle-timeout-ms 0 each page rebuilds the index (O(snapshotSize)).

* codex: address PR review feedback (#6525)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* qwen: fix CI failure on PR #6525

The Run ESLint step failed on vitest/valid-expect in packages/acp-bridge/src/bridge.test.ts: the getSessionTranscriptPage timeout test stores expect(request).rejects.toBeInstanceOf(BridgeTimeoutError) and awaits it only after advancing the fake timers (a deliberate deferred await so the pending timeout rejection has a handler before it fires). Auto-fixing would add an inline await and deadlock the test, so scope-disable the rule on that assignment with a rationale. lint:ci and the affected test pass.

* qwen: address PR review feedback (#6525)

Withhold nextCursor on a mid-page transcript replay error. When collectHistoryReplayUpdatesPage catches a replayError partway through a page, records after the failed one are dropped and pendingToolCalls reflect partial state; still emitting nextCursor advanced the client past the dropped records and carried corrupted pendingToolCalls forward (phantom in-progress tool calls on later pages). Now nextCursor is withheld whenever replay.replayError is set — the page is already flagged partial + replayError, so the client stops instead of paginating with corrupted cursor state. Update the handler test to assert no cursor is issued on a replay error.

* qwen: address PR review feedback (#6525)

Log when parseTranscriptReplayState drops malformed pending tool calls from a replay cursor. Previously rawPending.filter(isPendingReplayToolCall) silently discarded entries that no longer matched the shape (e.g. a cursor from a newer daemon or corrupted in transit), turning a version-mismatch/corruption into a hard-to-diagnose 'tool never completed' artifact on later pages. Now emit a debug warning with the dropped/total counts; behavior is otherwise unchanged.

* fix(serve): address transcript review feedback

Dispose superseded replay configs, preserve structured resolution errors, sanitize multi-workspace failures, and expand transcript replay coverage across unit and real-daemon integration paths.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* qwen: address transcript review feedback (#6525)

- [Critical] Map a missing transcript session to HTTP 404: the child throws a raw resourceNotFound (ENOENT without a cursor) that fell through sendBridgeError to 500. bridge.getSessionTranscriptPage now translates it to SessionNotFoundError, mirroring the load/resume path, with a bridge test.
- Dedup the untrusted-session-owner 403 onto the shared sendUntrustedWorkspaceResponse so the response format/message stay consistent across session routes (route logging + context preserved).
- Add coverage for parseTranscriptReplayState's non-object replay branch (cursor replay=garbage) -> empty pendingToolCalls + default cumulativeUsage.
- Document that cursorHmacKeys are cached for the daemon lifetime (external key rotation requires a restart).

* qwen: adopt transcript review suggestions (#6525)

- Add a handler test that a mid-page replay error preserves already-emitted events (events>=1) alongside partial+replayError and withholds the cursor.
- Add a two-call handler test for the cross-page cumulativeUsage round-trip: page 1 folds the bumped usage into the encoded cursor; page 2 decodes and propagates it into the replay context.
- Log (not silently drop) a superseded structured error in the multi-workspace transcript resolution fallback.

* qwen: clean up transcript test fixtures to fix no-AK CI flake (#6525)

The transcript-paging integration suite wrote ~6 persisted chats/*.jsonl sessions into the daemon's project dir and never removed them. Because vitest runs a file's suites sequentially, those leftover sessions widened a pre-existing race in the later 'PATCH /session/:id/metadata > updates displayName' test (a freshly-created session can exist on disk but not yet appear in the listWorkspaceSessions page), making it fail deterministically in the no-AK smoke run. Add an afterAll to the transcript suite that removes the project chats/ dir, restoring a clean session list for subsequent suites. Verified: full no-AK suite now passes 43/43 across repeated runs.

* qwen: harden transcript reader test timestamps + assert page fields (#6525)

The record() helper derived the ISO timestamp seconds from text.length, producing invalid values (e.g. 00:00:013) once a record's text reached 10+ chars — harmless today only because no test asserted startTime. Replace it with a monotonic base+offset timestamp (always valid, strictly increasing). Also assert the previously-unchecked required SessionTranscriptRecordPage fields (sessionId, filePath, startTime, lastUpdated); the strict-ISO checks on startTime/lastUpdated guard against the timestamp-helper class of bug.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-10 16:34:43 +00:00

308 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Session Lifecycle & Identity
## Overview
A daemon **session** is one logical conversation pinned to one ACP `sessionId`. The bridge maintains a `SessionEntry` per session (see [`03-acp-bridge.md`](./03-acp-bridge.md)) which couples the ACP child connection with HTTP-side bookkeeping: prompt FIFO, model-change FIFO, event bus, pending permissions, attached clients, heartbeats, restore state, terminal-frame tombstones.
A daemon **client** is identified by `X-Qwen-Client-Id` — an opaque, daemon-validated string the HTTP caller stamps on its requests. The bridge tracks which clients are attached to which sessions, and uses the originator client id to drive the `designated` permission policy, audit trails, and event attribution.
This doc explains every session lifecycle transition (create / attach / load / resume / close / die / evict) and every identity surface the daemon exposes.
## Responsibilities
- Mint, attach, restore, and reap sessions.
- Validate `X-Qwen-Client-Id` and reject malformed ids.
- Track multiple attached clients per session (`clientIds: Map<string, count>`, `attachCount`).
- Stamp `originatorClientId` on outbound events.
- Run heartbeats so dashboards know which clients are still connected.
- Surface session metadata (`displayName`) that operators set via `PATCH /session/:id/metadata`.
- Drive terminal frame emission (`session_died`, `session_closed`, `client_evicted`, `stream_error`).
## Architecture
| Concern | Source | Notes |
| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| `SessionEntry` | `packages/acp-bridge/src/bridge.ts` | Per-session struct; see [`03-acp-bridge.md`](./03-acp-bridge.md) for full field listing. |
| `BridgeSession` (public) | `packages/acp-bridge/src/bridgeTypes.ts` | `{ sessionId, workspaceCwd, attached, clientId?, createdAt? }` returned to HTTP handlers. |
| `BridgeSessionState` | `packages/acp-bridge/src/bridgeTypes.ts` | `LoadSessionResponse \| ResumeSessionResponse` cached on the entry as `restoreState`. |
| `DaemonSession` (SDK) | `packages/sdk-typescript/src/daemon/types.ts` | `{ sessionId, workspaceCwd, attached, clientId?, createdAt? }`. |
| Client-id validation | `packages/acp-bridge/src/bridge.ts` (around `spawnOrAttach`) | Pattern `[A-Za-z0-9._:-]{1,128}`; `InvalidClientIdError` if malformed. |
| Session disconnect-reaper | `packages/cli/src/serve/server.ts` | Tracks spawn-owner disconnects with `attachCount` + `spawnOwnerWantedKill`. |
### State machine
```mermaid
stateDiagram-v2
[*] --> SpawnInProgress: POST /session
SpawnInProgress --> Live: newSession success
SpawnInProgress --> [*]: initialize failure / spawn error
Live --> Live: attach (sessionScope=single, bump attachCount)
Live --> Live: detach (decrement attachCount)
Live --> RestoreInProgress: POST /session/:id/load or /resume
RestoreInProgress --> Live: restoreState cached on entry
RestoreInProgress --> Live: RestoreInProgressError (coalesce waiters)
Live --> Closed: DELETE /session/:id (last client)
Live --> Died: ACP child exit / channel.exited fired
Closed --> [*]: session_closed terminal frame
Died --> [*]: session_died terminal frame
```
### Attach vs spawn
Under `sessionScope: 'single'` (default), the bridge's `defaultEntry` is shared by every connecting client. A `POST /session` that arrives while `defaultEntry` already exists returns `attached: true` without spawning a new ACP child. The bridge synchronously bumps `attachCount` and registers the caller's `X-Qwen-Client-Id` into `clientIds`.
Under `sessionScope: 'thread'`, each thread can mint a distinct session. The caller still respects `maxSessions`.
### Identity
`X-Qwen-Client-Id` is **optional** but **strongly recommended**. The daemon does not generate one on the caller's behalf — clients pick their own and reuse it across requests so the daemon can attribute votes, audit events, and detect reconnects.
Validation rules:
- Charset: `[A-Za-z0-9._:-]`.
- Length: 1128.
- Outside this set: `InvalidClientIdError` (`400`).
The daemon stamps `originatorClientId` on outbound SSE events when:
1. The request that triggered the event carried `X-Qwen-Client-Id`, AND
2. The id is currently registered in the session's `clientIds` set, AND
3. The session has an `activePromptOriginatorClientId` set (inline `sessionUpdate` and `permission_request` inherit the originator from the active prompt).
Anonymous callers (no `X-Qwen-Client-Id`) work fine for `first-responder` policy; `designated` rejects their votes with `permission_forbidden{ reason: 'designated_mismatch' }`; `consensus` rejects with the same `forbidden` reason because the voter is not in the issue-time `votersAtIssue` snapshot; `local-only` is the only policy that accepts anonymous loopback voters.
## Workflow
### Create or attach
```mermaid
sequenceDiagram
autonumber
participant C as Client
participant R as POST /session
participant B as Bridge.spawnOrAttach
participant CH as ACP child
C->>R: POST /session<br/>X-Qwen-Client-Id: alice<br/>{cwd, sessionScope?}
R->>R: validate clientId pattern
R->>B: spawnOrAttach({cwd, sessionScope, clientId})
alt single scope + defaultEntry exists
B->>B: bump attachCount; register clientId
B-->>R: {sessionId, attached: true, restoreState?}
else cold
B->>CH: spawn + ACP initialize + newSession
CH-->>B: sessionId
B->>B: build SessionEntry; register in byId
B-->>R: {sessionId, attached: false}
end
R-->>C: 200 { sessionId, attached, ... }
```
### Load / resume
`POST /session/:id/load` — restores a persisted session and returns the current bounded replay snapshot window (`session/load` notifications or response-mode replay are seeded before the response returns).
`POST /session/:id/resume` — restores without replay (`connection.unstable_resumeSession`, exposed under the stable `session_resume` daemon capability; `unstable_session_resume` remains a deprecated alias).
Both:
1. Use a per-session `pendingRestoreIds` set on the channel so concurrent restore calls coalesce (`RestoreInProgressError`).
2. Cache `restoreState` on the entry so a late attacher gets the same payload the original restorer did.
### Heartbeat
`POST /session/:id/heartbeat` updates `sessionLastSeenAt` regardless of `clientId`. If the request carries a registered `X-Qwen-Client-Id`, `clientLastSeenAt.set(clientId, Date.now())` also updates. Per-client eviction is **not** implemented in v1; revocation is planned for F-series Wave 5. Today, heartbeats provide observability for dashboards and for the upcoming revocation policy in PR 24.
### Metadata
`PATCH /session/:id/metadata` accepts `{displayName?}`. Validation:
- Max length: `MAX_DISPLAY_NAME_LENGTH = 256`.
- Must not contain control characters (`hasControlCharacter` rejects code points ≤ 0x1f or == 0x7f).
- `InvalidSessionMetadataError` (`400`) on violation.
A successful update fans `session_metadata_updated` to every subscriber.
### Termination
| Terminal frame | Trigger |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `session_closed` | `DELETE /session/:id` (client_close) or programmatic close. |
| `session_died` | `channel.exited` fires for any reason (crash, child kill). Carries `exitCode?` + `signalCode?` when the OS exit path was used. |
| `client_evicted` | Per-subscriber queue overflow on the EventBus (see [`10-event-bus.md`](./10-event-bus.md)). NOT a session-level termination — only this subscriber is closed. |
| `stream_error` | SubscriberLimitExceededError or other route-level stream failure. |
Pending permissions are resolved as `{kind:'cancelled', reason:'session_closed'}` via `mediator.forgetSession(sessionId)` at every termination path.
### Disconnect-reaper guard
When the spawn-owning client's HTTP response cannot be written (TCP reset mid-handshake), the route calls `killSession({ requireZeroAttaches: true })`. If another client has already attached (`attachCount > 0`), the guard short-circuits and the session lives on. Setting `spawnOwnerWantedKill = true` remembers the intent so a later `detachClient()` that brings `attachCount` back to 0 completes the deferred reap. Without this, a fast-disconnecting spawn owner would tear down a healthy session every other reconnect.
## State & Lifecycle
`SessionEntry` fields critical to lifecycle:
| Field | Type | Meaning |
| -------------------------------- | --------------------- | -------------------------------------------------------------------------------- |
| `clientIds` | `Map<string, number>` | Registered client ids → registration ref count. |
| `attachCount` | `number` | Times `spawnOrAttach` returned `attached: true` for this entry. |
| `activePromptOriginatorClientId` | `string?` | Originator for the prompt currently running. |
| `restoreState` | `BridgeSessionState?` | Cached load/resume response so late attachers see consistent payloads. |
| `spawnOwnerWantedKill` | `boolean` | Deferred-reap tombstone (see disconnect-reaper above). |
| `sessionLastSeenAt` | `number?` | Most recent heartbeat across any client (epoch ms). |
| `clientLastSeenAt` | `Map<string, number>` | Per-client heartbeat. |
| `pendingPermissionIds` | `Set<string>` | ACP requestIds currently pending — used on cancel/close to resolve as cancelled. |
## Dependencies
- ACP layer: `connection.newSession`, `connection.unstable_resumeSession`, `connection.loadSession`.
- [`03-acp-bridge.md`](./03-acp-bridge.md) for the surrounding bridge architecture.
- [`04-permission-mediation.md`](./04-permission-mediation.md) for how originator + identity drive policy decisions.
- [`10-event-bus.md`](./10-event-bus.md) for terminal-frame delivery.
## Additional session endpoints
These endpoints extend the base lifecycle surface:
### Non-blocking Prompt (`non_blocking_prompt` capability tag)
`POST /session/:id/prompt` now returns HTTP **202** with
`{ promptId, lastEventId }` instead of blocking until the prompt completes. The
actual result arrives on SSE as `turn_complete` / `turn_error`, and the
`promptId` field correlates those events with the 202 response.
`DaemonSessionClient.prompt()` automatically uses the non-blocking path when it
has an active event subscription and transparently matches the result from the
SSE stream.
### Session Recap (`session_recap` capability tag)
`POST /session/:id/recap` asks the fast model for a one-line "where did I leave
off" summary. It returns `{ sessionId, recap: string | null }`; `null` means the
history was too short or the model failed temporarily. This endpoint is
best-effort.
### Session BTW / Side Question (`session_btw` capability tag)
`POST /session/:id/btw` asks a one-off question against the session context
without interrupting the main conversation flow. It uses `runForkedAgent` on the
cache path for a single-turn, no-tool LLM call and returns
`{ sessionId, answer: string | null }`. The implementation enforces
`BTW_MAX_INPUT_LENGTH`, cross-session leakage guards, and timeout handling.
### Shell Command Execution
`POST /session/:id/shell` executes a shell command directly on the daemon host,
without routing through the LLM. It streams output on the session SSE bus via
`user_shell_command` / `user_shell_result` events and injects the command plus
result into the LLM conversation history. The response is
`{ exitCode, output, aborted }`.
### Session Detach
`POST /session/:id/detach` explicitly detaches a client from a session by
decrementing `attachCount`; it does not close the session by itself. If no other
attach or subscriber remains, the session is reaped. The endpoint returns 204.
### Batch Session Delete
`POST /sessions/delete` accepts `{ sessionIds: string[] }` (up to 100 ids),
closes bridge sessions, and deletes active or archived transcript files. If both
active and archived JSONL files exist for the same id, hard delete removes both
so operators can clear the conflict. It cleans active and archived worktree
sidecars, but leaves file-history snapshots, subagent transcripts, and runtime
sidecars intact. It uses `Promise.allSettled` for resilience and returns
`{ removed, notFound, errors }`.
### Session Archive
`POST /sessions/archive` moves inactive session JSONL files from `chats/` into
`chats/archive/`. If the target session is live, the daemon first enters a
per-session archive gate and performs a strict close that requires the ACP child
to flush `ChatRecordingService`; archive leaves the JSONL in place if close or
flush fails.
`POST /sessions/unarchive` moves archived JSONL files back to `chats/`. This is
only a storage-state transition; clients must call `session/load` or
`session/resume` afterward. Archived sessions return `409 session_archived` for
load/resume, and mutations racing an archive transition return
`409 session_archiving`.
### Context Usage (`session_context_usage` capability tag)
`GET /session/:id/context-usage` returns structured context-window usage.
`?detail=true` includes finer-grained usage grouped by tool, memory, and skill.
### Session Stats (`session_stats` capability tag)
`GET /session/:id/stats` returns usage statistics: model metrics
(input/output tokens, cache reads/writes, total cost), per-tool call counts and
latencies, file edit counts, and per-skill invocation counts for the live
session. The `skills` block reflects skill body loads and skill slash commands
within this session only; it is not a cross-session activity aggregate.
### Session Tasks (`session_tasks` capability tag)
`GET /session/:id/tasks` returns a background-task snapshot for agent tasks,
shell tasks, monitor tasks, and their lifecycle states. Agent entries spawned
by another sub-agent carry optional lineage fields (`parentAgentId`,
`parentName`, `depth`) so clients can render nested sub-agents as a tree; see
the payload example in `qwen-serve-protocol.md`.
### Session LSP Status (`session_lsp` capability tag)
`GET /session/:id/lsp` returns sanitized per-session LSP status for daemon
clients: enablement, aggregate server counts, unavailable/initialization state,
and per-server `name`, `status`, `languages`, `transport`, `command`, and
`error`. Disabled or unavailable LSP is represented as HTTP 200 status data,
not as a transport error.
### Compacted Replay
`POST /session/:id/load` now returns a `BridgeRestoredSession` that can include
`compactedReplay?: BridgeEvent[]`, `liveJournal?: BridgeEvent[]`, and
`lastEventId?: number`. These fields are the daemon's bounded in-memory replay
window for a live session, not a full transcript API. The default window cap is
4 MiB per live session (`--compacted-replay-max-bytes`), and boot rejects
invalid caps; the hard ceiling is 256 MiB. `compactedReplay` is produced by
`TurnBoundaryCompactionEngine`: at turn boundaries it folds consecutive text /
thought blocks, collapses tool-call sequences to their final state, discards
transient signals, and produces O(turns) replay logs instead of O(tokens) logs
(typically a 25-30x reduction). When older replay entries have been dropped
from that byte window, `compactedReplay[0]` is a synthetic id-less
`history_truncated` marker with `{reason: 'replay_window_exceeded',
truncatedEvents, retainedEvents, maxBytes, truncatedTurns?,
fullTranscriptAvailable: boolean}`. `fullTranscriptAvailable` is a capability
flag: `true` means the client can page the full persisted transcript with
`GET /session/:id/transcript`, while `false` means only the bounded replay is
available. Clients should render it as status and apply the retained replay
normally; it must not trigger a resync loop.
### ACP Child Preheat
`bridge.preheat()` warms the ACP child process before the first session so that
the first real session avoids cold-start latency. It pairs with
`channelIdleTimeoutMs`, which keeps the ACP child alive after the last session
closes, and skip-relaunch behavior, which reuses an already idle child when a
new session arrives.
## Configuration
- `BridgeOptions.maxSessions` (default 20) — cap.
- `BridgeOptions.sessionScope` (default `'single'`; optional `'thread'`).
- `BridgeOptions.initializeTimeoutMs` (default 10s) — ACP `initialize` handshake.
- `BridgeOptions.channelIdleTimeoutMs` (default 0; reap the ACP child immediately).
- Capability tags: `session_create`, `session_scope_override`, `session_load`, `session_resume`, `unstable_session_resume` (deprecated alias), `session_list`, `session_close`, `session_metadata`, `session_set_model`, `client_identity`, `client_heartbeat`, `session_recap`, `session_btw`, `session_context_usage`, `session_tasks`, `session_stats`, `session_lsp`, `session_status`, `non_blocking_prompt`.
## Caveats & Known Limits
- `connection.unstable_resumeSession` may still be unstable at the ACP layer, but the daemon advertises the committed v1 route contract with `session_resume`. `unstable_session_resume` is kept only as a deprecated compatibility alias.
- v1 has **no per-client eviction**; only per-session and per-subscriber termination. Revocation policy is F-series Wave 5 / PR 24.
- `client_evicted` is per-subscriber, not per-session. A client whose SSE subscriber was evicted can reconnect.
- Anonymous clients (no `X-Qwen-Client-Id`) cannot vote under `designated` or `consensus` policies.
## References
- `packages/acp-bridge/src/bridge.ts` (SessionEntry definition)
- `packages/acp-bridge/src/bridgeTypes.ts` (`HttpAcpBridge`, `BridgeSession`, `BridgeSessionState`)
- `packages/sdk-typescript/src/daemon/types.ts` (`DaemonSession`)
- `packages/sdk-typescript/src/daemon/DaemonSessionClient.ts`
- Wire reference: [`../qwen-serve-protocol.md`](../qwen-serve-protocol.md) (route catalogue).