# Session 生命周期与身份标识

> 守护进程 session 是绑定到单个 ACP sessionId 的一次逻辑对话。bridge 为每个 session 维护一个 SessionEntry（参见 03-acp-bridge.md），它将 ACP 子连接与 HTTP 端的记…

- 网址：https://funcoding.ai/agents/qwen-code/developers/daemon/08-session-lifecycle/
- 来源：Qwen Code 官方文档原文（中文），Apache-2.0 许可，同步于 2026-10-11
- 官方原文：https://qwenlm.github.io/qwen-code-docs/zh/developers/daemon/08-session-lifecycle

---
## 概述

守护进程 **session** 是绑定到单个 ACP `sessionId` 的一次逻辑对话。bridge 为每个 session 维护一个 `SessionEntry`（参见 [`03-acp-bridge.md`](https://funcoding.ai/agents/qwen-code/developers/daemon/03-acp-bridge/)），它将 ACP 子连接与 HTTP 端的记录逻辑耦合在一起：包括 prompt FIFO、model-change FIFO、事件总线、待处理权限、已附加客户端、心跳、恢复状态以及终端帧墓碑。

守护进程 **client** 通过 `X-Qwen-Client-Id` 进行标识——这是一个不透明的、由守护进程验证的字符串，HTTP 调用方会将其标记在请求中。bridge 跟踪哪些 client 附加到了哪些 session，并使用 originator client id 来驱动 `designated` 权限策略、审计跟踪和事件归因。

本文档解释了每个 session 生命周期转换（create / attach / load / resume / close / die / evict）以及守护进程暴露的每个身份接口。

## 职责

- 创建、附加、恢复和回收 session。
- 验证 `X-Qwen-Client-Id` 并拒绝格式错误的 id。
- 跟踪每个 session 附加的多个 client（`clientIds: Map<string, count>`、`attachCount`）。
- 在出站事件上标记 `originatorClientId`。
- 运行心跳机制，以便仪表盘了解哪些 client 仍处于连接状态。
- 暴露操作员通过 `PATCH /session/:id/metadata` 设置的 session 元数据（`displayName`）。
- 驱动终端帧的发送（`session_died`、`session_closed`、`client_evicted`、`stream_error`）。

## 架构

| 关注点 | 源码位置 | 说明 |
| --- | --- | --- |
| `SessionEntry` | `packages/acp-bridge/src/bridge.ts` | 每个 session 的结构体；完整字段列表请参见 [`03-acp-bridge.md`](https://funcoding.ai/agents/qwen-code/developers/daemon/03-acp-bridge/)。 |
| `BridgeSession` (public) | `packages/acp-bridge/src/bridgeTypes.ts` | 返回给 HTTP handler 的 `{ sessionId, workspaceCwd, attached, clientId?, createdAt? }`。 |
| `BridgeSessionState` | `packages/acp-bridge/src/bridgeTypes.ts` | 作为 `restoreState` 缓存在 entry 上的 `LoadSessionResponse \| ResumeSessionResponse`。 |
| `DaemonSession` (SDK) | `packages/sdk-typescript/src/daemon/types.ts` | `{ sessionId, workspaceCwd, attached, clientId?, createdAt? }`。 |
| Client-id 验证 | `packages/acp-bridge/src/bridge.ts`（`spawnOrAttach` 附近） | 正则模式 `[A-Za-z0-9._:-]{1,128}`；格式错误时抛出 `InvalidClientIdError`。 |
| Session 断连回收器 | `packages/cli/src/serve/server.ts` | 使用 `attachCount` + `spawnOwnerWantedKill` 跟踪 spawn-owner 的断连。 |

### 状态机

```mermaid
stateDiagram-v2
    [*] --> SpawnInProgress: POST /session
    SpawnInProgress --> Live: newSession 成功
    SpawnInProgress --> [*]: 初始化失败 / spawn 错误
    Live --> Live: attach (sessionScope=single, 增加 attachCount)
    Live --> Live: detach (减少 attachCount)
    Live --> RestoreInProgress: POST /session/:id/load 或 /resume
    RestoreInProgress --> Live: restoreState 缓存在 entry 上
    RestoreInProgress --> Live: RestoreInProgressError (合并等待者)
    Live --> Closed: DELETE /session/:id (最后一个 client) 或已确认的工作区运行时停止
    Live --> Died: ACP 子进程退出 / 已确认工作区停止之外的 channel.exited，或守护进程关闭
    Closed --> [*]: session_closed 终端帧
    Died --> [*]: session_died 终端帧
```

### Attach vs spawn

在 `sessionScope: 'single'`（默认）下，bridge 的 `defaultEntry` 由每个连接的 client 共享。当 `defaultEntry` 已存在时，到达的 `POST /session` 请求会返回 `attached: true`，而不会 spawn 新的 ACP 子进程。bridge 会同步增加 `attachCount`，并将调用方的 `X-Qwen-Client-Id` 注册到 `clientIds` 中。

在 `sessionScope: 'thread'` 下，每个 thread 可以创建一个独立的 session。调用方仍需遵守 `maxSessions` 限制。

### 身份标识

`X-Qwen-Client-Id` 是**可选的**，但**强烈建议使用**。守护进程不会代为生成——client 需要自己选择并在请求中复用，以便守护进程进行投票归因、事件审计和重连检测。

每个独立的控制器应使用不同的、稳定的 ID。Web Shell 为兼容性保留历史使用的 `webui_` 前缀。宿主和嵌入式 Web Shell 仅在有意识地作为单个逻辑控制器行动时才应共享 ID；一旦共享，daemon 日志将无法区分是哪个发起了请求。

验证规则：

- 字符集：`[A-Za-z0-9._:-]`。
- 长度：1–128。
- 超出此字符集：抛出 `InvalidClientIdError` (`400`)。

守护进程会在出站 SSE 事件上标记 `originatorClientId`，条件如下：

1. 触发该事件的请求携带了 `X-Qwen-Client-Id`，并且
2. 该 id 当前已注册在 session 的 `clientIds` 集合中，并且
3. session 设置了 `activePromptOriginatorClientId`（内联的 `sessionUpdate` 和 `permission_request` 会继承活跃 prompt 的 originator）。

匿名调用方（无 `X-Qwen-Client-Id`）在 `first-responder` 策略下可以正常工作；`designated` 会以 `permission_forbidden{ reason: 'designated_mismatch' }` 拒绝其投票；`consensus` 也会以相同的 `forbidden` 原因拒绝，因为投票者不在 issue-time 的 `votersAtIssue` 快照中；`local-only` 是唯一接受匿名 loopback 投票者的策略。

## 工作流

### 创建或附加

```mermaid
sequenceDiagram
    autonumber
    participant C as Client
    participant R as POST /session
    participant B as Bridge.spawnOrAttach
    participant CH as ACP 子进程

    C->>R: POST /session<br/>X-Qwen-Client-Id: alice<br/>{cwd, sessionScope?}
    R->>R: 验证 clientId 模式
    R->>B: spawnOrAttach({cwd, sessionScope, clientId})
    alt single scope + defaultEntry 存在
        B->>B: 增加 attachCount；注册 clientId
        B-->>R: {sessionId, attached: true, restoreState?}
    else 冷启动
        B->>CH: spawn + ACP initialize + newSession
        CH-->>B: sessionId
        B->>B: 构建 SessionEntry；注册到 byId
        B-->>R: {sessionId, attached: false}
    end
    R-->>C: 200 { sessionId, attached, ... }
```

### Load / resume

`POST /session/:id/load` — 恢复持久化的会话并返回当前有界重放快照窗口（`session/load` 通知或响应模式重放在响应返回前播种）。
`POST /session/:id/resume` — 恢复但不重放（`connection.unstable_resumeSession`，在稳定的 `session_resume` 守护进程能力下暴露；`unstable_session_resume` 仍作为已弃用的别名保留）。

两者均：

1. 在 channel 上使用每个 session 的 `pendingRestoreIds` 集合，以便合并并发的 restore 调用（`RestoreInProgressError`）。
2. 在 entry 上缓存 `restoreState`，以便后附加的 client 获取与原始恢复者相同的有效载荷。

对于持久化的 Part 4A worktree 会话，恢复是此生命周期的完整性门控扩展。sidecar 显式标识请求的工作区根，checkout 必须规范地包含在相应的 `.qwen/worktrees/` 目录下，并且其标记必须是包含确切恢复会话 ID 的单链接常规文件。守护进程仅在这些检查通过后才重新定位空闲的已恢复子进程；活跃子进程仅在其报告的 cwd 已等于 worktree 时才被接受，而其报告的 cwd 缺失或在其他位置的活跃子进程会 fail closed 而不是在其 prompt 下被重新定位——除了无法延迟其恢复 prompt 的冷恢复（`suppressWorktreeContextRestore` 关闭，因此 bridge 触发了重新挂起的问题而不是停放它）：该形态保留 4B 之前的结果，返回不带 `worktreeState` 的规范 worktree 元数据，未重新定位，会话继续存活。重新定位和接受的响应返回带有 `worktreeState: "persisted-v1"` 的规范 worktree 元数据。携带 `supersededBy` 的 sidecar 永远不会恢复：路由返回 `409 worktree_session_superseded` 及替换会话 id，该分类仅根据该链接在任何标记读取之前决定，因此调用者仅在该 id 的加载成功后才重定向并修复其记录——预提交中断的传输命名的替换不是标记所有者，其本身无法恢复，并被重试的 reset 回收；`supersedes` 链接与旧 sidecar 一致而标记未移动（或缺失）的已恢复替换返回 `409 worktree_reset_interrupted`，其修复方式是对被取代会话重试 reset；缺少该一致链接对的缺失标记返回 `409 worktree_marker_missing`，其修复方式是重置任务而不是重试恢复，因为没有恢复路径会重新创建标记。中断分类优先检查。无效的 Part 4A 状态会分离现有附加或以 `requireZeroAttaches` 终止冷恢复；缺少 sidecar 同样无法提供证明。当有效恢复源为 Channel 所拥有时，路由会抑制 ACP 代理对 Part 4A 或无法分类的 sidecar 状态的尽力清理，因此验证失败会保留不确定的 checkout 证据。持久化源元数据优先；当其缺失时，load/resume 请求提供有效源。结构上有效的旧版 sidecar（无 `workspaceCwd`）保留现有的尽力代理恢复：它必须标识请求的工作区根或其 Git 仓库顶层，进行无标记证明的包含检查，可能被代理清理，并可能返回不带 `worktreeState` 的 `worktree`。除该显式旧版兼容情况外，仅有效恢复源非 Channel 所拥有的会话保留路由验证前的现有尽力清理。

Worktree 所有权转移（`POST /session/:id/worktree-reset`，由 `session_worktree_reset_v1` 宣传）为此生命周期扩展了 Channel 任务重置：守护进程在根工作区中生成新的线程作用域替换，将其重新定位到已验证的 checkout 中，链接 sidecar 对（先在旧会话上设置 `supersededBy`，然后在替换上设置 `supersedes`），在每个 checkout 路由锁和准入屏障下将标记翻转到替换，该屏障拦截 prompt 准入以及在 checkout 中开始工作或移动会话 cwd 的另外七个写入者（rewind、cwd 变更、branch、fork、shell、goal control、workflow-task action），而 release 和 stop 路径设计上不设屏障，然后断开被取代会话的客户端注册和内存中的 worktree 关联。断开报告会报告被取代会话是否确实已消失：子进程仍持有后台工作的存活者保持屏障激活，被记录，并向调用者报告为 `supersededSessionLive: true`，而不是被成功响应掩盖。在传输过程中在被取代会话上被准入的屏障写入者被拒绝并返回 `409 worktree_reset_active`；完整的失败分类（包括重试回滚哪些崩溃窗口以及哪些 fail closed 交由操作员修复）记录在 `qwen-serve-protocol.md` 的该路由文档中。

### 心跳

`POST /session/:id/heartbeat` 会更新 `sessionLastSeenAt`，无论是否携带 `clientId`。如果请求携带了已注册的 `X-Qwen-Client-Id`，还会执行 `clientLastSeenAt.set(clientId, Date.now())` 进行更新。v1 中**未**实现按 client 驱逐；撤销功能计划在 F-series Wave 5 中推出。目前，心跳机制为仪表盘以及 PR 24 中即将推出的撤销策略提供可观测性。

### 元数据

`PATCH /session/:id/metadata` 接受 `{displayName?}`。验证规则：

- 最大长度：`MAX_DISPLAY_NAME_LENGTH = 256`。
- 不得包含控制字符（`hasControlCharacter` 会拒绝码点 ≤ 0x1f 或 == 0x7f 的字符）。
- 违反时抛出 `InvalidSessionMetadataError` (`400`)。

成功更新后，会向每个订阅者广播 `session_metadata_updated` 事件。

### 终止

| 终端帧 | 触发条件 |
| --- | --- |
| `session_closed` | `DELETE /session/:id` (client_close)、编程式关闭，或已确认的工作区运行时停止（`cause: workspace_runtime_stop`）。与停止关联的致命退出还会携带 `persistenceUnconfirmed: true`。 |
| `session_died` | 已确认工作区停止之外的 `channel.exited`（崩溃或守护进程发起的 kill，例如 unknown-close-outcome 恢复），或守护进程关闭（`reason: daemon_shutdown`，不经过 channel 退出即发布）。当使用 OS 退出路径时，携带 `exitCode?` + `signalCode?`。 |
| `client_evicted` | EventBus 上的单订阅者队列溢出（参见 [`10-event-bus.md`](https://funcoding.ai/agents/qwen-code/developers/daemon/10-event-bus/)）。这不是 session 级别的终止——仅关闭该订阅者。 |
| `stream_error` | `SubscriberLimitExceededError` 或其他路由级别的 stream 失败。 |

在每个终止路径中，通过 `mediator.forgetSession(sessionId)` 将 pending permissions 解析为 `{kind:'cancelled', reason:'session_closed'}`。

### 断连回收器守卫

当 spawn-owning client 的 HTTP 响应无法写入（握手期间 TCP 重置）时，路由会调用 `killSession({ requireZeroAttaches: true })`。如果已有其他 client 附加（`attachCount > 0`），该守卫会短路，session 继续存活。设置 `spawnOwnerWantedKill = true` 会记住该意图，以便后续将 `attachCount` 降回 0 的 `detachClient()` 完成延迟回收。如果没有此机制，频繁快速断连的 spawn owner 会在每次重连时摧毁一个健康的 session。

## 状态与生命周期

对生命周期至关重要的 `SessionEntry` 字段：

| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `clientIds` | `Map<string, number>` | 已注册的 client id → 注册引用计数。 |
| `attachCount` | `number` | `spawnOrAttach` 为该 entry 返回 `attached: true` 的次数。 |
| `activePromptOriginatorClientId` | `string?` | 当前正在运行的 prompt 的 originator。 |
| `restoreState` | `BridgeSessionState?` | 缓存的 load/resume 响应，确保后附加的 client 看到一致的有效载荷。 |
| `spawnOwnerWantedKill` | `boolean` | 延迟回收墓碑（参见上文的断连回收器）。 |
| `sessionLastSeenAt` | `number?` | 所有 client 中最近的心跳时间（epoch 毫秒）。 |
| `clientLastSeenAt` | `Map<string, number>` | 每个 client 的心跳时间。 |
| `pendingPermissionIds` | `Set<string>` | 当前 pending 的 ACP requestIds —— 在 cancel/close 时用于将其解析为 cancelled。 |

## 依赖

- ACP 层：`connection.newSession`、`connection.unstable_resumeSession`、`connection.loadSession`。
- [`03-acp-bridge.md`](https://funcoding.ai/agents/qwen-code/developers/daemon/03-acp-bridge/) 了解周围的 bridge 架构。
- [`04-permission-mediation.md`](https://funcoding.ai/agents/qwen-code/developers/daemon/04-permission-mediation/) 了解 originator + identity 如何驱动策略决策。
- [`10-event-bus.md`](https://funcoding.ai/agents/qwen-code/developers/daemon/10-event-bus/) 了解终端帧的传递。

## 额外的 session 端点

这些端点扩展了基础生命周期接口：

### 非阻塞 Prompt（`non_blocking_prompt` 能力标签）

`POST /session/:id/prompt` 现在返回 HTTP **202** 及 `{ promptId, lastEventId }`，而不是阻塞直到 prompt 完成。实际结果会通过 SSE 以 `turn_complete` / `turn_error` 的形式到达，并且 `promptId` 字段将这些事件与 202 响应关联起来。当 `DaemonSessionClient.prompt()` 拥有活跃的事件订阅时，会自动使用非阻塞路径，并透明地匹配来自 SSE 流的结果。

### Session 总结（`session_recap` 能力标签）

`POST /session/:id/recap` 向快速模型请求一行“我上次进行到哪里了”的总结。它返回 `{ sessionId, recap: string | null }`；`null` 表示历史记录太短或模型暂时失败。此端点是尽力而为（best-effort）的。

### Session BTW / 顺带提问（`session_btw` 能力标签）

`POST /session/:id/btw` 针对会话上下文提出一次性问题，且不会中断主对话流。它在缓存路径上使用 `runForkedAgent` 进行单轮、无工具的 LLM 调用，并返回 `{ sessionId, answer: string | null }`。该实现强制执行 `BTW_MAX_INPUT_LENGTH` 限制、跨会话泄漏防护以及超时处理。

### Shell 命令执行

`POST /session/:id/shell` 直接在 daemon host 上执行 shell 命令，不经过 LLM 路由。它通过 `user_shell_command` / `user_shell_result` 事件在会话 SSE 总线上流式输出结果，并将命令及其结果注入 LLM 对话历史。响应格式为 `{ exitCode, output, aborted }`。对于活跃的次级工作区会话，单一 REST 路由会解析会话所有者并在该 runtime 的 bridge 上执行，因此命令在所属工作区的 cwd 中启动。该路由不提供路径沙箱。具有工作区资格的 ACP 客户端可以继续在所属工作区连接上使用 `_qwen/session/shell`。

### 会话回退

`GET /session/:id/rewind/snapshots` 和 `POST /session/:id/rewind` 解析所属的活跃工作区 runtime。持久化的会话必须先 load 或 resume 才能回退。回退会截断对话历史并可选地恢复由 `edit` 和 `write_file` 跟踪的文件；它不会撤消 shell 命令、Git、脚本或手动更改。文件恢复是尽力而为的，因此响应可能在对话历史已经移动后报告 `rewound: false` 和 `filesFailed[]`。SDK 回退调用始终使用所有者感知的 REST，即使客户端在其他情况下使用 ACP 传输也是如此，因为变更必须保留严格的 REST 身份验证。

### 会话分离

`POST /session/:id/detach` 通过递减 `attachCount` 显式将客户端从会话中分离；它本身不会关闭会话。如果没有其他附加（attach）或订阅者存在，该会话将被回收。该端点返回 204。

### 批量删除会话

`POST /sessions/delete` 接受 `{ sessionIds: string[] }`（最多 100 个 id），关闭 bridge 会话，并删除活跃或已归档的 transcript 文件。如果同一个 id 同时存在活跃和已归档的 JSONL 文件，硬删除会移除两者，以便运维人员清除冲突。它会清理活跃和已归档的 worktree sidecars，但保留 file-history 快照、子代理 transcript 和运行时 sidecars。它使用 `Promise.allSettled` 来保证弹性，并返回 `{ removed, notFound, errors }`。

### 会话归档

`POST /sessions/archive` 将非活跃会话的 JSONL 文件从 `chats/` 移动到 `chats/archive/`。如果目标会话处于活跃状态，daemon 会先进入每个会话的归档门控（archive gate），并执行严格关闭，要求 ACP 子进程 flush `ChatRecordingService`；如果关闭或 flush 失败，归档操作会将 JSONL 保留在原位。

`POST /sessions/unarchive` 将已归档的 JSONL 文件移回 `chats/`。这仅仅是存储状态的转换；客户端之后必须调用 `session/load` 或 `session/resume`。对于已归档的会话，load/resume 会返回 `409 session_archived`，而在归档转换期间发生竞争的变更操作会返回 `409 session_archiving`。

空的、损坏的和孤立的常规转录文件即使无法作为对话加载，仍然符合这些生命周期操作的条件。所有权安全检查可以有意地 fail closed 并要求操作员干预。在 writer 密封其认证的交接证明后，如果文件被更改，则会以 `SessionTranscriptChangedError` 失败，直到操作员解决密封锁和已更改的字节。超过有界所有权读取窗口的 JSON 格式首条物理记录会以 `SessionTranscriptIdentityUnavailableError` 失败，直到该记录被修复或缩减；带有非对象前缀的超大损坏记录仍然符合条件。可解析的恢复记录必须包含字符串类型的 `sessionId` 和 `cwd` 所有权字段，混合的本地/外部归档状态也会 fail closed。当宣传了 `session_storage_conflict_repair` 时，archive 和 unarchive 接受 `resolveConflicts: true`：archive 保留已归档的副本，而 unarchive 保留活跃的副本。不使用该选项时，活跃/归档冲突不会移动、删除或覆盖任何持久化副本，并会在批处理的 `errors` 数组中返回。Archive 仍然在分类冲突之前严格关闭活跃会话，这可能会将排队的记录 flush 到活跃转录中。具有工作区资格的生命周期路由现在使用 HTTP `200` 批处理信封，而不是早期的 HTTP `409 session_conflict` 响应。

### 上下文使用情况（`session_context_usage` 能力标签）

`GET /session/:id/context-usage` 返回结构化的上下文窗口使用情况。`?detail=true` 包含按 tool、memory 和 skill 分组的更细粒度的使用情况。

### 会话统计（`session_stats` 能力标签）

`GET /session/:id/stats` 返回使用统计信息：模型指标（输入/输出 tokens、缓存读/写、总成本）、每个 tool 的调用次数和延迟、文件编辑次数，以及当前活跃会话中每个 skill 的调用次数。`skills` 块仅反映该会话内的 skill body 加载和 skill 斜杠命令；它不是跨会话的活动聚合。

### 会话任务（`session_tasks` 能力标签）

`GET /session/:id/tasks` 返回 agent 任务、shell 任务、monitor 任务及其生命周期状态的后台任务快照。由另一个子代理生成的 agent 条目包含可选的 lineage 字段（`parentAgentId`、`parentName`、`depth`），以便客户端将嵌套的子代理渲染为树状结构；请参阅 `qwen-serve-protocol.md` 中的 payload 示例。

`session_monitor_tool_correlation` 能力额外保证 monitor 条目携带 `toolUseId`，允许客户端将转录中的工具调用与其任务详情进行关联。

### 会话 LSP 状态（`session_lsp` 能力标签）

`GET /session/:id/lsp` 为 daemon 客户端返回经过清理的每个会话的 LSP 状态：启用状态、聚合服务器数量、不可用/初始化状态，以及每个服务器的 `name`、`status`、`languages`、`transport`、`command` 和 `error`。禁用或不可用的 LSP 会表示为 HTTP 200 状态数据，而不是传输错误。

### 压缩重放

`POST /session/:id/load` 现在返回一个 `BridgeRestoredSession`，其中可以包含 `compactedReplay?: BridgeEvent[]`、`liveJournal?: BridgeEvent[]` 和 `lastEventId?: number`。这些字段是守护进程为活跃会话提供的有界内存重放窗口，而非完整的转录 API。默认窗口上限为每个活跃会话 4 MiB（`--compacted-replay-max-bytes`），启动时拒绝无效上限；硬上限为 256 MiB。`compactedReplay` 由 `TurnBoundaryCompactionEngine` 生成：在 turn 边界处，它会折叠连续的 text / thought 块，将 tool-call 序列折叠为其最终状态，丢弃瞬态信号，并生成 O(turns) 级别的重放日志，而不是 O(tokens) 级别的日志（通常可减少 25-30 倍）。当较旧的保留重放从该字节窗口中被丢弃时，`compactedReplay[0]` 是一个合成的无 id `history_truncated` 标记，包含 `{reason: 'replay_window_exceeded', truncatedEvents, retainedEvents, maxBytes, truncatedTurns?, fullTranscriptAvailable: boolean}`。`fullTranscriptAvailable` 是一个能力标志：`true` 表示客户端可以使用 `GET /session/:id/transcript` 翻页获取完整的持久化转录，而 `false` 表示仅有界重放可用。客户端应将其作为状态渲染并正常应用保留的重放；它不得触发重同步循环。

### ACP 子进程预热

`bridge.preheat()` 仍可供显式嵌入方使用，但 `qwen serve` 也会在启动后尝试预热 trusted primary child 以保持一致性。预热失败不会导致致命错误，下一个 runtime 命令或 Session 会重试；trusted secondary 在首次使用时启动。Workspace Runtime 在工作活跃期间拥有该子进程。在所有 Session 和管理租约 drain 之后，省略或为零的 `channelIdleTimeoutMs` 会立即回收该子进程；单纯的预热本身会为首次使用保留，且不会触发回收器。正值的配置延迟或活跃的 keepalive 会使子进程在更长的剩余窗口内保持可复用。公开的 Workspace Runtime `ensure` 命令会添加一个可续期的十分钟工作区租约；每次成功的调用都会重置该窗口，包括 channel 已经活跃的情况。

## 配置

- `BridgeOptions.maxSessions`（默认 32）— 上限。
- `BridgeOptions.sessionScope`（默认 `'single'`；可选 `'thread'`）。
- `BridgeOptions.initializeTimeoutMs`（默认 10s）— ACP 子进程启动截止时间（Channel factory + `initialize` 握手）及默认请求超时。
- `BridgeOptions.sessionRestoreTimeoutMs`（默认 60s）— ACP `loadSession` / `unstable_resumeSession` 截止时间。默认 60 秒；显式配置的 initialize 超时可以提高此值，但不能降低。
- `BridgeOptions.channelIdleTimeoutMs`（未设置或 `0` 时在 runtime 工作 drain 后回收，但单纯预热会为首次使用保留；正值或活跃的 keepalive 会延迟回收，且取较长的延迟）。
- Capability tags：`session_create`、`session_id_override`、`session_scope_override`、`session_load`、`session_resume`、`unstable_session_resume`（已弃用的别名）、`session_list`、`session_info`、`session_close`、`session_metadata`、`session_set_model`、`client_identity`、`client_heartbeat`、`session_recap`、`session_generation`、`session_btw`、`session_context_usage`、`session_tasks`、`session_monitor_tool_correlation`、`session_stats`、`session_lsp`、`session_resources`、`session_status`、`non_blocking_prompt`。

### 无状态 generation（`session_generation` 能力标签）

`POST /session/:id/generate` 接受 `{ "prompt": string }` 并返回一个请求作用域的 SSE 流，包含 `started`、可选的 `thinking`、`delta`、`done` 或 `error` 事件。该请求不读取对话历史、不记录轮次，也不暴露任何工具。ACP 子进程在可用时使用已配置的有效快速模型，否则使用会话的主模型。

## 注意事项与已知限制

- `connection.unstable_resumeSession` 在 ACP 层可能仍然不稳定，但 daemon 通过 `session_resume` 宣传已提交的 v1 路由契约。`unstable_session_resume` 仅作为已弃用的兼容性别名保留。
- v1 **没有 per-client 驱逐**；只有 per-session 和 per-subscriber 终止。撤销策略为 F-series Wave 5 / PR 24。
- `client_evicted` 是 per-subscriber 的，而不是 per-session 的。SSE 订阅者被驱逐的客户端可以重新连接。
- 匿名客户端（没有 `X-Qwen-Client-Id`）无法在 `designated` 或 `consensus` 策略下进行投票。

## 参考资料

- `packages/acp-bridge/src/bridge.ts`（SessionEntry 定义）
- `packages/acp-bridge/src/bridgeTypes.ts`（`HttpAcpBridge`、`BridgeSession`、`BridgeSessionState`）
- `packages/sdk-typescript/src/daemon/types.ts`（`DaemonSession`）
- `packages/sdk-typescript/src/daemon/DaemonSessionClient.ts`
- 协议参考：[`../qwen-serve-protocol.md`](https://funcoding.ai/agents/qwen-code/developers/qwen-serve-protocol/)（路由目录）。
