Qwen Code Channel Named Sessions(Part 2):为 Daemon 托管 Channel 构建多任务命名会话目录
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
本文基于 Qwen Code 仓库的设计文档 docs/design/channel-named-sessions-part2.md,系统讲解 Channel 命名会话(Named Sessions)系列设计的第一阶段落地:如何在 daemon 托管的 Channel 中,为一个用户在同一个聊天里维护最多 8 个有名字、可持久化的任务会话。读完本文,你将掌握multiSession: true的启用前提、命名任务目录(registry)的数据结构与持久化细节、/sessions与/session系列命令的完整语义、busy 状态判定与失败顺序(failure ordering),以及 Part 2 与后续 Part 3/Part 4 的功能边界划分。文中同时给出仓库源码与配置文件的真实相对路径,便于你继续深入阅读实现。
Part 2 的目标与功能边界
Part 2 是"Channel 命名会话"系列的第一个落地阶段,它的目标可以概括为一句话:为 daemon 托管的 Channel 增加第一个安全、可选的命名会话目录(named-session catalog)。
在sessionScope: "user"且multiSession: true的前提下,同一个发送者可以在同一个聊天中:
- 保留最多8 个命名任务;
- 选择一个空闲任务作为当前选中任务;
- 关闭一个任务而不删除它的 transcript;
- 之后重新打开(reopen)同一个daemon session,恢复之前的精确会话。
目录(catalog)的隔离维度是通道实例(channel instance)× 聊天(chat)× 发送者(sender),即不同 channel、不同聊天、不同用户之间的命名任务互不可见。
Part 2 刻意保持"同一时刻只选中一个任务"的策略:当当前任务仍在运行或存在挂起的权限请求(pending permission request)时,切换任务、创建并选中新任务、或关闭任务都会被拒绝。这一"运行中禁止切换"的守卫(busy guard)将在 Part 3 完成命名投递、取消和权限关联之后移除;Part 4 才会加入 worktree 创建能力。
Reviewed boundaries:运行期与兼容性约束
Part 2 对功能可用范围做了严格的 fail-closed 界定:
| 场景 | Part 2 行为 |
|---|---|
daemon 托管的 Channel worker +sessionScope: "user" | 唯一支持 named-session 的模式 |
独立运行的qwen channel start | 不支持 |
| 配置了 webhook 的 Channel | 不支持 |
channel 或 group 的groupHistoryLimit非零 | 不支持 |
| Channel loop | 不支持(含循环创建命令与 loop MCP 工具) |
multiSession缺省或为 false | 行为与旧版本完全一致 |
若某个 Channel 已经拥有一个启用的持久化 loop,daemon worker 会拒绝启动该多会话 Channel,且不会向该 Channel 的会话暴露 loop 创建命令或 loop MCP 工具。
在架构上,Part 2 采用"双指针"设计:
- legacy 路由文件(route file)保持原样,继续持有普通分发(normal dispatch)所使用的选中会话;
- 新增的注册表(registry)存放在 daemon 的 workspace 与 channel 作用域状态目录之下;
- 注册表是目录的权威(catalog authority),路由只是兼容性指针(compatibility pointer)。
另外,Channel memory 仍然保持 chat 作用域:它既不会被复制到每个任务,也不会被当作所有权索引。
Ownership 与命名规则
一个 owner(所有者)是精确三元组(channelName, chatId, senderId):
- Thread ID 不参与所有权,因为
sessionScope: "user"本身就已经在同一个聊天内的多个 thread 之间路由; - 每个操作都从经过认证的入站信封(authenticated inbound envelope)出发,绝不接受命令文本中携带的 owner 或 daemon session ID;
- 兼容性路由不是所有权权威:legacy 收养(adoption)会对照精确 owner 检查路由中存储的目标;本地选中会话命令则通过目录解析,而不是基于分隔符的路由键。
命名规则在源码 packages/channels/base/src/named-session-manager.ts 中有直接对应:
const REGISTRY_VERSION = 1; const MAX_OPEN_TASKS = 8; const TASK_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;即任务名匹配[A-Za-z0-9][A-Za-z0-9_-]{0,31}(1~32 位 ASCII 字母、数字、下划线或连字符,且必须以字母或数字开头),并在一个 owner 的打开与关闭任务全集内大小写不敏感地唯一。命令只按名字解析,绝不接受 session ID。每个 owner 最多同时有 8 个打开任务;关闭任务不占用打开任务配额。
Registry:版本 1 目录的数据结构与持久化
数据结构
版本 1 registry 是一个 owner 记录数组,每个 owner 记录包含:
- 精确身份
(channelName, chatId, senderId); - 可选的当前选中任务名(
activeTaskName); - 若干任务记录,每条任务记录包含:
| 字段 | 含义 |
|---|---|
name/sessionId | 显示名与精确 daemon session ID |
cwd/isolation | 工作目录与隔离模式(shared/worktree) |
status | open或closed |
createdAt/updatedAt/lastSelectedAt | 创建、更新与最近选中时间戳 |
target | 原始的 Channel 投递目标 |
这一结构在 packages/channels/base/src/named-session-manager.ts 中体现为StoredTask、StoredOwner、StoredRegistry三个接口,例如:
interface StoredRegistry { version: 1; workspaceCwd: string; owners: StoredOwner[]; }为什么用数组而不是对象键?设计文档明确:使用数组而非攻击者可控制的对象键,可以避免原型键(prototype-key)与分隔符冲突(delimiter-collision)两类安全隐患。这一点在 registry 校验逻辑isRegistry/isOwner/isTask(named-session-manager.ts)中也有体现:owner 键与 session ID 都通过Set去重校验,重复即判定 registry 无效。
原子持久化
owner 的变更在内存中按 owner 串行化(per-owner serialization),任务时间戳单调递增——即使在同一时钟 tick 内发生多次选中,时间戳也不会相等。持久化采用"唯一临时文件 + 原子 rename":
private writeRegistry(registry: StoredRegistry): void { const dir = dirname(this.filePath); const tempPath = `${this.filePath}.${process.pid}-${randomUUID()}.tmp`; // mkdirSync(dir, { recursive: true, mode: 0o700 }) // writeFileSync(tempPath, JSON.stringify(registry, null, 2), { encoding: 'utf8', mode: 0o600 }) // renameSync(tempPath, this.filePath) }(见 named-session-manager.ts)。commitOwner的顺序是先构造并校验下一版 registry、构建对应的派生索引,再原子写入,最后才把内存中的registry与taskBySessionId索引一起发布(named-session-manager.ts),写入失败时两者都不会发布。
对异常文件的处理策略:
- 畸形或不支持的 registry:导致该已启用 Channel无法启动,而不是静默替换所有权目录;
- 结构合法但来自旧 channel 工作目录的目录:归档为 stale 并重置;
- 指向当前工作目录之外的 legacy 路由:被遗忘(forgotten)而不是被收养。
路由原语(Router Primitives)与 busy 状态
为什么不用普通的 resolve()
命名会话操作不会走 router 普通的懒加载resolve()路径。原因在于该路径在"恢复的 legacy 路由无法加载"时会主动创建替代会话,这与"精确任务选择"的语义不兼容。
router 因此暴露了四个窄化的 managed-session 原语:
- create:创建一个活的 daemon session,但不改变当前选中路由;
- load:精确加载并校验一个会话,不创建替代;
- bind:把该校验过的会话绑定为选中的兼容路由,同时为其他活着的命名会话保留投递元数据;
- detach/forget:分离并遗忘一个已关闭会话,不删除 daemon 数据。
当 daemon bridge 崩溃并被替换时,所有 managed client 会被标记为 dormant;选中兼容路由由普通恢复流程恢复一次;非活跃任务只在之后被选中时才按精确 ID 重新挂接(reattach)。
如果精确加载失败,registry 选中状态与兼容路由都不会改变;如果创建 daemon session 之后 registry 持久化失败,新 client 会被分离,任务不会暴露给聊天。
Busy 状态判定
ChannelBase拥有 Part 2 的 busy 判定(源码见 packages/channels/base/src/ChannelBase.ts,NamedSessionManager构造时通过isBusy回调注入):
一个 session 在以下任一情况下被视为 busy:
- 仍有一个活跃的 Channel turn(active Channel turn);
- 存在挂起的权限请求(pending permission request);
- daemon bridge 报告存在活跃 prompt(active prompt)。
取消期间以 Channel turn 为权威:因为 daemon bridge 可能在 turn 的finally块完成之前就清除了自己的 active 标志。
busy 守卫作用于/session new、/session use、/session close这三个会离开当前选中任务的操作。而既有的/clear、/new、/reset仍然是显式的破坏性重置:它们只在同一个任务名下替换该选中任务的 daemon session,并对被退役的 session 走既有的有界取消与清理路径。
Part 2 命令全集
Part 2 引入的命令语义如下(与 ChannelBase.ts 中的handleNamedSessionsCommand/handleNamedSessionCommand实现一一对应):
| 命令 | 语义 |
|---|---|
/sessions | 列出当前聊天中该发送者拥有的打开任务 |
/sessions all | 额外列出关闭任务 |
/session current | 报告当前选中任务,不暴露 session ID |
/session new <name> | 创建共享工作目录(shared)任务并选中它 |
/session use <name> | 选中一个打开任务,或通过加载其精确持久化会话重新打开一个关闭任务 |
/session close <name> | 拒绝 busy 任务;分离活 client;保留目录记录与 transcript。关闭选中任务时,自动选中最近选中过的剩余打开任务;若没有剩余,普通消息会引导用户创建或重新打开任务 |
/session new <name> --worktree | 识别但拒绝,返回一条指向 Part 4 的可操作提示 |
/session cancel [<name>] | 识别但推迟到 Part 3。Telegram 既有/cancel继续取消选中任务;其他适配器要求任务结束后才能切换 |
命令实际输出的样式(来自实现):
Open tasks: * feature-a (open, shared) - feature-b (closed, shared)Current task: feature-a (shared)Created and selected task "feature-a" (shared workspace).Selected task "feature-b" (shared workspace).Closed task "feature-a". Selected "feature-b".第一个目录操作或第一条普通消息会把既有 legacy 路由收养为default——不加载替代、不改变其 transcript;若没有路由存在,第一条普通消息会在分发前创建并持久化default。
失败顺序(Failure Ordering):Create / Reset / Select / Close
Part 2 为每个操作定义了严格的失败顺序,保证任何一步失败都不会破坏既有状态:
- Create / Reset:先创建精确 daemon session → 再持久化所有权 → 最后绑定路由。持久化失败会分离新创建的 client 并保留旧选中。
- Select / Reopen:先精确加载目标 → 再持久化新选中 → 最后绑定路由。加载失败保持旧选中不变;持久化失败会分离仅为本次失败操作而加载的目标。
- Close:先校验 idle 状态并加载任何替代选中,然后才修改目录 → 提交 closed 状态 → 变更或移除选中路由 → 分离被关闭任务。分离失败会被暴露,任务在可能的情况下恢复到 open/selected 状态;绝不删除 transcript 或 worktree。
在实现中,close的失败回滚会在 detach 失败时用commitOwner恢复之前的 owner(见 named-session-manager.ts)。
验证与测试覆盖
设计文档列出的验证点覆盖了:
- 配置门禁(configuration gates);
- owner 隔离;
- 大小写不敏感的名字唯一性;
- 8 个打开任务上限;
- legacy 收养;
- 原子持久化;
- create 回滚;
- 精确加载失败;
- idle 守卫(含取消收尾 cancel wind-down);
- close/reopen;
- 选中任务 reset;
- 重启恢复;
- 命令输出不含 session ID;
- legacy 行为不变。
仓库中对应的单元测试位于 packages/channels/base/src/named-session-manager.test.ts,daemon-worker 测试验证运行时专属接线。daemon-backed E2E 计划覆盖"同一群组中的两个用户"与"重启恢复"两个场景;并发运行任务切换与 worktree 创建被明确保留为后续部分(Part 3 / Part 4)的用例。
与 Part 3 / Part 4 的衔接
Part 2 是系列设计的安全第一步,它为后续能力铺好了地基:
- Part 3(并发控制):允许一个 owner 的多个命名任务并发运行并自由切换,加入任务级取消(
/session cancel [<name>])、权限关联(permission correlation)与投递来源标签(source label,形如[feature-a]/[Alice · feature-a])。Part 3 不改变 version 1 registry、daemon session 协议与模型 transcript,其详细设计见 docs/design/channel-named-sessions-part3.md 与已实现的 docs/design/channel-named-sessions-part3a.md。 - Part 4(worktree):加入 worktree 创建与文件系统隔离,届时
/session new <name> --worktree才会真正可用。
配置示例与使用前提
命名会话是可选功能,通过 channel 配置中的multiSession: true开启(配置说明见 docs/users/features/channels/overview.md):
{ "channels": { "my-channel": { "type": "telegram", "sessionScope": "user", "multiSession": true } } }启用前提总结:
- 必须是daemon 托管的 Channel(独立
qwen channel start不支持); sessionScope必须为"user"(实现中违反此条件会直接抛错:requires sessionScope "user" when multiSession is enabled,见 ChannelBase.ts);- 不能配置 webhook;
- channel/group 的
groupHistoryLimit必须为零; - 不能存在已启用的 Channel loop。
sessionScope的完整取值(user默认 /chat_thread/single,legacythread仅兼容已有配置)同样记录在 docs/users/features/channels/overview.md。
小结
Part 2 以"安全、可选、精确"为原则,为 daemon 托管的 Channel 落地了命名会话目录:精确的三元组所有权、1~32 位 ASCII 任务名与大小写不敏感唯一性、8 个打开任务上限、数组化 registry 与原子 rename 持久化、窄化 router 原语、权威性 busy 判定以及严格的失败顺序。它刻意保留"单选中 + 禁止 busy 切换"的保守策略,把并发切换(Part 3)与 worktree 隔离(Part 4)留到安全前提就绪之后。对于希望在 IM 渠道中为同一用户维护多个并行编码任务的开发者来说,这套设计提供了可验证、可恢复、与 transcript 解耦的完整方案;相关实现可进一步阅读 named-session-manager.ts、ChannelBase.ts 与 named-session-manager.test.ts。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考