Qwen Code Channel Named Sessions(Part 2):为 Daemon 托管 Channel 构建多任务命名会话目录
2026/9/12 10:39:24 网站建设 项目流程

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
statusopenclosed
createdAt/updatedAt/lastSelectedAt创建、更新与最近选中时间戳
target原始的 Channel 投递目标

这一结构在 packages/channels/base/src/named-session-manager.ts 中体现为StoredTaskStoredOwnerStoredRegistry三个接口,例如:

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、构建对应的派生索引,再原子写入,最后才把内存中的registrytaskBySessionId索引一起发布(named-session-manager.ts),写入失败时两者都不会发布。

对异常文件的处理策略:

  • 畸形或不支持的 registry:导致该已启用 Channel无法启动,而不是静默替换所有权目录;
  • 结构合法但来自旧 channel 工作目录的目录:归档为 stale 并重置;
  • 指向当前工作目录之外的 legacy 路由:被遗忘(forgotten)而不是被收养。

路由原语(Router Primitives)与 busy 状态

为什么不用普通的 resolve()

命名会话操作不会走 router 普通的懒加载resolve()路径。原因在于该路径在"恢复的 legacy 路由无法加载"时会主动创建替代会话,这与"精确任务选择"的语义不兼容。

router 因此暴露了四个窄化的 managed-session 原语:

  1. create:创建一个活的 daemon session,但不改变当前选中路由;
  2. load:精确加载并校验一个会话,不创建替代
  3. bind:把该校验过的会话绑定为选中的兼容路由,同时为其他活着的命名会话保留投递元数据;
  4. 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 } } }

启用前提总结:

  1. 必须是daemon 托管的 Channel(独立qwen channel start不支持);
  2. sessionScope必须为"user"(实现中违反此条件会直接抛错:requires sessionScope "user" when multiSession is enabled,见 ChannelBase.ts);
  3. 不能配置 webhook;
  4. channel/group 的groupHistoryLimit必须为零;
  5. 不能存在已启用的 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询