cross-agent-teams-mcp 0.5.2 → 0.5.4
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 +100 -11
- package/README.zh-CN.md +100 -11
- package/dist/cli.js +66 -44
- package/dist/cli.js.map +1 -1
- package/package.json +8 -3
- package/src/daemon/cleanup.ts +21 -26
- package/src/mcp/get-inbox.ts +42 -23
- package/src/mcp/tools.ts +7 -1
- package/src/storage/agents-repo.ts +10 -2
- package/src/storage/schema.ts +15 -0
package/README.md
CHANGED
|
@@ -30,7 +30,36 @@ Common flags:
|
|
|
30
30
|
|
|
31
31
|
## 2. Configure your agent's MCP client
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### Recommended: install via `mcpsmgr`
|
|
34
|
+
|
|
35
|
+
The fastest path is the [`mcpsmgr`](https://www.npmjs.com/package/mcpsmgr) CLI. It reads this repo's manifest (`mcpsmgr.json`) and writes the right MCP entries (and any required stdio proxy entries) into your agent's config in one shot.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd <your-project>
|
|
39
|
+
|
|
40
|
+
# Pick one or run the bare command for an interactive picker.
|
|
41
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code
|
|
42
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a codex
|
|
43
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp # interactive
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
What it does:
|
|
47
|
+
|
|
48
|
+
- For Claude Code, writes both `.mcp.json` entries (the HTTP server AND the `cross-agent-teams-channel` stdio proxy) — you don't have to know there are two.
|
|
49
|
+
- For Codex, writes `~/.codex/config.toml` with `experimental_use_rmcp_client = true` plus the streamable-http MCP entry.
|
|
50
|
+
- Prints the post-install steps you still need to run yourself (e.g. the `--dangerously-load-development-channels server:cross-agent-teams-channel` flag for Claude Code, or the codex `--remote` setup if you want push wake).
|
|
51
|
+
|
|
52
|
+
Override the daemon port:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code --port 9300
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Manual config
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
|
|
62
|
+
#### Claude Code (needs both entries — HTTP for tools, stdio for channel wake)
|
|
34
63
|
|
|
35
64
|
`.mcp.json` (or `~/.claude.json`):
|
|
36
65
|
|
|
@@ -64,32 +93,92 @@ claude --dangerously-load-development-channels server:cross-agent-teams-channel
|
|
|
64
93
|
|
|
65
94
|
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
95
|
|
|
67
|
-
|
|
96
|
+
#### Codex CLI
|
|
97
|
+
|
|
98
|
+
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
99
|
|
|
69
|
-
|
|
100
|
+
##### Minimum config (mailbox only, no push wake)
|
|
70
101
|
|
|
71
102
|
`~/.codex/config.toml`:
|
|
72
103
|
|
|
73
104
|
```toml
|
|
74
|
-
|
|
105
|
+
experimental_use_rmcp_client = true
|
|
106
|
+
|
|
107
|
+
[mcp_servers.cross-agent-teams-mcp]
|
|
75
108
|
type = "streamable-http"
|
|
76
109
|
url = "http://127.0.0.1:9100/mcp"
|
|
77
110
|
```
|
|
78
111
|
|
|
79
|
-
|
|
112
|
+
`experimental_use_rmcp_client = true` MUST sit at the top level — without it, streamable-http MCP servers fail to load.
|
|
113
|
+
|
|
114
|
+
(With `--token <t>` on the daemon: add `[mcp_servers.cross-agent-teams-mcp.headers]` and `Authorization = "Bearer <t>"`.)
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
##### Let other agents wake you (codex-appserver poke)
|
|
119
|
+
|
|
120
|
+
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:
|
|
80
121
|
|
|
81
|
-
|
|
122
|
+
> **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`.
|
|
123
|
+
|
|
124
|
+
Start order:
|
|
82
125
|
|
|
83
126
|
```bash
|
|
84
|
-
codex app-server
|
|
85
|
-
codex --
|
|
127
|
+
# 1) Long-lived codex app-server somewhere (its CODEX_HOME decides the MCP set).
|
|
128
|
+
codex app-server --listen ws://127.0.0.1:8799
|
|
129
|
+
|
|
130
|
+
# 2) Codex TUI in a separate terminal, connected to the same app-server.
|
|
131
|
+
codex --remote ws://127.0.0.1:8799
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
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.
|
|
135
|
+
|
|
136
|
+
##### Recommended: launcher with tmux pane auto-bind
|
|
137
|
+
|
|
138
|
+
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`:
|
|
139
|
+
|
|
140
|
+
```zsh
|
|
141
|
+
free-xats-codex() {
|
|
142
|
+
local xats_agent_id codex_home search_dir
|
|
143
|
+
xats_agent_id="$(uuidgen)"
|
|
144
|
+
search_dir="$PWD"
|
|
145
|
+
while [[ "$search_dir" != "/" ]]; do
|
|
146
|
+
if [[ -f "$search_dir/.codex/config.toml" ]]; then
|
|
147
|
+
codex_home="$search_dir/.codex"
|
|
148
|
+
break
|
|
149
|
+
fi
|
|
150
|
+
search_dir="${search_dir:h}"
|
|
151
|
+
done
|
|
152
|
+
|
|
153
|
+
if [[ -n "$TMUX_PANE" ]]; then
|
|
154
|
+
npx -y cross-agent-teams-mcp pre-register-codex-pane \
|
|
155
|
+
--pane "$TMUX_PANE" \
|
|
156
|
+
--agent-id "$xats_agent_id" \
|
|
157
|
+
>/dev/null 2>&1 \
|
|
158
|
+
|| echo "[xats] pre-register failed (continuing without pane claim)" >&2
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if [[ -n "$codex_home" ]]; then
|
|
162
|
+
CODEX_HOME="$codex_home" exec codex \
|
|
163
|
+
--remote ws://127.0.0.1:8799 \
|
|
164
|
+
-c xats.agent_id="\"$xats_agent_id\"" "$@"
|
|
165
|
+
else
|
|
166
|
+
exec codex \
|
|
167
|
+
--remote ws://127.0.0.1:8799 \
|
|
168
|
+
-c xats.agent_id="\"$xats_agent_id\"" "$@"
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
86
171
|
```
|
|
87
172
|
|
|
88
|
-
|
|
173
|
+
What the launcher does:
|
|
174
|
+
|
|
175
|
+
- 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.
|
|
176
|
+
- `--remote ws://127.0.0.1:8799` connects to the long-lived app-server from step (1) above.
|
|
177
|
+
- `-c xats.agent_id="\"$uuid\""` exposes the uuid in codex's argv so the daemon can verify the pane.
|
|
89
178
|
|
|
90
|
-
|
|
179
|
+
More detail (auth headers, lower-level `register_agent` form): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
|
|
91
180
|
|
|
92
|
-
|
|
181
|
+
#### Other coding agents (opencode, cursor, ...)
|
|
93
182
|
|
|
94
183
|
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
184
|
|
package/README.zh-CN.md
CHANGED
|
@@ -30,7 +30,36 @@ daemon 默认监听 `127.0.0.1:9100`. MCP endpoint: `http://127.0.0.1:9100/mcp`
|
|
|
30
30
|
|
|
31
31
|
## 2. 在 agent 端配置 MCP client
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### 推荐: 用 `mcpsmgr` 安装
|
|
34
|
+
|
|
35
|
+
最快的方式是用 [`mcpsmgr`](https://www.npmjs.com/package/mcpsmgr) CLI. 它读取本仓库的 manifest (`mcpsmgr.json`), 一次性把对应 agent 需要的 MCP 条目 (含必要的 stdio proxy 条目) 写到 agent 的配置文件里.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd <your-project>
|
|
39
|
+
|
|
40
|
+
# 选一个, 或者用裸命令进入交互式选择.
|
|
41
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code
|
|
42
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a codex
|
|
43
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp # 交互式
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
它做了什么:
|
|
47
|
+
|
|
48
|
+
- Claude Code: 同时写两条 `.mcp.json` 条目 (HTTP 工具 + `cross-agent-teams-channel` stdio proxy) — 你不用记两条.
|
|
49
|
+
- Codex: 写 `~/.codex/config.toml`, 带上 `experimental_use_rmcp_client = true` 和 streamable-http MCP 条目.
|
|
50
|
+
- 打印你还需要自己跑的 post-install 步骤 (例如 Claude Code 的 `--dangerously-load-development-channels server:cross-agent-teams-channel` 启动 flag, 或 codex 想要 push 唤醒时的 `--remote` 配置).
|
|
51
|
+
|
|
52
|
+
覆盖 daemon 端口:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx mcpsmgr add jtianling/cross-agent-teams-mcp -a claude-code --port 9300
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 手动配置
|
|
59
|
+
|
|
60
|
+
如果不想用 `mcpsmgr` (私有 fork / 自定义 token / 自定义 stdio args / 或者就是想手写), 各 agent 的原始配置如下.
|
|
61
|
+
|
|
62
|
+
#### Claude Code (两个条目都需要 — HTTP 用于工具, stdio 用于 channel 唤醒)
|
|
34
63
|
|
|
35
64
|
`.mcp.json` (或 `~/.claude.json`):
|
|
36
65
|
|
|
@@ -64,32 +93,92 @@ claude --dangerously-load-development-channels server:cross-agent-teams-channel
|
|
|
64
93
|
|
|
65
94
|
`server:<name>` 后缀 **必须** 等于 `.mcp.json` 里的 MCP server key (上例中是 `cross-agent-teams-channel`). 如果 daemon 启动带了 `--token <t>`, 在 HTTP 条目里加 `"headers": { "Authorization": "Bearer <t>" }`.
|
|
66
95
|
|
|
67
|
-
|
|
96
|
+
#### Codex CLI
|
|
97
|
+
|
|
98
|
+
Codex 通过 Streamable HTTP 跟 daemon 通信. 唤醒走 Codex 自己的 app-server WebSocket, 不经 channel proxy.
|
|
68
99
|
|
|
69
|
-
|
|
100
|
+
##### 最小配置 (只能收邮箱, 没有 push 唤醒)
|
|
70
101
|
|
|
71
102
|
`~/.codex/config.toml`:
|
|
72
103
|
|
|
73
104
|
```toml
|
|
74
|
-
|
|
105
|
+
experimental_use_rmcp_client = true
|
|
106
|
+
|
|
107
|
+
[mcp_servers.cross-agent-teams-mcp]
|
|
75
108
|
type = "streamable-http"
|
|
76
109
|
url = "http://127.0.0.1:9100/mcp"
|
|
77
110
|
```
|
|
78
111
|
|
|
79
|
-
|
|
112
|
+
`experimental_use_rmcp_client = true` 必须放在**顶级**, 缺这条 streamable-http MCP 加载不了.
|
|
113
|
+
|
|
114
|
+
(daemon 带了 `--token <t>` 时, 加 `[mcp_servers.cross-agent-teams-mcp.headers]` 和 `Authorization = "Bearer <t>"`.)
|
|
115
|
+
|
|
116
|
+
这种最小配置下 `send_message` 给这个 codex 会写邮箱, 但需要手动调 `get_inbox` 拉读, 没有跨会话 push 唤醒.
|
|
117
|
+
|
|
118
|
+
##### 让别人能唤醒你 (codex-appserver poke)
|
|
119
|
+
|
|
120
|
+
要让别的 agent 能**主动唤醒**这个 codex thread (而不只是发邮件), 需要 `codex-appserver` delivery. 这里有个不直观的坑要写清楚:
|
|
80
121
|
|
|
81
|
-
|
|
122
|
+
> **`codex --remote` 模式下, MCP server 是 app-server 加载的, 不是 TUI 加载的**. 上面那段 MCP 配置必须放在 **app-server** 启动时读到的 `CODEX_HOME` 里 — 一般就是全局 `~/.codex/config.toml`. 仅在 TUI 这边设 `CODEX_HOME` 在 `--remote` 模式下对 MCP 不起作用.
|
|
123
|
+
|
|
124
|
+
启动顺序:
|
|
82
125
|
|
|
83
126
|
```bash
|
|
84
|
-
codex app-server
|
|
85
|
-
codex --
|
|
127
|
+
# 1) 在某个长跑终端起 codex app-server (它的 CODEX_HOME 决定 MCP set)
|
|
128
|
+
codex app-server --listen ws://127.0.0.1:8799
|
|
129
|
+
|
|
130
|
+
# 2) 在另一个终端启动 codex TUI, 连同一个 app-server
|
|
131
|
+
codex --remote ws://127.0.0.1:8799
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
如果第 1 步的 app-server 的 `CODEX_HOME` 里没配 `cross-agent-teams-mcp`, `--remote` 进去的 codex agent 根本看不到 MCP 工具, `register_agent` 调都调不到.
|
|
135
|
+
|
|
136
|
+
##### 推荐: launcher 函数 (tmux pane 自动绑定)
|
|
137
|
+
|
|
138
|
+
为了让 daemon 把 wake-hint 直接 inject 到 codex thread (而不是只 paste 到 tmux pane), daemon 需要知道 codex 进程在哪个 tmux pane. launcher 通过 `pre-register-codex-pane` CLI 在 exec codex 之前先把 pane 占住. 把下面的函数加到 `~/.zshrc`:
|
|
139
|
+
|
|
140
|
+
```zsh
|
|
141
|
+
free-xats-codex() {
|
|
142
|
+
local xats_agent_id codex_home search_dir
|
|
143
|
+
xats_agent_id="$(uuidgen)"
|
|
144
|
+
search_dir="$PWD"
|
|
145
|
+
while [[ "$search_dir" != "/" ]]; do
|
|
146
|
+
if [[ -f "$search_dir/.codex/config.toml" ]]; then
|
|
147
|
+
codex_home="$search_dir/.codex"
|
|
148
|
+
break
|
|
149
|
+
fi
|
|
150
|
+
search_dir="${search_dir:h}"
|
|
151
|
+
done
|
|
152
|
+
|
|
153
|
+
if [[ -n "$TMUX_PANE" ]]; then
|
|
154
|
+
npx -y cross-agent-teams-mcp pre-register-codex-pane \
|
|
155
|
+
--pane "$TMUX_PANE" \
|
|
156
|
+
--agent-id "$xats_agent_id" \
|
|
157
|
+
>/dev/null 2>&1 \
|
|
158
|
+
|| echo "[xats] pre-register failed (continuing without pane claim)" >&2
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
if [[ -n "$codex_home" ]]; then
|
|
162
|
+
CODEX_HOME="$codex_home" exec codex \
|
|
163
|
+
--remote ws://127.0.0.1:8799 \
|
|
164
|
+
-c xats.agent_id="\"$xats_agent_id\"" "$@"
|
|
165
|
+
else
|
|
166
|
+
exec codex \
|
|
167
|
+
--remote ws://127.0.0.1:8799 \
|
|
168
|
+
-c xats.agent_id="\"$xats_agent_id\"" "$@"
|
|
169
|
+
fi
|
|
170
|
+
}
|
|
86
171
|
```
|
|
87
172
|
|
|
88
|
-
|
|
173
|
+
行为:
|
|
174
|
+
|
|
175
|
+
- 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`.
|
|
176
|
+
- `--remote ws://127.0.0.1:8799` 让 codex 连步骤 (1) 起好的 app-server.
|
|
177
|
+
- `-c xats.agent_id="\"$uuid\""` 把 UUID 暴露在 codex argv 里, daemon 用它反向校验 pane.
|
|
89
178
|
|
|
90
|
-
详细配置 (auth header,
|
|
179
|
+
详细配置 (auth header, 底层 `register_agent` 用法): [docs/configs/codex-cli.md](docs/configs/codex-cli.md).
|
|
91
180
|
|
|
92
|
-
|
|
181
|
+
#### 其它编码 agent (opencode, cursor, ...)
|
|
93
182
|
|
|
94
183
|
非 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
184
|
|
package/dist/cli.js
CHANGED
|
@@ -195,10 +195,18 @@ function migrateMessagesNeedReplyColumn(db) {
|
|
|
195
195
|
if (existing.has("need_reply")) return;
|
|
196
196
|
db.exec(`ALTER TABLE messages ADD COLUMN need_reply INTEGER NOT NULL DEFAULT 1`);
|
|
197
197
|
}
|
|
198
|
+
function migrateAgentsCursorWatermark(db) {
|
|
199
|
+
db.exec(
|
|
200
|
+
`UPDATE agents
|
|
201
|
+
SET last_processed_event_id = COALESCE((SELECT MAX(event_id) FROM events), 0)
|
|
202
|
+
WHERE last_processed_event_id = 0`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
198
205
|
function applySchema(db) {
|
|
199
206
|
for (const sql of DDL) db.exec(sql);
|
|
200
207
|
migrateAgentsDeliveryColumns(db);
|
|
201
208
|
migrateMessagesNeedReplyColumn(db);
|
|
209
|
+
migrateAgentsCursorWatermark(db);
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
// src/daemon/auth.ts
|
|
@@ -425,9 +433,11 @@ var AgentsRepo = class {
|
|
|
425
433
|
this.db.prepare(
|
|
426
434
|
`INSERT INTO agents (
|
|
427
435
|
agent_id, agent_type, agent_type_name, team, role, name, model, registered_at, last_seen_at,
|
|
428
|
-
tmux_pane_id, claude_ui_pid, runtime_ui_pid, delivery_kind, delivery_payload
|
|
436
|
+
tmux_pane_id, claude_ui_pid, runtime_ui_pid, delivery_kind, delivery_payload,
|
|
437
|
+
last_processed_event_id
|
|
429
438
|
)
|
|
430
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
439
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
440
|
+
COALESCE((SELECT MAX(event_id) FROM events), 0))
|
|
431
441
|
ON CONFLICT (team, name) DO UPDATE SET
|
|
432
442
|
agent_type = excluded.agent_type,
|
|
433
443
|
agent_type_name = excluded.agent_type_name,
|
|
@@ -1364,27 +1374,40 @@ var GetInboxService = class {
|
|
|
1364
1374
|
const caller = this.agents.findById(args.caller);
|
|
1365
1375
|
if (!caller) return { messages: [], has_more: false, last_event_id: args.since_event_id ?? 0 };
|
|
1366
1376
|
const callerTeam = caller.team;
|
|
1367
|
-
const
|
|
1377
|
+
const callerRoleRow = this.db.prepare("SELECT role, last_processed_event_id FROM agents WHERE agent_id=?").get(args.caller);
|
|
1378
|
+
const callerRole = callerRoleRow?.role;
|
|
1379
|
+
const storedCursor = callerRoleRow?.last_processed_event_id ?? 0;
|
|
1368
1380
|
const limit = Math.min(args.limit ?? 50, 200);
|
|
1369
|
-
const
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1381
|
+
const implicit = args.since_event_id === void 0;
|
|
1382
|
+
const effectiveSince = implicit ? storedCursor : args.since_event_id;
|
|
1383
|
+
const tx = this.db.transaction(() => {
|
|
1384
|
+
const rows = this.db.prepare(
|
|
1385
|
+
`SELECT m.id, m.event_id, m.from_team, m.to_team, m.from_agent_id, m.to_agent_id, m.to_role, m.subject, m.body, m.need_reply, m.sent_at,
|
|
1386
|
+
a.role as from_role
|
|
1387
|
+
FROM messages m
|
|
1388
|
+
LEFT JOIN agents a ON a.agent_id = m.from_agent_id
|
|
1389
|
+
WHERE m.to_team = ?
|
|
1390
|
+
AND m.event_id > ?
|
|
1391
|
+
AND ( m.to_agent_id = ? OR (m.to_role IS NOT NULL AND m.to_role = ?) )
|
|
1392
|
+
ORDER BY m.event_id ASC
|
|
1393
|
+
LIMIT ?`
|
|
1394
|
+
).all(callerTeam, effectiveSince, args.caller, callerRole ?? "__none__", limit + 1);
|
|
1395
|
+
const has_more = rows.length > limit;
|
|
1396
|
+
const trimmed = (has_more ? rows.slice(0, limit) : rows).map((row) => ({
|
|
1397
|
+
...row,
|
|
1398
|
+
need_reply: row.need_reply === 1
|
|
1399
|
+
}));
|
|
1400
|
+
const last_event_id = trimmed.length > 0 ? trimmed[trimmed.length - 1].event_id : effectiveSince;
|
|
1401
|
+
if (implicit && last_event_id > storedCursor) {
|
|
1402
|
+
this.db.prepare(
|
|
1403
|
+
`UPDATE agents
|
|
1404
|
+
SET last_processed_event_id = ?
|
|
1405
|
+
WHERE agent_id = ? AND last_processed_event_id < ?`
|
|
1406
|
+
).run(last_event_id, args.caller, last_event_id);
|
|
1407
|
+
}
|
|
1408
|
+
return { messages: trimmed, has_more, last_event_id };
|
|
1409
|
+
});
|
|
1410
|
+
return tx();
|
|
1388
1411
|
}
|
|
1389
1412
|
};
|
|
1390
1413
|
|
|
@@ -3701,7 +3724,13 @@ function registerBusinessTools(server, db, getCallerAgentId, fanout, onRegisterS
|
|
|
3701
3724
|
"get_inbox",
|
|
3702
3725
|
{
|
|
3703
3726
|
title: "Get inbox",
|
|
3704
|
-
description:
|
|
3727
|
+
description: [
|
|
3728
|
+
"Return messages addressed to the caller (by agent_id or matching role) within the caller team.",
|
|
3729
|
+
"Default behaviour (since_event_id omitted): the daemon reads the caller's server-side cursor (`agents.last_processed_event_id`), returns mail past it, and ADVANCES the cursor to the highest returned event_id in the same transaction. Subsequent default calls return only newer mail.",
|
|
3730
|
+
"Pagination via `limit` advances the cursor only to the last RETURNED event_id; the next default call resumes from there.",
|
|
3731
|
+
"Explicit `since_event_id` (any number, including 0) is read-only inspection: the daemon uses the supplied value as the lower bound and does NOT advance the stored cursor \u2014 useful for re-reading history or debugging without disturbing live read position.",
|
|
3732
|
+
"Retention: messages older than 30 days are deleted by the cleanup routine regardless of read state. Agents that go offline for more than 30 days forfeit any unread mail in that window."
|
|
3733
|
+
].join(" "),
|
|
3705
3734
|
inputSchema: {
|
|
3706
3735
|
since_event_id: z3.number().int().optional(),
|
|
3707
3736
|
limit: z3.number().int().optional()
|
|
@@ -4210,30 +4239,23 @@ function mountMcp(app, db, fanout, channelWakeFanout, opts = {}) {
|
|
|
4210
4239
|
}
|
|
4211
4240
|
|
|
4212
4241
|
// src/daemon/cleanup.ts
|
|
4213
|
-
var DELETE_AGED_EVENTS_SQL = `
|
|
4214
|
-
WITH online_cursor AS (
|
|
4215
|
-
SELECT team AS to_team, MIN(last_processed_event_id) AS min_cursor
|
|
4216
|
-
FROM agents
|
|
4217
|
-
WHERE last_seen_at >= :cutoffOnline
|
|
4218
|
-
GROUP BY team
|
|
4219
|
-
)
|
|
4220
|
-
DELETE FROM events
|
|
4221
|
-
WHERE created_at < :ageCutoff
|
|
4222
|
-
AND (
|
|
4223
|
-
events.to_team NOT IN (SELECT to_team FROM online_cursor)
|
|
4224
|
-
OR events.event_id < (
|
|
4225
|
-
SELECT min_cursor FROM online_cursor WHERE online_cursor.to_team = events.to_team
|
|
4226
|
-
)
|
|
4227
|
-
)
|
|
4228
|
-
`;
|
|
4229
4242
|
function runCleanup(db, opts = {}) {
|
|
4230
4243
|
const now = opts.now ?? /* @__PURE__ */ new Date();
|
|
4231
|
-
const maxAgeDays = opts.maxAgeDays ??
|
|
4232
|
-
const onlineWindowMs = opts.onlineWindowMs ?? 5 * 60 * 1e3;
|
|
4244
|
+
const maxAgeDays = opts.maxAgeDays ?? 30;
|
|
4233
4245
|
const ageCutoff = new Date(now.getTime() - maxAgeDays * 86400 * 1e3).toISOString();
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4236
|
-
|
|
4246
|
+
const deleteStatus = db.prepare(
|
|
4247
|
+
`DELETE FROM message_delivery_status
|
|
4248
|
+
WHERE message_id IN (SELECT id FROM messages WHERE sent_at < ?)`
|
|
4249
|
+
);
|
|
4250
|
+
const deleteMessages = db.prepare(`DELETE FROM messages WHERE sent_at < ?`);
|
|
4251
|
+
const deleteEvents = db.prepare(`DELETE FROM events WHERE created_at < ?`);
|
|
4252
|
+
const tx = db.transaction(() => {
|
|
4253
|
+
const s = deleteStatus.run(ageCutoff);
|
|
4254
|
+
const m = deleteMessages.run(ageCutoff);
|
|
4255
|
+
const e = deleteEvents.run(ageCutoff);
|
|
4256
|
+
return Number(s.changes) + Number(m.changes) + Number(e.changes);
|
|
4257
|
+
});
|
|
4258
|
+
return { deleted: tx() };
|
|
4237
4259
|
}
|
|
4238
4260
|
|
|
4239
4261
|
// src/daemon/sse-fanout.ts
|