Skip to content

fix: 可靠信箱:Codex→Claude 确认制 at-least-once 投递(修复 #223)/ reliable mailbox: acknowledged at-least-once Codex→Claude delivery (fixes #223)#239

Open
KDRage wants to merge 1 commit into
raysonmeng:masterfrom
KDRage:fix/223-reliable-mailbox
Open

fix: 可靠信箱:Codex→Claude 确认制 at-least-once 投递(修复 #223)/ reliable mailbox: acknowledged at-least-once Codex→Claude delivery (fixes #223)#239
KDRage wants to merge 1 commit into
raysonmeng:masterfrom
KDRage:fix/223-reliable-mailbox

Conversation

@KDRage

@KDRage KDRage commented Jul 17, 2026

Copy link
Copy Markdown

Summary

中文:为 #223 实现"可靠信箱":每条 Codex→Claude 消息在 Channel 推送之前同步进入 adapter 内存信箱,Channel 推送只是延迟优化而非投递证明。get_messages 变为非破坏性读取(稳定顺序、稳定 ID),新增 ack_messages 工具:只有 Claude 显式确认"已处理"的 ID 才会被移除;未确认的推送按 FIFO 有界重试。空闲会话不再静默丢失 Codex 回复——最坏情况下消息安全地留在信箱里,Claude 一旦活跃即可取回。

English: Implements a "reliable mailbox" for #223: every Codex→Claude message enters the adapter's in-memory mailbox synchronously before its Channel push — the push is a latency optimization, not proof of receipt. get_messages becomes non-destructive (stable order, stable IDs), and a new ack_messages tool removes only IDs Claude explicitly confirms as fully processed; unacknowledged pushes get bounded FIFO retries. An idle session can no longer silently lose a Codex reply — worst case, the message stays safely in the mailbox until Claude is next active.

Motivation

中文#223:push-only 投递下,发往空闲 Claude 会话的 Codex 回复会静默丢失(#29 的回归,无队列兜底)。已合并前的两个尝试各有缺口:先推后入队有竞态且成功推送会重复处理;仅源侧改动未同步 bundle 且缺测试。本 PR 从 master(a3e927f)全新实现。

English: #223: with push-only delivery, a Codex reply sent to an idle Claude session was silently lost (a regression of #29, no queue fallback). Prior attempts each had gaps (queue-after-push races and double-processing of successful pushes; source-only changes with stale bundles and no tests). This PR is a fresh implementation on master (a3e927f).

Changes

  • 入箱先于推送:可入箱消息在任何 transport await 之前同步入箱 / Queue-before-push: admissible messages enter the mailbox synchronously before any transport await.
  • 不可变投递代 ID(与 daemon 源 ID 分离,meta 同时携带两者):迟到 ACK 不会误删复用同一源 ID 的新消息 / Immutable delivery-generation IDs separate from source IDs (both in Channel meta): a late ACK cannot delete a newer message reusing the same source ID.
  • get_messages 非破坏性、稳定有序;支持可选 ack_ids 顺带确认上一轮结果 / Non-destructive, stably ordered get_messages; optional ack_ids to confirm a previous batch in the same call.
  • 新增 ack_messages 工具;Channel 内容与 meta 都携带稳定 ID 与直接 ACK 指引(ack_required / ack_tool)/ New ack_messages tool; Channel content + meta carry the stable ID and a direct-ACK instruction.
  • 未确认推送 FIFO 有界重试:默认共 3 次尝试,60s 起指数退避;transport promise 乱序/挂起也不破坏 FIFO;ACK/逐出/耗尽都会取消对应定时器 / Bounded FIFO retries for unacknowledged pushes: 3 total attempts, exponential from 60s; FIFO survives out-of-order or hung transport promises; timers cancelled on ACK/eviction/exhaustion.
  • 源 ID 冲突保护:内容/来源/resume 指纹去重;载荷冲突生成确定性碰撞别名;活跃信箱条目在去重缓存 TTL/容量逐出后仍保持权威 / Source-ID collision safety: content/source/resume fingerprints; deterministic collision aliases for conflicting payloads; active mailbox entries stay authoritative even after dedupe-cache TTL/capacity eviction.
  • 边界与可观察性:100 条 / 4 MiB UTF-8 / 2048 去重项 / 20 分钟 TTL(均可用环境变量配置);溢出与超大消息都会产生可观察警告;超大消息不入箱,best-effort 推送并明确声明"不可恢复、勿确认"(ack_required:false)/ Bounds + observability: 100 messages / 4 MiB UTF-8 / 2048 dedupe entries / 20-min TTL (all env-configurable); overflow and oversized omissions emit observable warnings; an oversized message is not admitted — it is pushed best-effort with an honest "not retained, do not acknowledge" preamble (ack_required:false).
  • ack_ids 单次上限跟随信箱容量(max(100, maxBufferedMessages)),保证 drain 提示的"一次确认全部 ID"始终可执行 / The per-call ack_ids cap follows mailbox capacity, keeping the drain epilogue's "ack all pending IDs" instruction always executable.
  • 系统消息与预算恢复共用同一信箱;ack_resume(或对任一兄弟投递的 ack_messages)收敛该 resume 的全部兄弟尝试并调用既有 resume 处理器 / System + budget-resume messages use the same mailbox; ack_resume (or ack_messages on any sibling) retires all sibling attempts and invokes the existing resume handler.
  • 指令面全面迁移:MCP server instructions、collaboration-content.ts 注入块、仓库 CLAUDE.md/AGENTS.md、plugin README、marketplace 描述、kickoff 消息、双语 README(zh-CN 全部中文化)/ All instruction surfaces migrated: MCP server instructions, the collaboration-content.ts injected blocks, repo CLAUDE.md/AGENTS.md, plugin README, marketplace description, kickoff message, both READMEs (zh-CN fully in Chinese).
  • 新增 src/unit-test/reliable-mailbox.test.tsdocs/test-plans/issue-223-reliable-mailbox.md;plugin bundles 已按 ADR 重建提交 / New unit suite + E2E test plan; plugin bundles rebuilt and committed per the bundle ADR.

语义边界(诚实声明)/ Semantics & non-goals (honest scope):

  • 保证 / Guaranteed: 已入箱、未被边界逐出的消息在 Claude adapter 存活期间不丢失(P0);at-least-once + 接收端幂等指引(P1)。
  • 不保证 / Not guaranteed: 崩溃持久化(信箱仅内存,Claude adapter/插件进程重启会丢失未确认条目——P2 建议作为后续独立改动);完全空闲会话的必然唤醒(重试提高概率,get_messages 是活跃后的恢复路径)。README/plugin README 的 Limitations 均已如实写明。

Testing

  • bun run typecheck passes
  • bun test src passes — full suite 1673 pass / 0 fail (109 focused mailbox/delivery/resume tests, 321 assertions)
  • bun run check exit 0 (typecheck + tests + verify:plugin-sync + plugin version alignment)
  • smoke:built + smoke:pack pass (18 files packed, all required artifacts)
  • Manually tested with Claude Code + Codex (live, AGENTBRIDGE_SAFE=1, disposable repo, Claude Code 2.1.211 / Codex CLI 0.144.5)

Live reproduction (manual steps) / 手工复现步骤:

  1. Pair up in a disposable repo; leave Claude idle at its prompt; have Codex return a unique one-line sentinel. / 一次性仓库配对;Claude 空闲;让 Codex 返回唯一哨兵串。
  2. Claude idle → not woken (the Codex→Claude replies silently lost when Claude is idle (push-only regression of #29; no queue fallback) #223 symptom). Activate Claude: first get_messages returns the sentinel; a second get_messages returns the same stable delivery ID (non-destructive). / 空闲未唤醒(Codex→Claude replies silently lost when Claude is idle (push-only regression of #29; no queue fallback) #223 症状)。激活后第一次 get_messages 拿到哨兵;第二次返回同一稳定 ID(非破坏性)。
  3. ack_messages with exactly that ID → acknowledged=1; the next poll no longer contains the sentinel; unrelated messages remain. / 精确确认该 ID 后,下一次轮询不再包含哨兵;无关消息保留。
  4. Restart boundaries per docs/test-plans/issue-223-reliable-mailbox.md: daemon restart / Codex reconnect with the same adapter → mailbox survives; adapter process restart → mailbox lost (documented, P2 not claimed). / 重启边界见测试计划:同 adapter 下 daemon 重启/Codex 重连信箱存活;adapter 进程重启信箱丢失(如实记录,不声称 P2)。

Known remaining findings (recommended follow-ups)

中文:本 PR 经过多轮多智能体对抗式审查(每个发现均经独立反驳验证)。以下已确认、但刻意未在本 PR 内修复的问题,建议作为后续改动处理——它们都涉及需要 maintainer 拍板的设计取舍,不影响上述 P0/P1 保证:

English: This PR went through multiple adversarial multi-agent review rounds (every finding independently verified against refutation). The following are confirmed but deliberately not fixed here — each involves a design trade-off that deserves maintainer sign-off, and none breaks the P0/P1 guarantees above:

  1. 临时系统通知的重投策略(建议优先处理)/ Ephemeral system-notice redelivery (recommend addressing first). system_turn_started(⏳)/system_turn_completed(✅)与 daemon 断连/重连通知现在与普通消息一样享受重试与"确认前一直保留"。长会话中它们会累积占用 100 条容量,且过期的 ⏳ 可能在 ✅ 之后被重推(指令要求 Claude 见 ⏳ 不回复 → 可能卡住协作节奏)。建议:通知分级——瞬态通知不重试 + 取代语义(✅ 收敛对应 ⏳,reconnected 收敛 disconnected,或短 TTL),可执行指令保持可靠信箱待遇。/ Turn/connectivity notices now get the same retry + retain-until-ack treatment as real messages: they accumulate toward the 100-slot cap in long sessions, and a stale ⏳ can be re-pushed after its ✅ (instructions tell Claude not to reply while ⏳ is latest → possible stall). Suggestion: notice classes — ephemeral notices get no retry + supersession (✅ retires its ⏳; reconnected retires disconnected; or a short TTL), while actionable directives keep full mailbox treatment.
  2. 逐出时的去重墓碑 + 双重用途字节上限 / Eviction tombstones + dual-use byte cap. 溢出逐出会连同删除该消息的去重墓碑——即使其 Channel 推送已成功、Claude 正在处理;daemon 的 at-least-once 重放随后会以投递 ID 再次入箱 → 可能重复处理。另外 maxBufferedBytes 同时是单条上限与全信箱上限:一条恰好 4 MiB 的可入箱消息可一次逐出全部其余待确认条目。建议:推送成功过的条目在逐出时保留墓碑;单条与全箱上限解耦。/ Overflow eviction deletes the dedupe tombstone even when a push already succeeded, so a daemon at-least-once replay re-admits under a NEW delivery ID → possible double-processing. Also maxBufferedBytes is both the per-message and whole-mailbox cap: one exactly-4 MiB admission can evict every other pending entry. Suggestion: retain tombstones on eviction for entries with a resolved push; decouple the two caps.
  3. 空闲唤醒缺口三件套 / Idle-wake-gap family. (a) 3 次尝试耗尽后,空闲会话在该 adapter 生命周期内不会再被推送(文档已如实声明);(b) daemon 重放一条仍在箱内的消息被去重抑制时,未借机对既有稳定 ID 补一次推送/重新武装重试——白白放弃一次契约内安全的唤醒机会;(c) transport 直接抛错的推送也计入 3 次预算(3 次抛错 = 0 次真正送达)。建议与第 1 条一起设计(避免瞬态通知也被补推)。/ (a) After 3 attempts an idle session is never pushed again for that adapter's lifetime (honestly documented); (b) when a daemon replay of a still-pending message is dedupe-suppressed, no re-push/re-arm of the existing stable ID happens — a contract-safe wake is discarded; (c) a throwing transport write still consumes an attempt (3 throws = 0 delivered). Suggest designing together with item 1 so ephemeral notices don't gain free re-pushes.
  4. 跨进程 resume-ACK 竞态 / Cross-process resume-ACK race. ack_resume 落地时若 daemon 已有一条 resume 重推在途,该迟到兄弟会在本地收敛之后入箱,且没有自动收敛路径(adapter 对 resume 不武装重试、daemon 收到 ack 后停止重推)——它会一直留箱直到手动确认。建议:adapter 维护一个带 TTL 的"已确认 resumeId"记忆,迟到兄弟直接丢弃或入箱即收敛。/ If a daemon resume re-push is in flight when ack_resume lands, the late sibling is admitted after local retirement and nothing retires it automatically — it lingers until manually acked. Suggestion: a TTL'd memory of acked resumeIds in the adapter; drop or admit-then-retire late siblings.
  5. 源 ID 复用 + 相同内容被抑制 / Recycled source ID with identical content is suppressed. 去重键是源 ID + 内容指纹:如果上游真的复用了 item id 且字节级内容相同,一条真正的新消息会被当作重复静默抑制(源 ID 去重的固有边界,罕见)。/ Dedupe keys on source ID + content fingerprint: a genuinely new message that reuses an item id with byte-identical content is silently suppressed (inherent to source-ID dedupe; rare).
  6. 重启存活单测是 setter 模拟 / Restart-survival unit tests are setter simulations. daemon 重启/Codex 重连的"存活"单测通过重设 sender 模拟,真实进程边界由 docs/test-plans/issue-223-reliable-mailbox.md 的手工计划覆盖。若希望自动化,需要进程级集成测试。/ The daemon-restart/Codex-reconnect survival unit tests simulate via setter re-registration; real process boundaries are covered by the manual test plan. Process-level integration tests would be needed to automate them.

后续 / Future work: P2(跨 adapter 重启的持久化信箱)建议独立改动:daemon 侧带版本的存储、0700 目录/0600 文件、原子写入 + fsync、有界压缩、崩溃安全的 ACK 记录。/ P2 (persistence across adapter restarts) as a separate change: daemon-owned versioned store, 0700 dir / 0600 files, atomic write+fsync, bounded compaction, crash-safe ACK records.

Compatibility notes

  • get_messages 语义变化:从破坏性排空改为非破坏性 + 显式 ACK。所有随仓库分发的指令面已在本 PR 内迁移;已初始化项目需重跑 abg init 以刷新 CLAUDE.md/AGENTS.md 注入块。/ get_messages changes from destructive drain to non-destructive + explicit ACK. All in-repo instruction surfaces are migrated in this PR; already-initialized projects should re-run abg init to refresh their injected blocks.
  • Channel meta 新增字段(message_id 现为稳定投递 ID,新增 source_message_idack_requiredack_tool 等);AGENTBRIDGE_MODE 保持忽略 + 一次性警告。/ New Channel meta fields (message_id is now the stable delivery ID; adds source_message_id, ack_required, ack_tool); AGENTBRIDGE_MODE stays ignored with the one-time warning.

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the existing code style
  • I have added/updated tests as needed

Prepared with Claude Code (multi-agent adversarial review + live verification). 按仓库惯例欢迎 Codex 交叉 review。/ Cross-review per repo convention welcome.

🤖 Generated with Claude Code

…)/ reliable mailbox: acknowledged at-least-once Codex→Claude delivery (fixes raysonmeng#223)

核心改动 / Core changes:
- 入箱先于 Channel 推送;推送只是延迟优化 / queue synchronously before Channel push; push is a latency optimization
- 不可变投递代 ID(与源 ID 分离);get_messages 非破坏性、稳定有序 / immutable delivery IDs separate from source IDs; non-destructive stably-ordered get_messages
- 新增 ack_messages;get_messages 支持可选 ack_ids;ack_ids 上限跟随信箱容量 / new ack_messages tool; optional ack_ids on get_messages; ack cap follows mailbox capacity
- 未确认推送 FIFO 有界重试(默认 3 次,60s 起指数退避),ACK/逐出/耗尽取消定时器 / bounded FIFO retries (3 attempts, exponential from 60s), timers cancelled on ACK/eviction/exhaustion
- 源 ID 冲突指纹去重 + 碰撞别名;迟到 ACK 不误删复用 ID 的新消息 / source-ID fingerprint dedupe + collision aliases; late ACKs cannot delete newer reuses
- 边界:100 条 / 4 MiB UTF-8 / 2048 去重项 / 20 分钟 TTL,溢出与超大有可观察警告;超大消息 best-effort 推送并明确不可恢复(ack_required:false)/ bounds with observable warnings; oversized messages pushed best-effort with an honest not-retained preamble
- 系统与预算恢复消息共用信箱;ack_resume 收敛全部兄弟投递 / system + budget-resume messages share the mailbox; ack_resume retires all siblings
- 指令面全部迁移到新契约(MCP instructions、collaboration-content 注入块、CLAUDE.md/AGENTS.md、plugin README、marketplace、kickoff、双语 README)/ all instruction surfaces migrated to the new contract
- 新增 reliable-mailbox 单测套件与 E2E 测试计划;bundles 按 ADR 重建提交 / new unit suite + E2E test plan; bundles rebuilt per ADR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@KDRage

KDRage commented Jul 17, 2026

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@KDRage

KDRage commented Jul 17, 2026

Copy link
Copy Markdown
Author

关于红色的 CLA check / About the red CLA check

中文:CLA 已签署(见上方 bot 的 "All contributors have signed the CLA ✍️ ✅" 确认)。该 check 仍显示失败,是 workflow 的签名存储步骤在报错:cla.yml 只授予 contents: read,但配置了在仓库内存储签名(signatures/cla.json @ cla-signatures 分支),Action 因此报 Resource not accessible by integration,即使签名已被识别也会把 run 标为失败。修复需要给该 job 授予 contents: write(或改用外部签名存储);历史 PR 的 CLA run 也有相同的失败记录。

English: The CLA is signed (see the bot's "All contributors have signed the CLA ✍️ ✅" confirmation above). The check still shows failed because the workflow's signature-storage step errors out: cla.yml grants contents: read while being configured to store signatures in-repo (signatures/cla.json on the cla-signatures branch), so the action fails with Resource not accessible by integration even after recognizing the signature. Fixing it needs contents: write on that job (or external signature storage) — earlier PRs' CLA runs show the same failure pattern.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant