收件箱、团队与进度检查

持久化的 inbox 表承载后台 actor 的通知、权限转发与追加指令:写入与唤醒、排空成合成消息、渲染格式,以及残存的 Team 模块与进度日志检查。

作者 David更新于 第 19 篇(共 33 篇)

上一篇的 spawn 立即返回,子智能体在后台跑完,结果得有路回到父级;Orchestrator 的子会话要请求审批;父级还要给已经闲下来的子智能体追加任务。MiMo Code 给这些消息修了同一条路:一张持久化的 inbox 表。发送方只管写一行并叫醒收件方,收件方的主循环每次迭代开头把收件箱排空,变成一条合成的 user 消息。上游 v1.14.19 的 packages/opencode/src 下没有对应的模块。

怎么用

  • 模型发消息:actor send <to_actor_id> "<content>" [--session <id>] [--type <t>],收件方可以是同一会话的子智能体(如 explore-1)、某个会话的 main,或用 --session 指定的别的会话(packages/opencode/src/tool/actor.ts:577)。收件方不存在时不抛错,而是返回 inboxID 为 null 并附 receiver not found。Orchestrator 另有 session send,见下一篇。
  • 模型收消息:普通消息被包成 <inbox from="发送会话:发送者" sent_at="…">;运行时生成的通知是一段 <actor-notification>,两者都作为 user 消息出现在下一次迭代里。actor send 填的发送者是调用方的 agent 名(actor.ts:590),不是 actor ID。
  • 用户看到的:后台子智能体完成、失败、取消、停滞都会弹 toast;开了 Orchestrator 实验开关时,TUI 还会把 <actor-notification> 还原成一张卡片(packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:1618)。
  • 清理:没有配置项;Inbox 服务初始化时删除 7 天前的行(packages/opencode/src/inbox/inbox.ts:18、inbox.ts:140)。

表与服务

inbox 表(packages/opencode/src/inbox/inbox.sql.ts:5)以 ulid 为主键,收件方是 receiver_session_id 加 receiver_actor_id,发送方两列可空,type 默认 text,正文以 JSON 存成带 text 字段的对象,外加 created_at。索引建在“收件会话、收件 actor、id”上(inbox.sql.ts:21),ulid 按时间排序,排空时按 id 取出,大体就是到达顺序。Inbox 服务只有 send 与 drain 两个方法(inbox.ts:120)。

消息从哪来

来源type收件方位置
后台 actor 首轮结束(完成或失败),checkpoint-writer 除外actor_notification父会话里的 parentActorID,缺省 mainpackages/opencode/src/actor/spawn.ts:429
后台 actor 被取消actor_notification同上spawn.ts:912
停滞看门狗actor_notification同上spawn.ts:990
被唤醒的 peer 跑完一轮actor_notification父会话packages/opencode/src/session/prompt.ts:4895
Workflow 运行结束actor_notification所在会话的父 actorpackages/opencode/src/workflow/runtime.ts:1419
子会话的权限请求被转发text父会话的 mainpackages/opencode/src/permission/index.ts:417
actor send默认 text任意已登记的 actoractor.ts:585
session send,以及 session create --topic 命中已有子会话textpeer 子会话packages/opencode/src/tool/session.ts:893、tool/session.ts:752

取消与停滞通知只在对象是后台 subagent 或 peer 时发出,并排除 checkpoint-writer、dream、distill 这些运行时派生的智能体。Workflow 的通知虽然 type 也是 actor_notification,正文却是 Workflow completed. run_id: … 这样的纯文本,没有 <actor-notification> 外壳。

send:写一行,再叫醒

图表加载中…

send 先做存在性检查(inbox.ts:145):收件方必须在 actor_registry 里有行,否则失败为 InboxReceiverNotFound。所以 peer 要在 session create 返回前同步登记,每个会话自带的 main 行也在这里派上用场。随后插入、发布 inbox.arrived 事件,再 fork 一个唤醒(inbox.ts:178):

      const promptRef = sessionPromptRef.current
      if (promptRef) {
        yield* promptRef
          .loop({
            sessionID: input.receiverSessionID,
            agentID: input.receiverActorID,
            // Woken turns notify their parent on completion. The spawn turn goes
            // through SessionPrompt.prompt (no flag) so forkWork.notify remains
            // the sole notifier for turn 1 — no double-notify.
            notifyParentOnComplete: true,
          })
          .pipe(Effect.ignore, Effect.forkIn(scope))

唤醒 fiber 挂在 Inbox 服务自己的作用域里,发送方返回后照样送达。sessionPromptRef 是一个模块级的后绑定引用(packages/opencode/src/inbox/inbox-ref.ts:29),由 SessionPrompt 初始化时填入(prompt.ts:5326):主循环每次迭代都要调 inbox.drain,Inbox 若再以普通 Layer 依赖 SessionPrompt 就成环了。同样的手法还有 inboxServiceRef(actor 与 session 工具借它拿 Inbox,inbox-ref.ts:65)和 defaultModelRef(inbox-ref.ts:51)。

SessionPrompt.loop 按 sessionID:agentID 找运行器(prompt.ts:4923):空闲就开跑一个新的主循环;已经在跑,就只等待当前这次运行(packages/opencode/src/effect/runner.ts:114),新行留在表里,等下一次迭代开头排空。

有一点要注意:把 actor 标成 running、结束后写回结局的,只有上一篇的 runTurn(packages/opencode/src/actor/turn.ts:16),而经收件箱唤醒的轮次走 SessionPrompt.loop,不经过它。按代码读,一个已经 idle 的 actor 被唤醒后,注册表状态在这一轮里不会翻回 running:停滞看门狗不会盯它,session status 与 session join 看到的仍是上一轮的结局。

drain:变成一条合成 user 消息

主循环每次迭代开头、读取本切片历史之前,先排空自己的收件箱(prompt.ts:3624)。drain(inbox.ts:201)的步骤:

  1. 按 id 顺序取至多 MAX_DRAIN_PER_TURN 即 100 行(inbox.ts:19),一行没有就立即返回。
  2. 决定合成消息挂在哪个 agent、用哪个模型(resolveDrainSeed,inbox.ts:64):先找最近一条真实的、带模型的 user 或 assistant 消息,普通收件方只在自己切片里找,peer 跨切片找;找不到就用注册表行里的 agent 加项目默认模型;仍然没有则不写消息,行留在表里等下次。
  3. 逐行渲染,渲染出空白的行丢弃并记日志;全部空白就只删行、不写消息(inbox.ts:261、inbox.ts:277)。空白的 user 文本部件会被 AI SDK 过滤掉,发给提供方时变成空内容而被拒。
  4. 写一条 agentID 为收件方的 user 消息,每行一个 synthetic: true 的文本部件(inbox.ts:297),然后删除这些行。写消息与删行不在一个事务里,注释承认崩溃在两者之间会让下次重复注入同样的通知(inbox.ts:289)。

写入的是新的 user 消息,主循环于是把它当作本轮输入继续跑;这也是“闲下来的子智能体收到 send 就接着干”的全部机制。

渲染成什么样

renderInboxRow(packages/opencode/src/inbox/render.ts:13)只分两类:actor_notification 的正文由发送方预先渲染,原样放行;其余类型一律包成带发送者与时间的 <inbox> 元素(render.ts:24):

  const sender = row.sender_session_id
    ? `${row.sender_session_id}:${row.sender_actor_id ?? "?"}`
    : "system"
  const sentAt = new Date(row.created_at).toISOString()
  return `<inbox from="${sender}" sent_at="${sentAt}">\n${blankTo(content.text, "(empty)")}\n</inbox>`

转发来的权限请求只填了发送会话,所以显示为 from="子会话ID:?",正文是一段 <permission-request>,告诉 Orchestrator 用 session approve 或 session grant-approval 处理。

通知正文由 renderActorNotification(render.ts:31)生成,首行固定以 Background sub-session "描述" (actor_id: …) 开头,接下来的措辞按情形变化:

情形首行结尾后续行
完成,自报 success、partial 或没报completed.有自报时的 Status:,以及 Summary:、Result:
完成,自报 failed 或 blockedfinished (status: failed). 之类Summary:、Result:
完成,自报值无法识别ended (status not reported).Summary:、Result:
失败failed.Error:
停滞appears stalled (no activity for Ns).建议查看、催促或取消
取消was cancelled.—

之所以区分“进程正常结束”和“任务是否成功”,是为了不让父级把一次自报失败读成完成(render.ts:47 的注释)。TUI 用反向的 parseActorNotification(render.ts:101)还原出状态、描述与一行摘要。render.ts:86 起的注释说 stalled 留给将来的看门狗、目前不会产生,已经过时:看门狗早就在发(spawn.ts:990)。

唤醒后的回报

forkWork 的通知只包住 spawn 那一轮。持久的 peer 闲下来后被 send 唤醒,跑完新的一轮时,主循环在结尾检查 notifyParentOnComplete:本次是收件箱唤醒的、会话有父会话、自己是后台 peer 且不是系统智能体,就用同样的格式给父会话投一条完成或失败通知(prompt.ts:4883)。首轮不带这个标志,不会重复通知;Orchestrator 因此不必轮询子会话。

条件里写死了 actor.mode === "peer",还要求会话有 parentID。按代码,同会话的 subagent 被 actor send 唤醒后跑完的那一轮不会通知父级,这与 packages/opencode/src/tool/actor.txt:126 对 send 的说明不符:

The woken turn's result reaches you as a completion notification, not from the send call.

团队:只剩一个空壳

packages/opencode/src/team/index.ts:13 定义的 Team 服务只有五个方法:create、addMember、removeMember、getMembers、teamDir,数据是项目目录下 .mimocode/teams/<团队 ID>/members.json 里的成员数组(team/index.ts:36),成员记录会话 ID、agent、角色与加入时间。事件定义了 team.created 与 team.member.joined(packages/opencode/src/team/events.ts:5、team/events.ts:13),但只有前者在 create 里发布(team/index.ts:69),addMember 并不发后者;packages/opencode/src/team/schema.ts:7 的 TeamMessage 在 src 里也没有使用者。

源码里除了图层装配(prompt.ts:5367、packages/opencode/src/tool/registry.ts:547)找不到任何调用方。测试 packages/opencode/test/team/migrate-to-inbox.test.ts:97 断言 tool/team.ts 已删除,migrate-to-inbox.test.ts:107 断言 sendMessage、checkMessages、readState、writeState 不再存在:团队间收发消息那一套已经迁到收件箱,剩下的成员名册暂时没有入口。

进度检查:两种“盯”法

运行中靠上一篇的停滞看门狗:后台子智能体静默超过 6 分钟,父级收到一条 appears stalled 通知。

结束时靠内置插件 SubagentProgressCheckerPlugin(packages/opencode/src/plugin/subagent-progress-checker.ts:102,登记在 packages/opencode/src/plugin/index.ts:156 的内部插件列表里)。它挂在 actor.postStop 上,检查绑定了任务的子智能体有没有留下进度日志:

  • 匹配范围:用 excludeOnly 排除 checkpoint-writer、title、summary、dream、distill、compaction、main(subagent-progress-checker.ts:113)。默认 matcher 会跳过 general、explore 这些内置类型,而它们恰恰是最常绑定任务的子智能体,所以这里特意改写。
  • 何时跳过:spawn 时没传 task_id(subagent-progress-checker.ts:127);agent 自身规则禁止写入,canWrite 为 false(subagent-progress-checker.ts:132,由 spawn.ts:472 按 agent 权限算出);配置了 memory.disable_write: true,这时写入会被路径护栏拒绝,催也没用(subagent-progress-checker.ts:139)。
  • 检查什么:文件在 <数据目录>/memory/sessions/<会话 ID>/tasks/<TID>/progress.md(packages/opencode/src/session/checkpoint-paths.ts:84),必须包含 5 个标题 ## §1 Task identity 到 ## §5 Outcome and discoveries(subagent-progress-checker.ts:11)。
  • 不合格:文件不存在或缺标题时,返回 continue 并附上完整模板作为理由,子智能体据此再跑一轮去补写(subagent-progress-checker.ts:151、subagent-progress-checker.ts:157),至多 3 次。
  • 合格:插件往文件头写入 written-at 时间戳的 frontmatter(subagent-progress-checker.ts:164),不再要求重跑。这份日志随后由检查点写手读取、并入会话检查点,见 检查点写手 与 树状任务。

缺文件时,催促的理由以这一句开头(subagent-progress-checker.ts:48):

Before terminating, write the task progress journal to:

下一篇:Orchestrator:一个会话管所有会话——把 peer 子会话、收件箱与权限转发组合起来,用一个会话调度所有工作。

本页目录