Skip to main content
并行协作

跨会话消息

跨会话消息让同一台机器上、同一个用户账号下的两个 Qoder CLI CN 会话互相发现并收发消息。一个会话可以请另一个会话接手任务、传一个文件,或者问一个问题,不需要你在终端之间手工复制文本。 它适合工作天然跨越多个会话的场景:一个终端在改后端、另一个在改前端,或者你想从第二个窗口把东西交给一个长时间运行的会话。
Beta 跨会话消息当前是 beta 特性,默认不启用。启动 Qoder CLI CN 前需要先打开环境变量开关:QODERCN_FEATURE_CROSS_SESSION=1 qodercn 也可以把 QODERCN_FEATURE_CROSS_SESSION=1 写进用户级配置 .env,让之后的会话自动启用。用户级配置默认路径:$HOME/.qoder-cn/.env。修改 .env 后需要重启 Qoder CLI CN。 该特性依赖 Unix domain socket,仅在 macOS 和 Linux 上可用。

工作方式

开启后,每个会话监听一个私有 socket,并把它登记到当前用户的会话注册表里。其他会话读取该注册表来发现 peer,然后直接连过去投递消息。
会话 A(终端 1)                       会话 B(终端 2)
+-- 监听自己的 socket                  +-- 监听自己的 socket
+-- ListAgents  --------------------> 读注册表,发现 B
`-- SendMessage --------------------> 投递到 B 的下一轮对话
socket 及其所在目录只有你自己的用户账号可读。同一台机器上其他用户的会话既看不到也连不上你的会话。

发现 peer

你可以直接让 Agent 列出它能触达的对象,Agent 也会自己调用 ListAgents。列表分两组:当前会话内部的 agent,以及机器上其他的 peer 会话。
Agents in this session (address by name):
- Reviewer

Peer sessions (other Qoder sessions on this machine):
- api-service — interactive, idle, started 12m ago, cwd /home/dev/api, handle -3f
- api-7c — interactive, busy, started 3m ago, cwd /home/dev/api
- web-2b — interactive, idle, started 40m ago, cwd /home/dev/web
会话的名字就是它当前的标题,因此执行 /rename 或 Agent 重新拟定标题时,名字都会变化。状态列同样跟随会话变化:Agent 正在工作时为 busy,正在请求确认时为 waiting,用户处于 shell 提示符时为 shell,其余情况为 idle 尚未命名的会话以工作目录名加上它的 handle 命名,也就是上面的 -7c。没有这个后缀,同一目录下的会话会全部同名,谁也无法被寻址。因此 /home/dev/api 下的两个会话显示为 api-7capi-3f,而不是两个 api peer 用行内的标签寻址,也可以只用 handle。handle 由会话监听的 socket 推导而来,因此在每个会话的列表里都一样,并且会话改名时也不会变——无论它当时叫什么,-7c 都能送达。已命名会话的名字里不含 handle,所以它所在的行会单独标出:handle -3f 运行 /peers 可以看到同一份列表,并附带当前会话自己的 handle。

在提示词里 @ 一个 peer

输入 @ 再敲至少一个字符,补全列表里除了文件和 agent,还会出现 peer 会话。peer 行以 @ 开头,并标注为 session,带上该会话的状态和已运行时长,因此不会和同名文件混淆。
@rev
  @reviewer         session · idle · started 3m ago
选中该行会插入用于寻址那个会话的标签。标签里如果有空格,插入时会像文件路径一样对空格做转义。 @ 一个会话只是告诉 Agent 你指的是哪一个,本身不会发送任何东西。把要做的事写在同一条消息里——比如「让 @reviewer 重跑一次 CI」——Agent 才会去发消息。只 @ 而不提要求的消息,不会触发发送。 如果这个名字同时命中多个存活会话,Agent 拿到的是一组各自只指向一个会话的标签,并会先问你指的是哪一个,而不是自己猜。会话名字是各自取的、无法验证的,所以在有歧义时,不会仅凭名字就寻址到某个会话。 只敲 @ 不会列出 peer 会话;没有命中任何会话的 @ 会原样留在文本里。

发送消息

Agent 使用和给 teammate 发消息相同的 SendMessage 工具,只是收件人换成 peer 名字。可以用绝对路径附带文件,文件会被复制到接收方,供其读取。 发送结果是如实报告的。如果 peer 在列出之后、发送之前退出了,返回结果会说明这一点并建议重新列一次,而不是假装发送成功。

控制收到的内容

每个会话通过 security.crossSessionInbound 设置自行决定如何处理收到的 peer 消息:
  • accept:投递到该会话的下一轮对话。
  • hold:扣留待你复核;在你批准之前 Agent 完全看不到它。
  • refuse:一律拒收;不投递,也不会把任何附件写入磁盘。
未设置该项时,回退策略取决于本会话如何处理权限。绕过权限检查的会话会扣留收到的 peer 消息等你批准 —— 否则 peer 要求的任何操作都会直接执行,不再有确认。仍然会弹确认的会话则直接接受,因为 peer 无法借它跳过自己也要面对的检查。 在没有人能复核的会话里 —— 即通过显式 socket 路径启动的非交互式会话 —— 本该被上述回退策略扣留的消息会改为直接拒收,让发送方立刻得到答复,而不是等待一场不可能发生的复核。如果这类会话本就应当接受 peer 的指令,请显式把 security.crossSessionInbound 设为 accept;你自己配置的值在这种会话里始终被尊重。 项目级设置只能收紧不能放松。仓库里提交的设置可以把 accept 收紧为 hold,但不能把 hold 放宽为 accept。正因为仓库设置可以覆盖你自己的选择,/peers 会始终告知当前生效的设置及其来源文件。

批准被扣留的消息

当信任问题无法自动判定而扣留一条消息时,会话会直接询问你:
Another session sent this one a message

From worker -7c (name and address are supplied by the sender and are not verified)
This session bypasses permission checks, and the sender did not declare its own.
Anything it asks for would run without a further prompt.

  请运行集成测试并汇报失败项

  ❯ Deliver it to this session
    Decline — drop it and tell the sender
这个询问是本会话最后才会显示的东西,绝不会打断你正在回答的权限确认或任何对话框;只有在没有其他东西需要你回答时它才会出现。批准后消息在下一轮对话投递;拒绝则丢弃它,并告知发送方已被拒收。展示的正文经过截断 —— 批准后投递的是完整原文 —— 随消息附带的文件只显示数量,因为它们在消息到达时就已复制过来,你拒绝时会被删除。 同一时间只询问一条,而且只在屏幕空闲时询问。在其他询问或对话框打开期间到达的消息仍会被扣留,改用 /peers 复核;已经打开的询问如果被其他东西占用了屏幕,那条消息也会退回队列,而不是在遮挡后面耗完自己的时限。 询问等待多久由 general.dialogExpiry 决定,默认五分钟。超时后消息被丢弃,并告知发送方是"超时"而不是"被拒绝" —— 这个区别足以让发送方的 Agent 做出不同处理。never 会取消其他对话框的时限,但不会取消这一个,因此一条无人应答的询问不会让之后所有消息都失去询问的机会。 因你自己配置了 hold 而被扣留的消息只是静默入队,不会询问:你自己的长期指令不是一个问题。这类消息用 /peers 复核。 但来自仓库设置文件的 hold 会询问。值是同一个,可这是仓库替你做的决定而不是你自己的决定,所以它会被摆到你面前,而不是悄悄拿掉一次你本来会看到的确认。

复核被扣留的消息

运行 /peers 查看当前生效的策略、可触达的 peer 和等待复核的消息:
/peers
输出的第一行始终说明当前生效的收件策略及其来源 —— 你自己的设置、仓库设置,或权限模式回退;属于回退时说明的是规则本身,而不是某一条消息的判定结果。当消息完全没有到达时尤其值得先看这一行:仓库设置的 refuse 不会产生任何被扣留的消息来解释自己。 每条被扣留的消息会显示一个 id、发送方、扣留原因和内容预览。发送方以 handle 标识 —— 它由对方要求回信的地址推导而来,/peers 还会说明那个地址上是否仍有可触达的会话。地址和它旁边的名字都未经验证:没有任何会话能证明消息是谁发来的。但 handle 可以和上面的 peer 列表对照,名字只能读一读。 按 id 批准或拒绝:
/peers approve ab12cd34
/peers deny ab12cd34
批准会把消息排入下一轮对话;拒绝会丢弃它,并告知发送方已被拒收。

peer 消息能做什么、不能做什么

来自 peer 的消息被当作同事的请求,而不是你本人的指令:
  • 它永远不构成你对某个待确认权限提示的批准。
  • peer 消息里开头的 / 是纯文本,不会被当作 slash command 执行。
  • 权限边界按会话隔离。如果某个 peer 的操作被拒绝后,转而请求另一个会话代为执行,接收方 Agent 会拒绝并向你反馈。

安全说明

隔离依赖文件权限:socket 及其目录仅限你自己的用户账号访问。在这个边界之内,发送方自报的名字没有经过密码学校验,因此任何已经以你的身份运行的进程都可以冒充任意 peer。请把跨会话消息的可信度理解为“等同于以你的账号运行的一切”,并在权限较高的会话上优先使用 hold

设置项参考

  • security.crossSessionInboundacceptholdrefuse,控制本会话如何处理收到的 peer 消息。
  • general.dialogExpiry60s5m10mnever,批准询问等待多久后把消息按超时丢弃,默认 5m。只从你自己的设置读取,不从仓库设置读取。