qwen-code/docs/developers/daemon/08-session-lifecycle.md
jinye 75e8f259c4
docs: Refresh daemon developer docs (#4412)
* docs: Refresh daemon developer docs

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

* docs: Address daemon review feedback

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

* docs: Address daemon review suggestions

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

* codex: address PR review feedback (#4412)

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

* codex: address PR review feedback (#4412)

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

* codex: address PR review feedback (#4412)

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

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-15 02:01:37 +08:00

231 lines
15 KiB
Markdown
Raw 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 生命周期与身份
## 概览
daemon **session** 是一段绑定到一个 ACP `sessionId` 的逻辑对话。bridge 为每个 session 维护一个 `SessionEntry`(见 [`03-acp-bridge.md`](./03-acp-bridge.md)),把 ACP child connection 与 HTTP 侧的簿记捆在一起prompt FIFO、model-change FIFO、event bus、pending permission、attach 的客户端、心跳、restore 状态、终态 tombstone。
daemon **客户端**由 `X-Qwen-Client-Id` 标识 —— 一段不透明、由 daemon 校验的字符串调用方自行在请求里盖。daemon 自己不会替调用方生成 id客户端自取并复用daemon 据此归属投票、审计事件、识别重连。
本文讲清每一次 session 状态迁移create / attach / load / resume / close / die / evict以及 daemon 暴露的每个身份相关 surface。
## 职责
- 创建、attach、restore、回收 session。
- 校验 `X-Qwen-Client-Id`,错的格式直接拒。
- 跟踪 session 上多个 attach 的客户端(`clientIds: Map<string, count>``attachCount`)。
- 给出站事件盖 `originatorClientId`
- 跑心跳,让 dashboard 知道谁还在连着。
- 提供 `displayName`operator 通过 `PATCH /session/:id/metadata` 设置。
- 推送终态帧(`session_died``session_closed``client_evicted``stream_error`)。
## 架构
| 关注点 | 源 | 说明 |
| ------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `SessionEntry` | `packages/acp-bridge/src/bridge.ts` | 每 session 结构体,字段列表见 [`03-acp-bridge.md`](./03-acp-bridge.md) |
| `BridgeSession`(对外) | `packages/acp-bridge/src/bridgeTypes.ts` | `{ sessionId, workspaceCwd, attached, clientId?, createdAt? }` 回给 HTTP handler |
| `BridgeSessionState` | `packages/acp-bridge/src/bridgeTypes.ts` | `LoadSessionResponse \| ResumeSessionResponse`,缓存为 `restoreState` |
| `DaemonSession`SDK | `packages/sdk-typescript/src/daemon/types.ts` | `{ sessionId, workspaceCwd, attached, clientId?, createdAt? }` |
| ClientId 校验 | `packages/acp-bridge/src/bridge.ts``spawnOrAttach` 附近) | 正则 `[A-Za-z0-9._:-]{1,128}`,违法抛 `InvalidClientIdError` |
| Session disconnect-reaper | `packages/cli/src/serve/server.ts` | 用 `attachCount` + `spawnOwnerWantedKill` 跟踪 spawn 拥有者断连 |
### 状态机
```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 与 Spawn
`sessionScope: 'single'`默认bridge 的 `defaultEntry` 被所有连进来的客户端共享。`POST /session` 到来时 `defaultEntry` 已存在 → 不 spawn 新 ACP child直接返回 `attached: true`。bridge 同步 bump `attachCount` 并把调用方的 `X-Qwen-Client-Id` 登记到 `clientIds`
`sessionScope: 'thread'`:每次 `POST /session` 新建一个 session。仍然受 `maxSessions` 约束。
### 身份
`X-Qwen-Client-Id` **可选**但**强烈建议**带。daemon 不会替调用方生成客户端自己挑、在所有请求里复用daemon 才能归属投票、审计事件、识别重连。
校验:
- 字符集 `[A-Za-z0-9._:-]`
- 长度 1128。
- 不合规 → `InvalidClientIdError``400`)。
daemon 在以下条件全部满足时给出站 SSE 事件盖 `originatorClientId`
1. 触发该事件的请求带了 `X-Qwen-Client-Id`,且
2. 该 id 已登记在 session 的 `clientIds` 集合里,且
3. session 当前有 `activePromptOriginatorClientId`(在跑的 prompt 的内联 `sessionUpdate``permission_request` 继承该 originator
匿名调用(不带 `X-Qwen-Client-Id`)在 `first-responder` 下可用;`designated` 会拒它的投票为 `permission_forbidden{ reason: 'designated_mismatch' }``consensus` 同样拒为 `forbidden`(不在发起时 `votersAtIssue` 快照中);`local-only` 是唯一接受匿名 loopback 投票者的策略。
## 流程
### 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` — 重放完整 ACP 历史(`session/load` 通知先于响应返回)。
- `POST /session/:id/resume` — 不重放(`connection.unstable_resumeSession`,由 `unstable_session_resume` 能力暴露)。
两者都:
1. 在 channel 的 `pendingRestoreIds` 集合里登记,让并发 restore 合并(`RestoreInProgressError`)。
2.`restoreState` 缓存到 entry让晚到的 attacher 看到与原始 restore 调用一致的 payload。
### 心跳
`POST /session/:id/heartbeat` 不管带不带 `clientId` 都会更新 `sessionLastSeenAt`。如果请求带了已登记的 `X-Qwen-Client-Id``clientLastSeenAt.set(clientId, Date.now())` 也会 bump。v1 **没有** per-client 剔除revocation 是 F 系列 Wave 5。当前心跳的价值是给 dashboard / 给将来的 PR 24 撤权策略提供观测。
### Metadata
`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或程序化关闭 |
| `session_died` | `channel.exited` 触发(崩溃、被 killOS exit 路径下带 `exitCode?` + `signalCode?` |
| `client_evicted` | EventBus 每订阅者队列溢出(见 [`10-event-bus.md`](./10-event-bus.md)**非** session 级终态,仅关掉当前订阅者 |
| `stream_error` | `SubscriberLimitExceededError` 或其他路由流错误 |
每个终态路径都会 `mediator.forgetSession(sessionId)`,把所有 pending permission 解析为 `{kind:'cancelled', reason:'session_closed'}`
### Disconnect-reaper 守护
spawn 拥有者的 HTTP 响应写不出去时TCP 在握手中途 reset路由会 `killSession({ requireZeroAttaches: true })`。如果其他客户端已经 attach 了(`attachCount > 0`bail 短路、session 继续活着,但 `spawnOwnerWantedKill = true` 留作 tombstone之后某次 `detachClient()``attachCount` 拉回 0 时完成延迟回收。没有这个守护spawn 拥有者快速断连会每隔一次重连就拆掉一个健康 session。
## 状态与生命周期
`SessionEntry` 中和生命周期最密切的字段:
| 字段 | 类型 | 含义 |
| -------------------------------- | --------------------- | --------------------------------------------------------------- |
| `clientIds` | `Map<string, number>` | 已登记 clientId → 引用计数 |
| `attachCount` | `number` | `spawnOrAttach` 对该 entry 返回 `attached: true` 的次数 |
| `activePromptOriginatorClientId` | `string?` | 当前在跑的 prompt 的 originator |
| `restoreState` | `BridgeSessionState?` | load/resume 响应缓存,让晚到 attacher 看到一致 payload |
| `spawnOwnerWantedKill` | `boolean` | 延迟回收 tombstone |
| `sessionLastSeenAt` | `number?` | 任何客户端最近一次心跳epoch ms |
| `clientLastSeenAt` | `Map<string, number>` | per-client 心跳 |
| `pendingPermissionIds` | `Set<string>` | 当前 pending 的 ACP requestId — cancel/close 时解析为 cancelled |
## 依赖
- ACP 层:`connection.newSession``connection.unstable_resumeSession``connection.loadSession`
- [`03-acp-bridge.md`](./03-acp-bridge.md) — 周围的 bridge 架构。
- [`04-permission-mediation.md`](./04-permission-mediation.md) — originator + identity 如何驱动策略。
- [`10-event-bus.md`](./10-event-bus.md) — 终态帧投递。
## 新增 session 端点daemon_mode_b_main
以下端点在基础生命周期之上扩展了 session 的能力:
### Non-blocking Prompt`non_blocking_prompt` 能力 tag
`POST /session/:id/prompt` 现在返回 HTTP **202** `{ promptId, lastEventId }`,不再阻塞直到 prompt 完成。实际结果通过 SSE 上的 `turn_complete` / `turn_error` 事件投递,`promptId` 字段与 202 响应关联。SDK `DaemonSessionClient.prompt()` 在有活跃事件订阅时自动走 non-blocking 路径,透明地通过 SSE 流匹配结果。
### Session Recap`session_recap` 能力 tag
`POST /session/:id/recap` —— 使用 fast model 对 session 生成一句话 "where did I leave off" 摘要。返回 `{ sessionId, recap: string | null }``null` 表示历史太短或模型暂时失败。best-effort。
### Session BTW / Side Question`session_btw` 能力 tag
`POST /session/:id/btw` —— 在不中断主对话流的情况下针对 session 的上下文问一个一次性问题。使用 `runForkedAgent`cache 路径)做单 turn、无工具的 LLM 调用。返回 `{ sessionId, answer: string | null }`。有输入长度限制(`BTW_MAX_INPUT_LENGTH`)、跨 session 泄漏防护和超时处理。
### Shell Command Execution
`POST /session/:id/shell` —— 直接在 daemon 宿主上执行 shell 命令(不经过 LLM。通过 session SSE bus 流式输出(`user_shell_command` / `user_shell_result` 事件),并把命令和结果注入 LLM 的聊天历史。返回 `{ exitCode, output, aborted }`
### Session Detach
`POST /session/:id/detach` —— 显式解除客户端与 session 的绑定(减 `attachCount`),不关闭 session。如果没有其他 attach/subscriber 存活则回收 session。返回 204。
### Batch Session Delete
`POST /sessions/delete` —— 接受 `{ sessionIds: string[] }`(最多 100 个),关闭 bridge session 并删除 transcript 文件。使用 `Promise.allSettled` 保证弹性。返回 `{ removed, notFound, errors }`
### Context Usage`session_context_usage` 能力 tag
`GET /session/:id/context-usage` —— 返回 session 的 context window 结构化使用量。`?detail=true` 返回按 tool/memory/skill 分类的细粒度用量。
### Session Stats`session_stats` 能力 tag
`GET /session/:id/stats` —— 返回 session 使用统计模型指标input/output tokens、cache reads/writes、total cost、per-tool 调用次数和耗时、文件编辑次数。
### Session Tasks`session_tasks` 能力 tag
`GET /session/:id/tasks` —— 返回 session 的后台任务快照agent 任务、shell 任务、monitor 任务及其生命周期状态。
### Compacted Replay
`POST /session/:id/load` 返回的 `BridgeRestoredSession` 现在包含 `compactedReplay?: BridgeEvent[]``liveJournal?: BridgeEvent[]``lastEventId?: number``compactedReplay``TurnBoundaryCompactionEngine` 生成:在 turn 边界折叠连续文本/思考块、工具调用序列折到最终状态、丢弃瞬态信号,产出 O(turns) 而非 O(tokens) 量级的重放日志(通常 25-30x 压缩)。
### ACP Child Preheat
`bridge.preheat()` —— 提前预热 ACP child 进程,让第一个 session 不付冷启动延迟。配合 `channelIdleTimeoutMs`(最后 session 关闭后保持 ACP child 存活的时间)和 skip-relaunch新 session 到达时复用已有的空闲 child使用。
## 配置
- `BridgeOptions.maxSessions`(默认 20
- `BridgeOptions.sessionScope`(默认 `'single'`,可选 `'thread'`)。
- `BridgeOptions.initializeTimeoutMs`(默认 10s
- `BridgeOptions.channelIdleTimeoutMs`(默认 0= 立即回收 ACP child
- 能力 tag`session_create``session_scope_override``session_load``unstable_session_resume``session_list``session_close``session_metadata``session_set_model``client_identity``client_heartbeat``session_recap``session_btw``session_context_usage``session_tasks``session_stats``non_blocking_prompt`
## 注意 & 已知局限
- `connection.unstable_resumeSession` 不稳定ACP 方法形状还可能变。能力 tag 故意带 `unstable_` 前缀,让客户端 feature-detect 而不是硬绑 v1。
- v1 **没有** per-client 剔除,只有 per-session 与 per-subscriber 终态。撤权策略是 F 系列 Wave 5 / PR 24。
- `client_evicted` 是 per-subscriber 不是 per-session订阅者被剔除的客户端可以重连。
- 匿名客户端在 `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`
- Wire 参考:[`../qwen-serve-protocol.md`](../qwen-serve-protocol.md)。