news 2026/9/20 22:41:20

oh-my-openagent Team Mailbox Fallback Wake:团队消息递送失败时的确定性唤醒回退机制解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
oh-my-openagent Team Mailbox Fallback Wake:团队消息递送失败时的确定性唤醒回退机制解析

oh-my-openagent Team Mailbox Fallback Wake:团队消息递送失败时的确定性唤醒回退机制解析

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

导读

本文以 oh-my-openagent(OmO)仓库中的 Issue 5317 验证证据文档为主线,系统讲解 Team Mode 下邮件信箱(mailbox)的 fallback wake(回退唤醒)机制:当团队消息无法通过实时通道递送时,系统如何通过确定性 prompt-gate 回退与信箱注入,保证消息不丢失、不重复、不扰乱正在运行的会话。读完本文,你将掌握 mailbox 的磁盘落地模型、<peer_message>注入信封、消费租约与确认(ack)语义,以及围绕"唤醒保留(hold)"与"过时唤醒取消(cancellation)"的完整回归验证方法论。

一、背景:为什么需要 fallback wake

在 oh-my-openagent 的 Team Mode 中,成员之间通过 team mailbox 传递消息,核心调用面由 packages/team-core/src/team-mailbox/index.ts 导出,包括sendMessagelistUnreadMessagespollAndBuildInjectionackMessagesreserveMessageForDelivery等原语。消息递送存在两条路径:

  1. 实时递送(live delivery):成员会话空闲时,消息通过 idle-injection 协调器直接"唤醒"(wake)会话并注入上下文;
  2. 回退递送(fallback delivery):实时递送被阻塞(例如接收方会话正处于活跃回合、prompt-gate 判定当前不该打扰)时,消息在信箱中排队,等待下一个可注入时机以"排队唤醒"(queued fallback wake)的形式投递。

Issue 5317 的验证目标正是这两条路径衔接处的确定性行为:排队中的 fallback wake 是否会被准时分发、是否会被过时取消、是否会被错误地重复分发

二、Issue 5317 验证了什么

依据 .omo/evidence/20260828-issue-5317-team-message-fallback-wake/README.md,本轮验证覆盖三个层面:

  • 确定性 prompt-gate 回退与信箱注入:在模块接缝(module seam)处以确定性方式驱动"保留空闲接收方门控 → 发送 → 确认未读 → 释放阻塞器"的完整链路;
  • 聚焦与关联测试、类型检查与构建:对修复点做聚焦回归,同时对受影响的关联模块做全量回归;
  • 真实环境验证:使用一次性(disposable)OpenCode 1.18.4 容器、CI-Bun 1.4.0 本地 bundle,以真实 Team Mode 运行并观察team_createteam_send_message工具注册、SSE 连接与宿主会话状态。

三、测试过程与三轮红绿迭代

3.1 RED:排队 fallback wake 超时

第一轮(failing-first)暴露的缺陷记录在 verification.txt:

messaging.test.ts: timed out waiting for queued fallback mailbox wake corrected gate-blocker path: 1 failed

即:当 prompt-gate 阻塞实时递送后,消息进入排队状态,但排队的 fallback wake 迟迟未被分发,测试等待超时。这是"预约(reservation)→ 唤醒(wake)"状态转换失败导致的。

3.2 REVIEW RED:二次递送与已确认消息的幽灵唤醒

评审(review)阶段又发现两类更微妙的缺陷:

  1. 第二次递送与已接受的实时 prompt 重叠:fallback wake 在接收方正在处理实时 prompt 时被再次分发,造成上下文重叠;
  2. 已确认消息仍分发其排队的 fallback wake:消息已被精确确认(exact-message ack),但排队中的唤醒仍被派发,形成"幽灵投递"。

这两类问题对应到源码中即是:唤醒分发必须尊重pendingInjectedMessageIds与已消费台账(consumed ledger),否则会造成同一消息多次进入接收方上下文。

3.3 GREEN:聚焦与关联回归全绿

修复后各门禁结果(最终 post-merge 数据,见 README):

门禁结果
focused messaging40/40 通过
related messaging/idle-wake/prompt-route68/68 通过
prompt gate core70/70 通过
typecheck / build通过
Senpi 包门禁2419 通过,1 个 Windows-only 跳过
一次性 OpenCode 双次发送完成且无 NDJSON 错误

3.4 真实服务器验证

在一次性 Docker 容器(OpenCode 1.18.4 + CI-Bun 1.4.0 本地dist/index.js)中:

  • 健康检查:healthy
  • team_createteam_send_message均已注册;
  • SSEserver.connected已连接;
  • 宿主会话数保持7933 → 7933(容器运行前后不变,证明 disposable 容器未污染宿主);
  • 实时递送早期预约释放数为 0(live-delivery hold 被正确保留);
  • fallback 在阻塞器过期后正确分发。

四、源码级机制:mailbox 的磁盘落地与唤醒分发

4.1 信箱目录结构与消息文件

信箱以文件系统为存储介质,路径由 packages/team-core/src/team-registry/paths.ts 的getInboxDir决定:

<base_dir>/runtime/<teamRunId>/inboxes/<memberName>/

每个未读消息是<messageId>.json;消息进入递送预约后重命名为.delivering-<messageId>.json;确认消费后移动到processed/<messageId>.jsonbase_dir默认是~/.omo,也可通过配置覆盖。所有路径段经过resolveContainedPath校验,防止路径穿越(TeamPathTraversalError)。

消息结构由 packages/team-core/src/types.ts 的MessageSchema定义:messageId(UUID)、fromtokindbody(上限 32KB)、timestamp、可选的summaryreferencescorrelationIdcolor

4.2 发送与背压控制

sendMessage(packages/team-core/src/team-mailbox/send.ts)在写盘前做多重校验:

  • 广播(to: "*")仅限 lead 角色,否则抛BroadcastNotPermittedError
  • 负载超过message_payload_max_bytes(默认 32768 字节)抛PayloadTooLargeError
  • 团队处于 deleting/deleted 状态抛TeamDeletingError
  • 收件人未知或不活跃抛InvalidRecipientError
  • 未读信箱总字节超过recipient_unread_max_bytes(默认 262144 字节)抛RecipientBackpressureError(背压);
  • 消息 ID 已存在(未预约或已预约)抛DuplicateMessageIdError

写盘通过withLock<inboxDir>.lock)串行化,保证并发写者互不覆盖——这一点由 send.test.ts 的"4 个并发写者写入 4 个不同文件"用例验证。值得注意的是,对于预约中的收件人,消息会直接以.delivering-预约形态落盘(reservedRecipients分支),这正是"预约 → 唤醒"转换的起点。

4.3 轮询与注入:<peer_message>信封

packages/team-core/src/team-mailbox/poll.ts 的pollAndBuildInjection是唤醒分发的前置环节:它列出未读消息,排除已在pendingInjectedMessageIds中的消息,并将每条消息序列化为<peer_message>信封(buildEnvelope),携带fromtimestampmessageIdkindcorrelationId等属性。同时通过运行时状态机保证:

  • 同一 turn marker 内不重复注入(lastInjectedTurnMarker);
  • 存在 pending 消息时返回reason: "pending ack",避免在确认前重复投递。

MessageSchema中消息正文上限 32KB,与发送侧的message_payload_max_bytes默认值一致,构成端到端约束闭环。

4.4 唤醒协调器:IdleInjectionCoordinator

真正的 wake 由 packages/omo-senpi/src/extension/idle-injection-coordinator.ts 的IdleInjectionCoordinator承担。它是父会话的单一注入队列:任务完成、团队消息、DAG 运行摘要、ulw 续跑等通知全部入队,在批次窗口内合并为恰好一次omo-senpi:wake注入(customType: "omo-senpi:wake"),再以steerfollowUp方式送入运行中的回合。

关键设计:

  • 确定性排序SOURCE_RANK规定了混合批次中先注入哪些来源——任务完成最先、DAG 摘要最后、team-message排在第 1 位;
  • 回执契约:已接受的注入必有且仅有一个回执——成功触发onFlushed,失败触发onDeliveryFailed;被拒绝(协调器已退休)则无回执,由生产者自行走持久化失败路径;
  • 退休即取消:会话关闭时retire()取消已武装的批次窗口定时器、清空队列并给所有待注入项发失败回执,保证任何回调都无法触达已失效的生成器(issue #7932 的教训)。

这条源码脉络印证了 README 中"保留已接受的实时递送 hold、仅在阻塞器过期后分发 fallback"的修复目标:fallback wake 必须经由协调器排队,而不是绕过批次窗口直接投递。

4.5 消费租约与确认

  • 消费租约:consumer-lease.ts 的withInboxConsumerLease通过.consumer.lock租约文件串行化同一信箱的消费方,支持staleAfterMs过期回收(死进程租约可被立即重获,见 consumer-lease.test.ts);
  • 确认:ack.ts 的ackMessages将未读文件或预约文件原子地renameprocessed/,且对已确认的消息重复确认幂等(ack.test.ts 验证了两次 ack 不报错、不残留);
  • 消费台账:consumed-ledger.ts 的isMessageConsumed通过检查processed/<id>.json是否存在判定消息是否已消费。

这三者共同支撑了"exact-message ack 后取消过时唤醒"的修复:一旦消息落入processed/,任何排队的 wake 都应被判定为过时并取消。

4.6 预约回收与待交付恢复

packages/team-core/src/team-mailbox/reservation.ts 定义了递送预约的三态文件迁移:<id>.json(未读)→.delivering-<id>.json(预约中)→processed/<id>.json(已消费)。reclaimStaleReservations会把超过staleTtlMs的预约文件恢复为未读文件,防止预约永久卡死。

packages/team-core/src/team-mailbox/pending-delivery-recovery.ts 则提供两类关键恢复原语:

  • findDeliveredMessageIds:通过client.session.messages读取接收方会话历史,检测<peer_message messageId="...">信封是否真的进入了上下文。未返回的消息意味着"wake 被接受但从未进入上下文",此时 ack 会静默丢消息,因此必须重新排队而非确认——任何读取错误都返回空集合(loss-safe 答案,调用方重排而不是 ack);
  • requeuePendingLiveDeliveries:把确认未达的预约消息恢复为未读文件,供下一轮 poll-injection 或 wake-hint 重新投递。

这两条路径正是 README 中"保留 filesystem errors、不把不可读信箱条目变成虚假确认"的实现依据。

五、为什么"这样就够了":修复清单背后的验证推理

README 的 "Why it is enough" 部分逐条说明了各回归用例所覆盖的缺陷面,结合源码可以对应到具体机制:

回归覆盖点对应机制
失败的"预约 → 唤醒"转换reservation 三态迁移 +pollAndBuildInjection的 pending 过滤
保留已接受的 prompt hold协调器批次窗口 +lastInjectedTurnMarker防重
exact-message ack 后取消唤醒ackMessages迁移到processed/+ 消费台账判定
ack 旧的可合并消息不 strand 新消息消息级去重:按messageId独立处理,非按批次
transient 校验错误排队重试而非取消 wake协调器的onDeliveryFailed回执走持久化失败路径
团队状态缺失时取消过时条目sendMessageTeamDeletingError/ runtime 状态检查
pending ack 的消息取消冗余 queued wakependingInjectedMessageIds去重
分类的发送前连接失败保留 fallback 队列条目wake 接受后才清理队列项
可重试队列失败最多 3 次capped retry(三上限)
预约槽读取保持可见.delivering-文件在getUnreadSizeBytes中被计入
重试分类不再清除保守分发 hold修复 REVIEW RED 的"重叠分发"问题
fallback wake 采用 capped exponential backoff替代全局丢弃策略,延迟的持久条目轮转在无关 prompt 之后
明确的 host/network 不可达与连接超时进入 durable retry连接分类器(connection classifier)判定
无资格的 fetch 错误不进入重试路径因可能发生在 OpenCode 已接受请求之后,只有错误本身或其 cause 上的明确连接码可重试
exact-message 读取保留文件系统错误inbox.ts 的readUnreadMessageById:读取失败抛错而非返回 undefined

最后一条值得展开:readUnreadMessageById会依次尝试未读路径与预约路径,读取过程中若出现 ENOENT 之外的真实错误(如权限问题),会记录team-mailbox-exact-message-read-failed重新抛出,而不是吞掉错误返回"未找到"。这保证了"不可读的信箱条目"绝不会被误判为"消息已读/可确认",与 README 中"Exact-message reads preserve filesystem errors instead of turning an unreadable inbox entry into a false acknowledgement"完全对应。

六、验证证据的组织方式

本仓库将验证证据沉淀为两层:

  1. 主证据:.omo/evidence/20260828-issue-5317-team-message-fallback-wake/——包含 README(测试范围、观察结果、充分性论证、省略项)、manual-qa.txt(确定性模块表面与真实 OpenCode 表面的逐项检查清单)、verification.txt(failing-first 与修复后的数值门禁);
  2. Senpi 适配器子证据:.omo/evidence/omo-senpi-adapter/20260828-issue-5317-team-message-fallback-wake/——包含 live-driver.json(真实 Senpi 驱动结果为PASS、ultrawork 注入被观测到、comment checker 通过、真实 agent 目录未被触碰)、package-gate 与 self-test 记录。

文档同时明确记录了省略项:凭据、宿主配置、请求头与含密钥的日志;未做完整 provider turn(确定性竞态在模块接缝处覆盖);完整套件中无关的 OAuth 回调失败以 scoped gates 记录替代。这种"测了什么、为什么够、省了什么"的三段式证据结构,本身即是一种可复用的回归验证模板。

七、如何复现与深入

7.1 运行 mailbox 相关测试

仓库使用 bun 作为测试运行器,可在仓库根目录执行聚焦测试:

# 聚焦 team-mailbox 全模块测试 bun test packages/team-core/src/team-mailbox # 聚焦消息发送/确认/轮询注入 bun test packages/team-core/src/team-mailbox/send.test.ts bun test packages/team-core/src/team-mailbox/ack.test.ts bun test packages/team-core/src/team-mailbox/poll.test.ts # idle 唤醒协调器 bun test packages/omo-senpi/src/extension/idle-injection-coordinator.test.ts bun test packages/omo-senpi/src/components/memory/kibitzer/delivery-idle.test.ts # 类型检查与构建 bun run typecheck bun run build

7.2 关键配置项

Team Mode 的 mailbox 行为由 packages/team-core/src/config.ts 的TeamModeConfigSchema控制,常用项及默认值:

配置项默认值说明
enabledfalse是否启用 Team Mode
base_dir~/.omo团队运行时数据根目录
message_payload_max_bytes32768单条消息负载上限(与MessageSchema.body上限一致)
recipient_unread_max_bytes262144单收件人未读信箱背压上限
mailbox_poll_interval_ms3000信箱轮询间隔(最小 500ms)
max_parallel_members4最大并行成员数(1~8)
max_members8团队最大成员数(1~8)
max_messages_per_run10000单次运行最大消息数
max_wall_clock_minutes120单次运行最大墙钟时间
max_member_turns500单成员最大回合数

7.3 真实环境验证的复现前提

真实服务器验证依赖一次性 OpenCode 容器(OpenCode 1.18.4)与 CI-Bun 1.4.0 本地 bundle,验证目标是"工具面(tool surface)真实验证 + 模块接缝确定性覆盖"的组合:容器化运行证明 shipped 工具面可用,模块级测试证明 hold 保留与过时唤醒取消。复现时注意保持宿主 OpenCode 会话数与容器运行前后一致(本证据中为 7933),以确认 disposable 容器不产生宿主副作用。

八、结论

Issue 5317 的修复与验证为 oh-my-openagent 的 Team Mode 消息递送建立了明确的正确性契约:

  1. 不丢:wake 被接受但未进入上下文的消息必须重新排队(pending-delivery-recovery),不可读的条目保留错误而非虚假确认;
  2. 不重:同一 turn marker、同一pendingInjectedMessageIds、已消费台账三重去重,配合协调器的单次注入契约;
  3. 不扰:fallback wake 尊重实时递送 hold,仅在阻塞器过期后分发;过时唤醒(消息已 ack、团队已删除、协调器已退休)一律取消;
  4. 可恢复:可重试失败走 capped exponential backoff 的 durable 重试路径,明确连接类错误才可重试,无资格错误不进重试。

这套从磁盘文件状态机到唤醒协调器、再到三层证据验证(failing-first → review 修正 → post-merge 重跑)的完整链路,为后续 Team Mode 消息可靠性相关的改动提供了一个可直接参照的回归基准。

【免费下载链接】oh-my-openagentOmO: Just type "mass ulw" keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 22:40:51

讨论和结论到底差在哪 —— 别把两个章节写成一样的内容

很多学生的论文中&#xff0c;讨论部分和结论部分读起来几乎一模一样 —— 都在总结研究发现、都在讲理论贡献、都在提未来方向。这是结构上的浪费。讨论和结论承担不同的功能&#xff0c;应该有不同的写法。汇写&#xff08;https://www.huixielunwen.com/tool/graduationThes…

作者头像 李华
网站建设 2026/9/20 22:39:19

WorkBuddy实战:用AI智能体搭建每日工作自动化流程

每天打开电脑&#xff0c;有多少时间是花在重复劳动上的&#xff1f;整理日报、汇总数据、定时签到、抓取网页信息、回复固定格式的邮件……这些事情不复杂&#xff0c;但就是消耗时间。我一直在找一款能把这类“琐碎但必须做”的工作真正自动化的工具&#xff0c;试过不少脚本…

作者头像 李华
网站建设 2026/9/20 22:38:05

专科生必学:10款抗AI工具提升职业竞争力

1. 专科生如何应对AI时代的工具选择困境2026年的专科生正面临前所未有的就业压力——AI自动化正在快速取代传统岗位。根据最新行业调研&#xff0c;未来3年内约有47%的现有岗位将受到AI影响。作为专科生&#xff0c;掌握能降低被AI替代风险&#xff08;简称"降AI率"&…

作者头像 李华