cross-agent-teams-mcp 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,12 +4,47 @@
4
4
 
5
5
  A local MCP daemon that lets multiple AI coding agents (Claude Code, Codex, opencode) running on the same machine talk to each other. Agents register, send 1-to-1 messages, broadcast to a team or role, and wake each other up — all over a single daemon, no external services.
6
6
 
7
- ## What's in the npm package
7
+ ## Quick start
8
8
 
9
- `cross-agent-teams-mcp` ships two bins from the same package:
9
+ ### Claude Code
10
10
 
11
- - **`cross-agent-teams-mcp daemon`** — the long-running HTTP daemon. Stores agents, mailboxes, and the task list in a local SQLite file, exposes its tools at `http://127.0.0.1:9100/mcp`.
12
- - **`cross-agent-teams-channel`** a stdio MCP shim that lets Claude Code receive `notifications/channel_wake` from the daemon (Claude Code's experimental channel capability). Required for Claude Code wake-ups; not needed for Codex (which uses its own app-server transport) or opencode (which falls back to tmux-pane injection).
11
+ ```bash
12
+ # 1. Start the daemon (run once, keep it alive)
13
+ npx -y cross-agent-teams-mcp@latest daemon --port 9100 &
14
+
15
+ # 2. In your project, install the MCP config
16
+ npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code
17
+
18
+ # 3. Start Claude Code with the channel loader (manual permission prompt expected)
19
+ claude --dangerously-load-development-channels server:cross-agent-teams-channel
20
+ ```
21
+
22
+ ### Other agents (Codex, opencode, ...)
23
+
24
+ ```bash
25
+ # 1. Start the daemon (run once, keep it alive)
26
+ npx -y cross-agent-teams-mcp@latest daemon --port 9100 &
27
+
28
+ # 2. In your project, install the MCP config (interactive picker)
29
+ npx mcpsmgr add jtianling/cross-agent-teams-mcp
30
+
31
+ # 3. Start your coding agent as usual
32
+ ```
33
+
34
+ Note: only Claude Code gets push wake out of the box. Codex needs the `--remote` + launcher setup (see section 2 below) for pokes; without it, it has a mailbox but no wake. opencode / cursor / other agents only receive pokes when running inside a tmux pane. If push wake isn't wired up, ask the agent to check its inbox manually ("check my xats inbox").
35
+
36
+ Then talk to your agent in plain language:
37
+
38
+ ```
39
+ # In agent A:
40
+ Register me to xats as backend on team default.
41
+
42
+ # In agent B:
43
+ Register me to xats as frontend on team default.
44
+ Send backend a message: the API has changed.
45
+ ```
46
+
47
+ That's it. Sections below cover the details — daemon flags, manual MCP config, codex `--remote` setup, more usage patterns.
13
48
 
14
49
  ## 1. Start the daemon
15
50
 
@@ -30,7 +65,21 @@ Common flags:
30
65
 
31
66
  ## 2. Configure your agent's MCP client
32
67
 
33
- ### Claude Code (needs both entries — HTTP for tools, stdio for channel wake)
68
+ ### Recommended: `mcpsmgr` (shown in Quick start)
69
+
70
+ [`mcpsmgr`](https://www.npmjs.com/package/mcpsmgr) reads this repo's `mcpsmgr.json` and writes the right MCP entries into your agent's config in one shot — including the Claude Code stdio channel proxy entry, the Codex `experimental_use_rmcp_client` toggle, and the streamable-http MCP entry.
71
+
72
+ To override the daemon port:
73
+
74
+ ```bash
75
+ npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code --port 9300
76
+ ```
77
+
78
+ ### Manual config
79
+
80
+ If you don't want `mcpsmgr` (private fork, custom token, custom stdio args, or you just prefer hand-edited config), the raw per-agent configs are below.
81
+
82
+ #### Claude Code (needs both entries — HTTP for tools, stdio for channel wake)
34
83
 
35
84
  `.mcp.json` (or `~/.claude.json`):
36
85
 
@@ -64,32 +113,92 @@ claude --dangerously-load-development-channels server:cross-agent-teams-channel
64
113
 
65
114
  The `server:<name>` suffix MUST equal the MCP server key in `.mcp.json` (`cross-agent-teams-channel` above). If your daemon uses `--token <t>`, add `"headers": { "Authorization": "Bearer <t>" }` to the HTTP entry.
66
115
 
67
- ### Codex CLI
116
+ #### Codex CLI
117
+
118
+ Codex talks to the daemon over Streamable HTTP. Wake-ups go through Codex's own app-server WebSocket transport — there is no channel proxy involved.
68
119
 
69
- Codex talks to the daemon directly over Streamable HTTP. No channel proxy is needed — Codex has no `claude/channel` capability, and wake-ups are delivered via Codex's own app-server websocket transport (or tmux paste fallback).
120
+ ##### Minimum config (mailbox only, no push wake)
70
121
 
71
122
  `~/.codex/config.toml`:
72
123
 
73
124
  ```toml
74
- [mcp_servers.cross-agent-teams]
125
+ experimental_use_rmcp_client = true
126
+
127
+ [mcp_servers.cross-agent-teams-mcp]
75
128
  type = "streamable-http"
76
129
  url = "http://127.0.0.1:9100/mcp"
77
130
  ```
78
131
 
79
- (daemon with `--token <t>`: add `[mcp_servers.cross-agent-teams.headers]` and `Authorization = "Bearer <t>"`.)
132
+ `experimental_use_rmcp_client = true` MUST sit at the top level — without it, streamable-http MCP servers fail to load.
133
+
134
+ (With `--token <t>` on the daemon: add `[mcp_servers.cross-agent-teams-mcp.headers]` and `Authorization = "Bearer <t>"`.)
80
135
 
81
- If you want other agents to be able to **wake** this Codex thread (not only mail it), start Codex's app-server alongside Codex itself:
136
+ In this minimum mode, `send_message` to this Codex still drops a row in its mailbox, but you have to call `get_inbox` yourself to read it — no push wake.
137
+
138
+ ##### Let other agents wake you (codex-appserver poke)
139
+
140
+ To let other agents **wake** this Codex thread (not just mail it), you need `codex-appserver` delivery. The setup has one non-obvious gotcha worth calling out:
141
+
142
+ > **In `codex --remote` mode, MCP servers are loaded by the app-server, NOT by the TUI.** The MCP entry above must therefore live in the `CODEX_HOME` that the **app-server** reads at startup — usually the global `~/.codex/config.toml`. Setting `CODEX_HOME` on the TUI alone does nothing for MCP under `--remote`.
143
+
144
+ Start order:
82
145
 
83
146
  ```bash
84
- codex app-server --listen ws://127.0.0.1:8799 # in one terminal
85
- codex --remote ws://127.0.0.1:8799 # in another terminal (TUI)
147
+ # 1) Long-lived codex app-server somewhere (its CODEX_HOME decides the MCP set).
148
+ codex app-server --listen ws://127.0.0.1:8799
149
+
150
+ # 2) Codex TUI in a separate terminal, connected to the same app-server.
151
+ codex --remote ws://127.0.0.1:8799
86
152
  ```
87
153
 
88
- Without an app-server, `send_message` to this Codex still queues a mailbox row, but you have to call `get_inbox` yourself to read it there is no push to wake the thread.
154
+ If the app-server's `CODEX_HOME` doesn't have `cross-agent-teams-mcp` configured, the codex agent inside `--remote` won't see the MCP tools at all and `register_agent` will never fire.
155
+
156
+ ##### Recommended: launcher with tmux pane auto-bind
157
+
158
+ For pokes to be injected directly into the running Codex thread (rather than landing as a tmux paste), the daemon needs to know which tmux pane the codex process lives in. The launcher pre-claims a pane via the `pre-register-codex-pane` CLI before exec'ing codex. Add to `~/.zshrc`:
159
+
160
+ ```zsh
161
+ free-xats-codex() {
162
+ local xats_agent_id codex_home search_dir
163
+ xats_agent_id="$(uuidgen)"
164
+ search_dir="$PWD"
165
+ while [[ "$search_dir" != "/" ]]; do
166
+ if [[ -f "$search_dir/.codex/config.toml" ]]; then
167
+ codex_home="$search_dir/.codex"
168
+ break
169
+ fi
170
+ search_dir="${search_dir:h}"
171
+ done
172
+
173
+ if [[ -n "$TMUX_PANE" ]]; then
174
+ npx -y cross-agent-teams-mcp pre-register-codex-pane \
175
+ --pane "$TMUX_PANE" \
176
+ --agent-id "$xats_agent_id" \
177
+ >/dev/null 2>&1 \
178
+ || echo "[xats] pre-register failed (continuing without pane claim)" >&2
179
+ fi
180
+
181
+ if [[ -n "$codex_home" ]]; then
182
+ CODEX_HOME="$codex_home" exec codex \
183
+ --remote ws://127.0.0.1:8799 \
184
+ -c xats.agent_id="\"$xats_agent_id\"" "$@"
185
+ else
186
+ exec codex \
187
+ --remote ws://127.0.0.1:8799 \
188
+ -c xats.agent_id="\"$xats_agent_id\"" "$@"
189
+ fi
190
+ }
191
+ ```
192
+
193
+ What the launcher does:
194
+
195
+ - Inside tmux (`$TMUX_PANE` set): pre-registers the pane → uuid mapping with the daemon (120s TTL). When the codex agent later calls `register_agent({agent_type: "codex", thread_id: $CODEX_THREAD_ID, ...})`, the daemon resolves `tmux_pane_id` automatically by matching the pre-reg against the codex argv.
196
+ - `--remote ws://127.0.0.1:8799` connects to the long-lived app-server from step (1) above.
197
+ - `-c xats.agent_id="\"$uuid\""` exposes the uuid in codex's argv so the daemon can verify the pane.
89
198
 
90
- Detailed config (auth headers, tmux fallback, lower-level `register_agent` form): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
199
+ More detail (auth headers, lower-level `register_agent` form): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
91
200
 
92
- ### Other coding agents (opencode, cursor, ...)
201
+ #### Other coding agents (opencode, cursor, ...)
93
202
 
94
203
  Anything that is not Claude Code or Codex — opencode, cursor, an editor extension, your own harness — connects over plain Streamable HTTP and registers as `agent_type="custom"` (the agent figures this out for you). There is no dedicated wake-up transport for these; cross-agent pokes are delivered by injecting text into the agent's tmux pane, so run the agent inside a tmux window and the daemon will resolve `pid → tty → pane` automatically when you register.
95
204
 
package/README.zh-CN.md CHANGED
@@ -4,12 +4,47 @@
4
4
 
5
5
  一个本地 MCP daemon, 让同一台机器上的多个 AI 编码 agent (Claude Code, Codex, opencode) 互相通信. agent 注册到 daemon, 互发 1-to-1 消息, 在 team 或 role 内广播, 互相唤醒 — 全部通过一个本地 daemon 完成, 不依赖任何外部服务.
6
6
 
7
- ## npm 包内容
7
+ ## 快速开始
8
8
 
9
- `cross-agent-teams-mcp` 在同一个包里发两个 bin:
9
+ ### Claude Code
10
10
 
11
- - **`cross-agent-teams-mcp daemon`** — 长驻 HTTP daemon. 把 agent 注册表和邮箱存在本地 SQLite 文件里, MCP endpoint 在 `http://127.0.0.1:9100/mcp`.
12
- - **`cross-agent-teams-channel`** stdio MCP shim, 让 Claude Code 通过 `notifications/channel_wake` 接收唤醒通知 (Claude Code 的 experimental channel capability). Claude Code 需要它接收 wake; Codex 用自己的 app-server 通道, opencode 走 tmux-pane 文本注入, 都不需要 channel proxy.
11
+ ```bash
12
+ # 1. 启动 daemon (跑一次, 保持运行)
13
+ npx -y cross-agent-teams-mcp@latest daemon --port 9100 &
14
+
15
+ # 2. 在你的项目下安装 MCP 配置
16
+ npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code
17
+
18
+ # 3. 带上 channel loader 启动 Claude Code (需要手动确认权限)
19
+ claude --dangerously-load-development-channels server:cross-agent-teams-channel
20
+ ```
21
+
22
+ ### 其它 agent (Codex, opencode, ...)
23
+
24
+ ```bash
25
+ # 1. 启动 daemon (跑一次, 保持运行)
26
+ npx -y cross-agent-teams-mcp@latest daemon --port 9100 &
27
+
28
+ # 2. 在你的项目下安装 MCP 配置 (交互式选择对应 agent)
29
+ npx mcpsmgr add jtianling/cross-agent-teams-mcp
30
+
31
+ # 3. 按平时的方式启动对应 coding agent
32
+ ```
33
+
34
+ 注意: 只有 Claude Code 默认就能收到 push 唤醒. Codex 需要 `--remote` + launcher 配置 (见下面 section 2) 才能被 poke; 没配的话只有邮箱, 不会自动醒. opencode / cursor 等其它 agent 只有跑在 tmux pane 里才能被 poke. 没接通 push 唤醒的情况下, 让 agent 自己手动收信即可 (跟它说"查一下我的 xats inbox").
35
+
36
+ 之后用平时跟 agent 对话的语言就能用了:
37
+
38
+ ```
39
+ # Agent A 里:
40
+ Register me to xats as backend on team default.
41
+
42
+ # Agent B 里:
43
+ Register me to xats as frontend on team default.
44
+ Send backend a message: the API has changed.
45
+ ```
46
+
47
+ 就这些. 下面是细节 — daemon 参数, 手动 MCP 配置, codex `--remote` 设置, 更多使用方式.
13
48
 
14
49
  ## 1. 启动 daemon
15
50
 
@@ -30,7 +65,21 @@ daemon 默认监听 `127.0.0.1:9100`. MCP endpoint: `http://127.0.0.1:9100/mcp`
30
65
 
31
66
  ## 2. 在 agent 端配置 MCP client
32
67
 
33
- ### Claude Code (两个条目都需要 — HTTP 用于工具, stdio 用于 channel 唤醒)
68
+ ### 推荐: `mcpsmgr` (快速开始里已经演示)
69
+
70
+ [`mcpsmgr`](https://www.npmjs.com/package/mcpsmgr) 读取本仓库的 `mcpsmgr.json`, 一次性把对应 agent 需要的 MCP 条目写进配置 — 包括 Claude Code 的 stdio channel proxy 条目, Codex 的 `experimental_use_rmcp_client` 开关和 streamable-http MCP 条目.
71
+
72
+ 覆盖 daemon 端口:
73
+
74
+ ```bash
75
+ npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code --port 9300
76
+ ```
77
+
78
+ ### 手动配置
79
+
80
+ 如果不想用 `mcpsmgr` (私有 fork / 自定义 token / 自定义 stdio args / 或者就是想手写), 各 agent 的原始配置如下.
81
+
82
+ #### Claude Code (两个条目都需要 — HTTP 用于工具, stdio 用于 channel 唤醒)
34
83
 
35
84
  `.mcp.json` (或 `~/.claude.json`):
36
85
 
@@ -64,32 +113,92 @@ claude --dangerously-load-development-channels server:cross-agent-teams-channel
64
113
 
65
114
  `server:<name>` 后缀 **必须** 等于 `.mcp.json` 里的 MCP server key (上例中是 `cross-agent-teams-channel`). 如果 daemon 启动带了 `--token <t>`, 在 HTTP 条目里加 `"headers": { "Authorization": "Bearer <t>" }`.
66
115
 
67
- ### Codex CLI
116
+ #### Codex CLI
117
+
118
+ Codex 通过 Streamable HTTP 跟 daemon 通信. 唤醒走 Codex 自己的 app-server WebSocket, 不经 channel proxy.
68
119
 
69
- Codex 直接通过 Streamable HTTP 跟 daemon 通信, 不需要 channel proxy — Codex 没有 `claude/channel` capability, 唤醒走 Codex 自己的 app-server websocket (或 tmux paste 兜底).
120
+ ##### 最小配置 (只能收邮箱, 没有 push 唤醒)
70
121
 
71
122
  `~/.codex/config.toml`:
72
123
 
73
124
  ```toml
74
- [mcp_servers.cross-agent-teams]
125
+ experimental_use_rmcp_client = true
126
+
127
+ [mcp_servers.cross-agent-teams-mcp]
75
128
  type = "streamable-http"
76
129
  url = "http://127.0.0.1:9100/mcp"
77
130
  ```
78
131
 
79
- (daemon 带了 `--token <t>` 时, 加 `[mcp_servers.cross-agent-teams.headers]` `Authorization = "Bearer <t>"`.)
132
+ `experimental_use_rmcp_client = true` 必须放在**顶级**, 缺这条 streamable-http MCP 加载不了.
133
+
134
+ (daemon 带了 `--token <t>` 时, 加 `[mcp_servers.cross-agent-teams-mcp.headers]` 和 `Authorization = "Bearer <t>"`.)
80
135
 
81
- 如果你希望别的 agent 能**唤醒**这个 Codex thread (不只是给它发邮件), 在跑 Codex 之前把 codex app-server 一起拉起来:
136
+ 这种最小配置下 `send_message` 给这个 codex 会写邮箱, 但需要手动调 `get_inbox` 拉读, 没有跨会话 push 唤醒.
137
+
138
+ ##### 让别人能唤醒你 (codex-appserver poke)
139
+
140
+ 要让别的 agent 能**主动唤醒**这个 codex thread (而不只是发邮件), 需要 `codex-appserver` delivery. 这里有个不直观的坑要写清楚:
141
+
142
+ > **`codex --remote` 模式下, MCP server 是 app-server 加载的, 不是 TUI 加载的**. 上面那段 MCP 配置必须放在 **app-server** 启动时读到的 `CODEX_HOME` 里 — 一般就是全局 `~/.codex/config.toml`. 仅在 TUI 这边设 `CODEX_HOME` 在 `--remote` 模式下对 MCP 不起作用.
143
+
144
+ 启动顺序:
82
145
 
83
146
  ```bash
84
- codex app-server --listen ws://127.0.0.1:8799 # 一个终端
85
- codex --remote ws://127.0.0.1:8799 # 另一个终端 (TUI)
147
+ # 1) 在某个长跑终端起 codex app-server (它的 CODEX_HOME 决定 MCP set)
148
+ codex app-server --listen ws://127.0.0.1:8799
149
+
150
+ # 2) 在另一个终端启动 codex TUI, 连同一个 app-server
151
+ codex --remote ws://127.0.0.1:8799
86
152
  ```
87
153
 
88
- 不启 app-server 也能用 `send_message` 给这个 Codex 仍然会写到邮箱, 但需要你自己调 `get_inbox` 拉读, 没有推送唤醒.
154
+ 如果第 1 步的 app-server `CODEX_HOME` 里没配 `cross-agent-teams-mcp`, `--remote` 进去的 codex agent 根本看不到 MCP 工具, `register_agent` 调都调不到.
155
+
156
+ ##### 推荐: launcher 函数 (tmux pane 自动绑定)
157
+
158
+ 为了让 daemon 把 wake-hint 直接 inject 到 codex thread (而不是只 paste 到 tmux pane), daemon 需要知道 codex 进程在哪个 tmux pane. launcher 通过 `pre-register-codex-pane` CLI 在 exec codex 之前先把 pane 占住. 把下面的函数加到 `~/.zshrc`:
159
+
160
+ ```zsh
161
+ free-xats-codex() {
162
+ local xats_agent_id codex_home search_dir
163
+ xats_agent_id="$(uuidgen)"
164
+ search_dir="$PWD"
165
+ while [[ "$search_dir" != "/" ]]; do
166
+ if [[ -f "$search_dir/.codex/config.toml" ]]; then
167
+ codex_home="$search_dir/.codex"
168
+ break
169
+ fi
170
+ search_dir="${search_dir:h}"
171
+ done
172
+
173
+ if [[ -n "$TMUX_PANE" ]]; then
174
+ npx -y cross-agent-teams-mcp pre-register-codex-pane \
175
+ --pane "$TMUX_PANE" \
176
+ --agent-id "$xats_agent_id" \
177
+ >/dev/null 2>&1 \
178
+ || echo "[xats] pre-register failed (continuing without pane claim)" >&2
179
+ fi
180
+
181
+ if [[ -n "$codex_home" ]]; then
182
+ CODEX_HOME="$codex_home" exec codex \
183
+ --remote ws://127.0.0.1:8799 \
184
+ -c xats.agent_id="\"$xats_agent_id\"" "$@"
185
+ else
186
+ exec codex \
187
+ --remote ws://127.0.0.1:8799 \
188
+ -c xats.agent_id="\"$xats_agent_id\"" "$@"
189
+ fi
190
+ }
191
+ ```
192
+
193
+ 行为:
194
+
195
+ - tmux 内 (`$TMUX_PANE` 非空): 先发一条 pre-register (pane_id + UUID + 120s TTL) 给 daemon. codex agent 之后调 `register_agent({agent_type: "codex", thread_id: $CODEX_THREAD_ID, ...})` 时, daemon 会用 pending pre-reg + 匹配 codex 进程 argv 自动绑 `tmux_pane_id`.
196
+ - `--remote ws://127.0.0.1:8799` 让 codex 连步骤 (1) 起好的 app-server.
197
+ - `-c xats.agent_id="\"$uuid\""` 把 UUID 暴露在 codex argv 里, daemon 用它反向校验 pane.
89
198
 
90
- 详细配置 (auth header, tmux fallback, 底层 `register_agent` 用法): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
199
+ 详细配置 (auth header, 底层 `register_agent` 用法): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
91
200
 
92
- ### 其它编码 agent (opencode, cursor, ...)
201
+ #### 其它编码 agent (opencode, cursor, ...)
93
202
 
94
203
  非 Claude Code 也非 Codex 的工具 — opencode, cursor, 编辑器扩展, 自己的 harness — 直接通过 Streamable HTTP 连 daemon, 注册时用 `agent_type="custom"` (agent 自己会判断). 这些 agent 没有专用的唤醒通道; 跨 agent poke 通过把文本注入到 agent 所在的 tmux pane 实现, 所以把 agent 跑在 tmux 窗口里, 注册时 daemon 会自动解析 `pid → tty → pane`.
95
204
 
package/dist/cli.js CHANGED
@@ -388,6 +388,7 @@ function toAgentRow(row) {
388
388
  var AgentsRepo = class {
389
389
  constructor(db) {
390
390
  this.db = db;
391
+ this.list = this.list.bind(this);
391
392
  }
392
393
  db;
393
394
  findByIdentity(args) {
@@ -525,8 +526,8 @@ var AgentsRepo = class {
525
526
  );
526
527
  }
527
528
  list(args) {
528
- const rows = this.db.prepare(
529
- `SELECT
529
+ const exclude = args.excludeRoles ?? [];
530
+ const baseSelect = `SELECT
530
531
  agent_id,
531
532
  agent_type,
532
533
  agent_type_name,
@@ -539,9 +540,17 @@ var AgentsRepo = class {
539
540
  delivery_payload,
540
541
  last_seen_at
541
542
  FROM agents
542
- WHERE team=?
543
- ORDER BY registered_at ASC`
544
- ).all(args.team);
543
+ WHERE team=?`;
544
+ const orderBy = ` ORDER BY registered_at ASC`;
545
+ let rows;
546
+ if (exclude.length > 0) {
547
+ const placeholders = exclude.map(() => "?").join(",");
548
+ rows = this.db.prepare(
549
+ `${baseSelect} AND role NOT IN (${placeholders})${orderBy}`
550
+ ).all(args.team, ...exclude);
551
+ } else {
552
+ rows = this.db.prepare(`${baseSelect}${orderBy}`).all(args.team);
553
+ }
545
554
  const nowMs = Date.now();
546
555
  return rows.map((row) => {
547
556
  const agent = toAgentRow(row);
@@ -3642,7 +3651,7 @@ function registerBusinessTools(server, db, getCallerAgentId, fanout, onRegisterS
3642
3651
  if (typeof who !== "string") return toText(who);
3643
3652
  const row = agents.findById(who);
3644
3653
  return run(() => ({
3645
- agents: agents.list({ team: row.team }).map(toPublicAgentRow)
3654
+ agents: agents.list({ team: row.team, excludeRoles: ["__channel_proxy__"] }).map(toPublicAgentRow)
3646
3655
  }));
3647
3656
  }
3648
3657
  );
@@ -4249,11 +4258,24 @@ function runCleanup(db, opts = {}) {
4249
4258
  );
4250
4259
  const deleteMessages = db.prepare(`DELETE FROM messages WHERE sent_at < ?`);
4251
4260
  const deleteEvents = db.prepare(`DELETE FROM events WHERE created_at < ?`);
4261
+ const deleteStaleProxies = db.prepare(
4262
+ `DELETE FROM agents
4263
+ WHERE role = '__channel_proxy__'
4264
+ AND last_seen_at < ?
4265
+ AND NOT EXISTS (
4266
+ SELECT 1 FROM agents host
4267
+ WHERE host.delivery_kind = 'claude-channel'
4268
+ AND host.role <> '__channel_proxy__'
4269
+ AND json_extract(host.delivery_payload, '$.channel_session_id')
4270
+ = json_extract(agents.delivery_payload, '$.channel_session_id')
4271
+ )`
4272
+ );
4252
4273
  const tx = db.transaction(() => {
4253
4274
  const s = deleteStatus.run(ageCutoff);
4254
4275
  const m = deleteMessages.run(ageCutoff);
4255
4276
  const e = deleteEvents.run(ageCutoff);
4256
- return Number(s.changes) + Number(m.changes) + Number(e.changes);
4277
+ const p = deleteStaleProxies.run(ageCutoff);
4278
+ return Number(s.changes) + Number(m.changes) + Number(e.changes) + Number(p.changes);
4257
4279
  });
4258
4280
  return { deleted: tx() };
4259
4281
  }