Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"plugins": [
{
"name": "agentbridge",
"description": "Bridge Claude Code and Codex through a shared daemon, push channel delivery, and reply/get_messages tools.",
"description": "Bridge Claude Code and Codex through a shared daemon, acknowledged channel delivery, and reply/get_messages/ack_messages tools.",
"version": "0.1.30",
"author": {
"name": "AgentBridge Contributors",
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

You are working in a **multi-agent environment** powered by AgentBridge.
Another AI agent (Claude, by Anthropic) is available in a parallel session on this machine.
Communication happens via AgentBridge MCP tools — Claude has `reply` and `get_messages` tools.
Communication happens via AgentBridge MCP tools — Claude has `reply`, `get_messages`, and `ack_messages` tools.

### When to collaborate vs. work solo
- **Collaborate** when the task benefits from a second perspective, parallel execution, or capabilities the other agent has.
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Claude Code ── MCP stdio ──▶ bridge.ts (foreground)
- **`src/bridge.ts`** — foreground MCP server registered as a Claude Code plugin channel. Exits when Claude Code closes.
- **`src/daemon.ts`** — long-lived background process; owns the Codex app-server proxy and the single source of truth for bridge state. Survives Claude Code restarts; `bridge.ts` reconnects with exponential backoff.
- **`src/control-protocol.ts`** — message schema for the control WebSocket between foreground and daemon.
- **`src/claude-adapter.ts`** — MCP tool surface exposed to Claude (`reply`, `get_messages`). Emits `notifications/claude/channel` on inbound messages (push mode).
- **`src/claude-adapter.ts`** — MCP tool surface exposed to Claude (`reply`, `get_messages`, `ack_messages`). Emits `notifications/claude/channel` on inbound messages (push mode) over an acknowledged in-memory mailbox.
- **`src/codex-adapter.ts`** — WebSocket proxy in front of Codex app-server; intercepts `agentMessage` items and injects turns via `turn/start`.
- **`src/message-filter.ts`** — collapses noisy intermediate events so only meaningful `agentMessage` payloads reach Claude.
- **`src/daemon-lifecycle.ts`** — shared `ensureRunning` / `kill` / startup-lock logic; both the CLI and `bridge.ts` call into this.
Expand All @@ -66,7 +66,7 @@ Claude Code ── MCP stdio ──▶ bridge.ts (foreground)
### Data flow invariants

- Every `BridgeMessage` carries a `source: "claude" | "codex"` — the bridge **never forwards a message back to its origin** (loop prevention).
- Message delivery is always push (channel notifications). A failed push falls back to an in-memory queue drained by `get_messages`. (The legacy `AGENTBRIDGE_MODE=pull` mode was removed; the env var is ignored with a one-time warning.)
- Message delivery is push (channel notifications) over an authoritative in-memory mailbox: every admissible message is queued **before** its Channel push, `get_messages` re-reads pending messages non-destructively under stable delivery IDs, and only `ack_messages` removes them (bounded retries re-push unacknowledged messages). (The legacy `AGENTBRIDGE_MODE=pull` mode was removed; the env var is ignored with a one-time warning.)
- Ports are allocated per pair from a registry (slot-based, +10 strides from the base 4500/4501/4502); the legacy fixed defaults remain the slot-0 values. Multiple pairs run side-by-side, one per project directory.
- All state lives in the platform state dir (`AGENTBRIDGE_STATE_DIR`, default `~/Library/Application Support/AgentBridge/` on macOS, `$XDG_STATE_HOME/agentbridge/` on Linux). The daemon uses `startup.lock` + `killed` sentinel to coordinate startup and explicit-kill-don't-restart semantics.

Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ What that buys you, concretely:
## Features

- **Bidirectional Claude ↔ Codex messaging** in one working session — the daemon intercepts Codex output and pushes it to Claude as channel notifications; Claude replies via the `reply` MCP tool, and the bridge injects the reply into the Codex thread as a `turn/start`.
- **Push delivery with fallback** — messages arrive as channel notifications; a failed push falls back to an in-memory queue drained by `get_messages`. Loop prevention via the per-message `source` field.
- **Acknowledged at-least-once delivery** - every Codex message enters a bounded in-memory mailbox before its Channel push. Each admission gets an immutable delivery-generation ID, separate from the daemon source ID, so a delayed ACK cannot delete a newer message. Claude acknowledges processed IDs with `ack_messages`; missed pushes remain visible through non-destructive `get_messages` polling, and unacknowledged pushes receive bounded FIFO retries. Loop prevention uses the per-message `source` field.
- **Turn coordination** — a busy-guard rejects replies during an active Codex turn; a per-turn inactivity watchdog stops a lost `turn/completed` from locking injection forever; noisy intermediate events are collapsed so only meaningful `agentMessage` payloads reach Claude.
- **Multiple pairs side by side** — one Claude+Codex pair per project directory, ports allocated per pair in +10 strides from 4500. Pair-aware `claude` / `codex` / `resume` / `kill` / `doctor` / `budget` via `--pair`.
- **Resilient lifecycle** — a persistent background daemon survives Claude Code restarts (auto-reconnect with backoff); orphan-process cleanup; `abg doctor` read-only diagnostics; `abg pairs prune` reclaims stranded state.
Expand Down Expand Up @@ -249,6 +249,10 @@ The config is loaded by the CLI and daemon at startup. Re-running `init` is idem
| `AGENTBRIDGE_CODEX_TRANSPORT` | `auto` | How the daemon reaches the Codex app-server: `auto` (probe `codex app-server --help`, use `ws://` if supported else fall back to a `unix://` socket via a transparent relay), `ws` (force ws), or `unix` (force unix socket + relay). For builds that drop `ws://` listen support (issue #85) |
| `AGENTBRIDGE_STATE_DIR` | Platform default | State directory for pid, status, logs (macOS: `~/Library/Application Support/agentbridge/`, Linux: `$XDG_STATE_HOME/agentbridge/`) |
| `AGENTBRIDGE_DAEMON_ENTRY` | `./daemon.ts` | Override daemon entry point (used by plugin bundles) |
| `AGENTBRIDGE_MAX_BUFFERED_MESSAGES` | `100` | Maximum unacknowledged messages in the Claude adapter mailbox; overflow evicts the oldest entry with an observable warning |
| `AGENTBRIDGE_MAX_BUFFERED_BYTES` | `4194304` | Maximum UTF-8 content bytes in the mailbox; a single larger message is omitted with an observable warning |
| `AGENTBRIDGE_DELIVERY_RETRY_BASE_MS` | `60000` | Delay before retrying an unacknowledged Channel push; later retries use exponential backoff |
| `AGENTBRIDGE_DELIVERY_MAX_ATTEMPTS` | `3` | Total Channel attempts per ordinary message, including the initial push; exhausted messages remain available through `get_messages` |
| `NO_UPDATE_NOTIFIER` | unset | Set to any value to disable the "update available" notice (ecosystem-standard opt-out) |
| `AGENTBRIDGE_NO_UPDATE_NOTIFIER` | unset | Namespaced opt-out for the update notice (same effect as `NO_UPDATE_NOTIFIER`) |
| `AGENTBRIDGE_UPDATE_PROMPT` | unset | Set to `0` to disable the interactive update prompt and keep pure notice-only behavior |
Expand Down Expand Up @@ -282,6 +286,9 @@ AgentBridge can keep a long task moving across subscription-quota windows instea
## Current Limitations

- Only forwards `agentMessage` items, not intermediate `commandExecution`, `fileChange`, or similar events
- The acknowledged mailbox is in the Claude adapter process and is not persisted. It survives daemon or Codex reconnects while that adapter stays alive, but a Claude adapter/plugin process restart loses unacknowledged entries. This is not crash-durable delivery.
- Channel retry improves latency and recovery probability but cannot guarantee that a fully idle Claude session wakes automatically. `get_messages` is the recovery path once Claude is active.
- Delivery is at least once, not exactly once. Claude must finish processing a stable message ID before acknowledging it; a lost acknowledgement can cause a bounded redelivery.
- Single Codex thread per pair, no multi-session support within a pair yet
- Single Claude foreground connection per pair; a new Claude session replaces the previous one
- Multiple pairs run side-by-side on one machine (one per project directory); Windows is not an officially supported platform yet
Expand Down
12 changes: 11 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ English version: [README.md](README.md)
## 功能

- **Claude ↔ Codex 双向消息**(同一工作会话):拦截 Codex 输出并以 channel 通知推给 Claude;Claude 用 `reply` MCP tool 回复,作为 `turn/start` 注入 Codex thread。
- **Push 投递 + 兜底**:消息以 channel 通知投递;推送失败回退到内存队列,由 `get_messages` 排空。靠每条消息的 `source` 字段防循环。
- **确认制 at-least-once 投递**:每条 Codex 消息在 Channel 推送前先进入有界内存信箱;每次入箱分配一个不可变的投递代 ID(与 daemon 源 ID 分离),迟到的 ACK 不会误删复用同一源 ID 的新消息。Claude 用 `ack_messages` 确认已处理的 ID;漏掉的推送可通过非破坏性的 `get_messages` 轮询看到,未确认的推送按 FIFO 有界重试。靠每条消息的 `source` 字段防循环。
- **回合协调**:busy-guard 在 Codex 活跃 turn 期间拒绝回复;单 turn 非活动看门狗避免丢失 `turn/completed` 永久锁死注入;折叠噪声中间事件,只把有意义的 `agentMessage` 送达 Claude。
- **多对并行**:每个项目目录一对 Claude+Codex,端口按 +10 步长从 4500 分配;`claude` / `codex` / `resume` / `kill` / `doctor` / `budget` 支持 `--pair` 指定。
- **韧性生命周期**:常驻后台 daemon 跨 Claude Code 重启存活(指数退避自动重连);孤儿进程清理;`abg doctor` 只读诊断;`abg pairs prune` 回收滞留状态。
Expand Down Expand Up @@ -241,6 +241,10 @@ CLI 和 daemon 启动时会加载该配置。重复运行 `init` 是幂等的,

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `AGENTBRIDGE_MAX_BUFFERED_MESSAGES` | `100` | Claude adapter 内存信箱最多保留的未确认消息数;溢出时逐出最旧条目并给出可观察的警告 |
| `AGENTBRIDGE_MAX_BUFFERED_BYTES` | `4194304` | 信箱保留内容的 UTF-8 字节上限;单条超限消息被省略并给出可观察的警告 |
| `AGENTBRIDGE_DELIVERY_RETRY_BASE_MS` | `60000` | 未确认 Channel 推送的首次重试延迟;后续重试按指数退避 |
| `AGENTBRIDGE_DELIVERY_MAX_ATTEMPTS` | `3` | 每条普通消息的 Channel 总尝试次数(含首推);用尽后消息仍可通过 `get_messages` 获取 |
| `CODEX_WS_PORT` | `4500` | Codex app-server WebSocket 端口 |
| `CODEX_PROXY_PORT` | `4501` | Bridge 代理端口,Codex TUI 连接此端口 |
| `AGENTBRIDGE_CONTROL_PORT` | `4502` | bridge.ts 与 daemon.ts 之间的控制端口 |
Expand Down Expand Up @@ -288,6 +292,12 @@ AgentBridge 能让长任务跨订阅额度窗口持续推进,而不是某一

休眠/禁用状态、Codex `.git` 限制及其它坑,见 **[排错文档](docs/TROUBLESHOOTING.md)**。

### 投递可靠性边界

每条 Codex 消息都会在 Channel 推送前入箱,并获得一个不可变的投递代 ID。`get_messages` 以稳定顺序重复返回未确认消息;`ack_messages` 只移除请求中已处理的 ID。这是 Claude adapter 存活期间的有界 at-least-once 投递。投递语义是 at-least-once 而非 exactly-once:Claude 应在处理完成后再确认对应的稳定 ID;确认丢失可能造成有界的重复投递。

信箱不持久化:Claude adapter / 插件进程重启会丢失未确认条目。Channel 重试也无法保证完全空闲的 Claude 会话被自动唤醒。溢出与超大消息的行为可观察,但会按配置边界逐出内容。

## Roadmap

- **更多 adapter**:今天 AgentBridge 接的是 Claude Code ↔ Codex。下一个候选:**OpenCode、OpenClaw、Hermes Agent、Gemini CLI**。到 [adapter roadmap issue](https://github.com/raysonmeng/agent-bridge/issues/212) 投票。
Expand Down
84 changes: 84 additions & 0 deletions docs/test-plans/issue-223-reliable-mailbox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Issue 223 reliable mailbox test plan

This plan validates Codex-to-Claude delivery without weakening normal Claude or Codex permission controls. It covers the in-memory P0/P1 mailbox implemented for issue 223. It does not claim crash durability or guaranteed idle wake-up.

## Safety setup

Use a disposable Git repository with no production files. Build source and committed plugin artifacts with the pinned Bun version, then verify synchronization:

```bash
bun install --frozen-lockfile --ignore-scripts
bun run typecheck
bun run build:cli
bun run build:plugin
bun run verify:plugin-sync
```

Launch both native subscription-authenticated CLIs with safe controls:

```bash
AGENTBRIDGE_SAFE=1 bun dist/cli.js --pair issue223 claude \
--safe --permission-mode manual

AGENTBRIDGE_SAFE=1 bun dist/cli.js --pair issue223 codex \
--safe --new --sandbox read-only --ask-for-approval untrusted --no-alt-screen
```

Approve only the specific AgentBridge MCP calls under test. No API key is required.

## Deterministic suite

Run:

```bash
bun test src/unit-test/reliable-mailbox.test.ts
```

The suite must cover:

- A resolved Channel write with no consumer, followed by two identical `get_messages` snapshots.
- Successful Channel processing followed by `ack_messages`, with no later pull or retry duplicate.
- A throwing Channel write that leaves exactly one mailbox entry.
- Partial and repeated polling with deletion limited to requested immutable delivery IDs.
- Both lost-ACK directions: no committed ACK remains recoverable; a committed ACK with a lost response is idempotent when repeated.
- Adapter restart loss and same-adapter daemon, Claude Channel, and Codex reconnect survival.
- Rapid arrivals, ACK concurrent with arrival, source-ID conflicts, TTL/capacity reuse, collision aliases, multiple adapters, and FIFO retry order.
- Count and UTF-8 byte limits, oversized messages, overflow, ordinary system notices, budget-resume aliases, retry exhaustion, and invalid ACK input.

## Live idle recovery

Leave Claude idle at its prompt. Ask Codex to return a unique one-line sentinel. If Claude does not visibly wake, activate Claude manually and ask it to call `get_messages` twice without acknowledging.

Pass criteria:

1. Both polls contain the sentinel under the same delivery-generation ID.
2. `ack_messages` for that exact ID succeeds after processing.
3. A later poll no longer contains the sentinel.

If Claude wakes and processes the initial Channel push, that round tests successful push delivery rather than the idle-drop recovery path. Repeat with a new sentinel. If every push wakes Claude, record the live idle-drop result as inconclusive and retain the deterministic silent-consumer simulation as the proof for P0.

## Live Channel acknowledgement

While Claude is active, send a unique Codex sentinel. The Channel body and metadata expose the immutable delivery ID and direct Claude to call `ack_messages`. Claude must acknowledge without first discovering the ID through `get_messages`. A later poll must not return that sentinel.

## Restart boundaries

Leave a message unacknowledged and test each boundary separately:

- Restart only the daemon, retaining the same Claude adapter process: the adapter mailbox remains.
- Reconnect Codex, retaining the same Claude adapter: completed replies already in the mailbox remain.
- Reconnect the Claude Channel through the same adapter process: the mailbox remains.
- Exit and relaunch the Claude plugin/adapter process: the mailbox is lost. This demonstrates that P2 is not implemented.

Do not infer crash durability from a daemon-only restart because the authoritative mailbox lives in the Claude adapter process.

## Cleanup

Exit Codex and Claude, then stop and remove only the disposable pair:

```bash
AGENTBRIDGE_SAFE=1 bun dist/cli.js --pair issue223 kill
bun dist/cli.js pairs rm issue223
```

Remove any local development marketplace/plugin registration only if this test created it and no previous AgentBridge installation needs to be restored.
5 changes: 3 additions & 2 deletions plugins/agentbridge/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# AgentBridge Plugin

Claude Code plugin for AgentBridge. This plugin packages the AgentBridge MCP frontend with push channel delivery (a failed push falls back to an in-memory queue drained by `get_messages`), the `/agentbridge:init` command, and a non-blocking SessionStart health check.
Claude Code plugin for AgentBridge. This plugin packages the AgentBridge MCP frontend with acknowledged Channel delivery, the `/agentbridge:init` command, and a non-blocking SessionStart health check. Every admissible message is queued before push, receives an immutable delivery-generation ID, and remains available through `get_messages` until Claude confirms that ID with `ack_messages` (a message exceeding the mailbox size bound is not retained — it is delivered best-effort only and omitted with an observable warning).

## Structure

Expand Down Expand Up @@ -38,6 +38,7 @@ This creates self-contained bundles at:
## Notes

- The plugin frontend launches the sibling daemon bundle via `AGENTBRIDGE_DAEMON_ENTRY=./daemon.js`.
- Claude delivery is always push notifications. If a push fails, the message is queued and can be drained via `get_messages` (per-message fallback — the legacy `AGENTBRIDGE_MODE=pull` mode was removed and the env var is ignored with a one-time warning).
- Claude delivery uses Channel push as a latency optimization over a bounded in-memory mailbox. `get_messages` repeats unacknowledged stable IDs without deleting them; `ack_messages` removes only IDs Claude has finished processing. Ordinary unacknowledged pushes retry twice by default with exponential backoff. The legacy `AGENTBRIDGE_MODE=pull` value remains ignored with a one-time warning.
- Mailbox state is not persisted. A Claude adapter/plugin process restart loses it, and Channel cannot guarantee that a fully idle Claude session wakes automatically. Delivery is at least once while the adapter is alive, not exactly once or crash durable.
- The SessionStart hook is informational only. It never starts or stops the daemon.
- The command at `/agentbridge:init` edits project-local `.agentbridge/` files only; plugin installation and marketplace registration remain terminal-side tasks (`agentbridge init` / `agentbridge dev`).
Loading
Loading