cross-agent-teams-mcp 0.2.0

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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +296 -0
  3. package/README.zh-CN.md +306 -0
  4. package/dist/channel-cli.d.ts +18 -0
  5. package/dist/channel-cli.js +358 -0
  6. package/dist/channel-cli.js.map +1 -0
  7. package/dist/cli.d.ts +1 -0
  8. package/dist/cli.js +4585 -0
  9. package/dist/cli.js.map +1 -0
  10. package/package.json +62 -0
  11. package/src/channel/auto-daemon.ts +130 -0
  12. package/src/channel/daemon-client.ts +155 -0
  13. package/src/channel/proxy.ts +28 -0
  14. package/src/channel-cli.ts +122 -0
  15. package/src/cli.ts +136 -0
  16. package/src/daemon/auth.ts +17 -0
  17. package/src/daemon/channel-wake-fanout.ts +39 -0
  18. package/src/daemon/channel-wake-send.ts +38 -0
  19. package/src/daemon/cleanup.ts +38 -0
  20. package/src/daemon/errors.ts +18 -0
  21. package/src/daemon/pid.ts +33 -0
  22. package/src/daemon/port.ts +16 -0
  23. package/src/daemon/runtime-identity.ts +238 -0
  24. package/src/daemon/server.ts +64 -0
  25. package/src/daemon/shutdown.ts +12 -0
  26. package/src/daemon/sse-fanout.ts +96 -0
  27. package/src/daemon/tmux-cli.ts +61 -0
  28. package/src/daemon/tmux-pane-detect.ts +276 -0
  29. package/src/lib/client-kind.ts +1 -0
  30. package/src/lib/default-team.ts +18 -0
  31. package/src/lib/delivery-spec.ts +172 -0
  32. package/src/lib/schema-diff.ts +79 -0
  33. package/src/mcp/agent-public-row.ts +52 -0
  34. package/src/mcp/auto-bind-channel.ts +106 -0
  35. package/src/mcp/auto-bind-codex-pane.ts +170 -0
  36. package/src/mcp/auto-poke-fanout.ts +129 -0
  37. package/src/mcp/bind-channel.ts +39 -0
  38. package/src/mcp/bind-runtime-identity.ts +43 -0
  39. package/src/mcp/broadcast-to-role.ts +127 -0
  40. package/src/mcp/broadcast.ts +115 -0
  41. package/src/mcp/codex-appserver-dispatch.ts +169 -0
  42. package/src/mcp/codex-appserver-rpc.ts +227 -0
  43. package/src/mcp/codex-pane-pre-register-repo.ts +57 -0
  44. package/src/mcp/delivery-status.ts +114 -0
  45. package/src/mcp/diff-contracts.ts +25 -0
  46. package/src/mcp/echo.ts +8 -0
  47. package/src/mcp/fanout-with-retry.ts +56 -0
  48. package/src/mcp/get-contract.ts +24 -0
  49. package/src/mcp/get-inbox.ts +57 -0
  50. package/src/mcp/identity.ts +8 -0
  51. package/src/mcp/pending-contract-events.ts +36 -0
  52. package/src/mcp/poke-guard.ts +32 -0
  53. package/src/mcp/poke-retry.ts +159 -0
  54. package/src/mcp/poke.ts +190 -0
  55. package/src/mcp/pre-register-codex-pane.ts +65 -0
  56. package/src/mcp/register-agent.ts +84 -0
  57. package/src/mcp/register-codex-self.ts +276 -0
  58. package/src/mcp/register-contract.ts +60 -0
  59. package/src/mcp/send-message.ts +159 -0
  60. package/src/mcp/subscribe-channel-wake.ts +31 -0
  61. package/src/mcp/subscribe-contract.ts +24 -0
  62. package/src/mcp/task-add.ts +37 -0
  63. package/src/mcp/task-claim.ts +54 -0
  64. package/src/mcp/task-complete.ts +36 -0
  65. package/src/mcp/task-list.ts +33 -0
  66. package/src/mcp/tools.ts +1240 -0
  67. package/src/mcp/transport-dispatch.ts +171 -0
  68. package/src/mcp/transport.ts +204 -0
  69. package/src/mcp/unregister-self.ts +46 -0
  70. package/src/storage/agents-repo.ts +328 -0
  71. package/src/storage/db.ts +13 -0
  72. package/src/storage/events-outbox.ts +44 -0
  73. package/src/storage/schema.ts +180 -0
package/dist/cli.js ADDED
@@ -0,0 +1,4585 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
5
+ import { homedir } from "os";
6
+ import { join } from "path";
7
+
8
+ // src/daemon/server.ts
9
+ import Fastify from "fastify";
10
+
11
+ // src/storage/db.ts
12
+ import Database from "better-sqlite3";
13
+ import { mkdirSync } from "fs";
14
+ import { dirname } from "path";
15
+ function openDb(path) {
16
+ mkdirSync(dirname(path), { recursive: true });
17
+ const db = new Database(path);
18
+ db.pragma("journal_mode = WAL");
19
+ db.pragma("busy_timeout = 5000");
20
+ db.pragma("synchronous = NORMAL");
21
+ db.pragma("foreign_keys = ON");
22
+ return db;
23
+ }
24
+
25
+ // src/storage/schema.ts
26
+ var DDL = [
27
+ `CREATE TABLE IF NOT EXISTS events (
28
+ event_id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ from_team TEXT NOT NULL,
30
+ to_team TEXT NOT NULL,
31
+ event_type TEXT NOT NULL,
32
+ actor_agent_id TEXT,
33
+ payload TEXT NOT NULL,
34
+ created_at TEXT NOT NULL
35
+ )`,
36
+ `CREATE INDEX IF NOT EXISTS idx_events_from_team_eventid ON events(from_team, event_id)`,
37
+ `CREATE INDEX IF NOT EXISTS idx_events_to_team_eventid ON events(to_team, event_id)`,
38
+ `CREATE TABLE IF NOT EXISTS agents (
39
+ agent_id TEXT PRIMARY KEY,
40
+ client TEXT,
41
+ client_name TEXT,
42
+ team TEXT NOT NULL,
43
+ role TEXT NOT NULL,
44
+ name TEXT NOT NULL,
45
+ model TEXT,
46
+ registered_at TEXT NOT NULL,
47
+ last_seen_at TEXT NOT NULL,
48
+ last_processed_event_id INTEGER NOT NULL DEFAULT 0,
49
+ tmux_pane_id TEXT,
50
+ claude_ui_pid INTEGER,
51
+ runtime_ui_pid INTEGER,
52
+ runtime_tty TEXT,
53
+ runtime_verification_mode TEXT,
54
+ runtime_bound_at TEXT,
55
+ channel_session_id TEXT,
56
+ delivery_kind TEXT NOT NULL DEFAULT 'none',
57
+ delivery_payload TEXT
58
+ )`,
59
+ `CREATE UNIQUE INDEX IF NOT EXISTS agents_identity_idx ON agents(team, name)`,
60
+ `CREATE TABLE IF NOT EXISTS messages (
61
+ id TEXT PRIMARY KEY,
62
+ event_id INTEGER NOT NULL REFERENCES events(event_id),
63
+ from_team TEXT NOT NULL,
64
+ to_team TEXT NOT NULL,
65
+ from_agent_id TEXT NOT NULL,
66
+ to_agent_id TEXT,
67
+ to_role TEXT,
68
+ subject TEXT,
69
+ body TEXT NOT NULL,
70
+ need_reply INTEGER NOT NULL DEFAULT 1,
71
+ sent_at TEXT NOT NULL
72
+ )`,
73
+ `CREATE TABLE IF NOT EXISTS message_delivery_status (
74
+ message_id TEXT NOT NULL,
75
+ agent_id TEXT NOT NULL,
76
+ wake_status TEXT NOT NULL CHECK(wake_status IN ('delivered','retrying','skipped','failed')),
77
+ skip_reason TEXT,
78
+ retry_attempts INTEGER NOT NULL DEFAULT 0,
79
+ updated_at TEXT NOT NULL,
80
+ delivered_at TEXT,
81
+ PRIMARY KEY (message_id, agent_id)
82
+ )`,
83
+ `CREATE INDEX IF NOT EXISTS idx_message_delivery_status_message ON message_delivery_status(message_id)`,
84
+ `CREATE TABLE IF NOT EXISTS tasks (
85
+ id TEXT PRIMARY KEY,
86
+ team TEXT NOT NULL,
87
+ title TEXT NOT NULL,
88
+ description TEXT,
89
+ status TEXT NOT NULL CHECK(status IN ('pending','in_progress','completed')),
90
+ depends_on TEXT NOT NULL,
91
+ claimed_by TEXT,
92
+ claimed_at TEXT,
93
+ completed_at TEXT,
94
+ result TEXT,
95
+ created_at TEXT NOT NULL
96
+ )`,
97
+ `CREATE TABLE IF NOT EXISTS contracts (
98
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
99
+ team TEXT NOT NULL,
100
+ name TEXT NOT NULL,
101
+ version INTEGER NOT NULL,
102
+ format TEXT NOT NULL CHECK(format='jsonschema'),
103
+ schema TEXT NOT NULL,
104
+ note TEXT,
105
+ registered_by TEXT NOT NULL,
106
+ registered_at TEXT NOT NULL,
107
+ UNIQUE(team, name, version)
108
+ )`,
109
+ `CREATE TABLE IF NOT EXISTS contract_subscriptions (
110
+ agent_id TEXT NOT NULL,
111
+ team TEXT NOT NULL,
112
+ contract_name TEXT NOT NULL,
113
+ subscribed_at TEXT NOT NULL,
114
+ PRIMARY KEY (agent_id, team, contract_name)
115
+ )`,
116
+ `CREATE TABLE IF NOT EXISTS codex_pane_pre_registrations (
117
+ pane_id TEXT PRIMARY KEY,
118
+ xats_agent_id TEXT NOT NULL,
119
+ expires_at TEXT NOT NULL
120
+ )`
121
+ ];
122
+ function migrateAgentsDeliveryColumns(db) {
123
+ const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='agents'`).get();
124
+ if (!tableExists) return;
125
+ const cols = db.pragma("table_info(agents)");
126
+ const existing = new Set(cols.map((c) => c.name));
127
+ const needClient = !existing.has("client");
128
+ const needClientName = !existing.has("client_name");
129
+ const needKind = !existing.has("delivery_kind");
130
+ const needPayload = !existing.has("delivery_payload");
131
+ const needRuntimeUiPid = !existing.has("runtime_ui_pid");
132
+ const needRuntimeTty = !existing.has("runtime_tty");
133
+ const needRuntimeVerificationMode = !existing.has("runtime_verification_mode");
134
+ const needRuntimeBoundAt = !existing.has("runtime_bound_at");
135
+ const needClaudeUiPid = !existing.has("claude_ui_pid");
136
+ if (!needClient && !needClientName && !needKind && !needPayload && !needRuntimeUiPid && !needRuntimeTty && !needRuntimeVerificationMode && !needRuntimeBoundAt && !needClaudeUiPid) return;
137
+ const tx = db.transaction(() => {
138
+ if (needClient) {
139
+ db.exec(`ALTER TABLE agents ADD COLUMN client TEXT`);
140
+ }
141
+ if (needClientName) {
142
+ db.exec(`ALTER TABLE agents ADD COLUMN client_name TEXT`);
143
+ }
144
+ if (needKind) {
145
+ db.exec(`ALTER TABLE agents ADD COLUMN delivery_kind TEXT NOT NULL DEFAULT 'none'`);
146
+ }
147
+ if (needPayload) {
148
+ db.exec(`ALTER TABLE agents ADD COLUMN delivery_payload TEXT`);
149
+ }
150
+ if (needRuntimeUiPid) {
151
+ db.exec(`ALTER TABLE agents ADD COLUMN runtime_ui_pid INTEGER`);
152
+ }
153
+ if (needRuntimeTty) {
154
+ db.exec(`ALTER TABLE agents ADD COLUMN runtime_tty TEXT`);
155
+ }
156
+ if (needRuntimeVerificationMode) {
157
+ db.exec(`ALTER TABLE agents ADD COLUMN runtime_verification_mode TEXT`);
158
+ }
159
+ if (needRuntimeBoundAt) {
160
+ db.exec(`ALTER TABLE agents ADD COLUMN runtime_bound_at TEXT`);
161
+ }
162
+ if (needClaudeUiPid) {
163
+ db.exec(`ALTER TABLE agents ADD COLUMN claude_ui_pid INTEGER`);
164
+ }
165
+ if (needKind || needPayload) {
166
+ db.exec(`UPDATE agents
167
+ SET delivery_kind = 'claude-channel',
168
+ delivery_payload = json_object('channel_session_id', channel_session_id)
169
+ WHERE channel_session_id IS NOT NULL AND delivery_kind = 'none'`);
170
+ }
171
+ });
172
+ tx();
173
+ }
174
+ function migrateMessagesNeedReplyColumn(db) {
175
+ const tableExists = db.prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name='messages'`).get();
176
+ if (!tableExists) return;
177
+ const cols = db.pragma("table_info(messages)");
178
+ const existing = new Set(cols.map((c) => c.name));
179
+ if (existing.has("need_reply")) return;
180
+ db.exec(`ALTER TABLE messages ADD COLUMN need_reply INTEGER NOT NULL DEFAULT 1`);
181
+ }
182
+ function applySchema(db) {
183
+ for (const sql of DDL) db.exec(sql);
184
+ migrateAgentsDeliveryColumns(db);
185
+ migrateMessagesNeedReplyColumn(db);
186
+ }
187
+
188
+ // src/daemon/auth.ts
189
+ function extractToken(req) {
190
+ const h = req.headers["authorization"];
191
+ if (typeof h === "string" && h.startsWith("Bearer ")) return h.slice(7);
192
+ const q = req.query?.token;
193
+ return typeof q === "string" ? q : void 0;
194
+ }
195
+ function makeAuthHook(expected) {
196
+ return async (req, reply) => {
197
+ if (req.url.startsWith("/health")) return;
198
+ if (!expected) return;
199
+ const got = extractToken(req);
200
+ if (got !== expected) return reply.code(401).send({ error: "invalid_token" });
201
+ };
202
+ }
203
+
204
+ // src/mcp/transport.ts
205
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
206
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
207
+ import { randomUUID as randomUUID6, createHash } from "crypto";
208
+
209
+ // src/mcp/echo.ts
210
+ import { z } from "zod";
211
+ var echoSchema = { msg: z.string() };
212
+ async function echoHandler(args) {
213
+ const out = { msg: args.msg, echoed_at: (/* @__PURE__ */ new Date()).toISOString() };
214
+ return { content: [{ type: "text", text: JSON.stringify(out) }] };
215
+ }
216
+
217
+ // src/mcp/tools.ts
218
+ import { z as z3 } from "zod";
219
+
220
+ // src/storage/agents-repo.ts
221
+ import { randomUUID } from "crypto";
222
+
223
+ // src/lib/delivery-spec.ts
224
+ var DELIVERY_KINDS = [
225
+ "none",
226
+ "claude-channel",
227
+ "codex-appserver"
228
+ ];
229
+ function parseDeliveryRow(row) {
230
+ const kind = row.delivery_kind;
231
+ if (kind === "none") {
232
+ return { kind: "none" };
233
+ }
234
+ if (!DELIVERY_KINDS.includes(kind)) {
235
+ throw new Error("corrupt_delivery_payload");
236
+ }
237
+ let payload;
238
+ try {
239
+ payload = row.delivery_payload == null ? {} : JSON.parse(row.delivery_payload);
240
+ } catch {
241
+ throw new Error("corrupt_delivery_payload");
242
+ }
243
+ if (typeof payload !== "object" || payload === null) {
244
+ throw new Error("corrupt_delivery_payload");
245
+ }
246
+ const record = payload;
247
+ if (kind === "claude-channel") {
248
+ const csid = record.channel_session_id;
249
+ if (typeof csid !== "string" || csid.length === 0) {
250
+ throw new Error("corrupt_delivery_payload");
251
+ }
252
+ return { kind: "claude-channel", channel_session_id: csid };
253
+ }
254
+ if (kind === "codex-appserver") {
255
+ const threadId = record.thread_id;
256
+ if (typeof threadId !== "string" || threadId.length === 0) {
257
+ throw new Error("corrupt_delivery_payload");
258
+ }
259
+ const wsUrl = record.ws_url;
260
+ if (typeof wsUrl !== "string" || wsUrl.length === 0) {
261
+ throw new Error("corrupt_delivery_payload");
262
+ }
263
+ const hasAuthTokenRef = Object.prototype.hasOwnProperty.call(record, "auth_token_ref");
264
+ if (hasAuthTokenRef) {
265
+ const authTokenRef = record.auth_token_ref;
266
+ if (typeof authTokenRef !== "string" || authTokenRef.length === 0) {
267
+ throw new Error("corrupt_delivery_payload");
268
+ }
269
+ return {
270
+ kind: "codex-appserver",
271
+ thread_id: threadId,
272
+ ws_url: wsUrl,
273
+ auth_token_ref: authTokenRef
274
+ };
275
+ }
276
+ return { kind: "codex-appserver", thread_id: threadId, ws_url: wsUrl };
277
+ }
278
+ throw new Error("corrupt_delivery_payload");
279
+ }
280
+ function serializeDelivery(spec) {
281
+ if (spec.kind === "none") {
282
+ return { delivery_kind: "none", delivery_payload: null };
283
+ }
284
+ const { kind, ...rest } = spec;
285
+ return {
286
+ delivery_kind: kind,
287
+ delivery_payload: JSON.stringify(rest)
288
+ };
289
+ }
290
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
291
+ function readTrimmedString(input, key) {
292
+ const value = input[key];
293
+ if (typeof value !== "string") return void 0;
294
+ const trimmed = value.trim();
295
+ return trimmed.length > 0 ? trimmed : "";
296
+ }
297
+ function validateDeliveryForWrite(input) {
298
+ if (typeof input !== "object" || input === null) {
299
+ return { error: "invalid_delivery", reason: "unknown_kind" };
300
+ }
301
+ const record = input;
302
+ const kind = record.kind;
303
+ if (kind === "none") {
304
+ return { ok: { kind: "none" } };
305
+ }
306
+ if (kind === "claude-channel") {
307
+ const csid = readTrimmedString(record, "channel_session_id");
308
+ if (csid === void 0 || csid.length === 0) {
309
+ return { error: "invalid_delivery", reason: "missing_channel_session_id" };
310
+ }
311
+ return { ok: { kind: "claude-channel", channel_session_id: csid } };
312
+ }
313
+ if (kind === "codex-appserver") {
314
+ const threadId = readTrimmedString(record, "thread_id");
315
+ if (threadId === void 0 || threadId.length === 0 || !UUID_RE.test(threadId)) {
316
+ return { error: "invalid_delivery", reason: "invalid_thread_id" };
317
+ }
318
+ const wsUrl = readTrimmedString(record, "ws_url");
319
+ if (wsUrl === void 0 || wsUrl.length === 0) {
320
+ return { error: "invalid_delivery", reason: "invalid_ws_url" };
321
+ }
322
+ try {
323
+ const parsed = new URL(wsUrl);
324
+ if (parsed.protocol !== "ws:" && parsed.protocol !== "wss:") {
325
+ return { error: "invalid_delivery", reason: "invalid_ws_url" };
326
+ }
327
+ } catch {
328
+ return { error: "invalid_delivery", reason: "invalid_ws_url" };
329
+ }
330
+ const authTokenRef = readTrimmedString(record, "auth_token_ref");
331
+ if (authTokenRef === "") {
332
+ return { error: "invalid_delivery", reason: "invalid_auth_token_ref" };
333
+ }
334
+ return {
335
+ ok: {
336
+ kind: "codex-appserver",
337
+ thread_id: threadId,
338
+ ws_url: wsUrl,
339
+ ...authTokenRef === void 0 ? {} : { auth_token_ref: authTokenRef }
340
+ }
341
+ };
342
+ }
343
+ return { error: "invalid_delivery", reason: "unknown_kind" };
344
+ }
345
+
346
+ // src/storage/agents-repo.ts
347
+ var ONLINE_MS = 5 * 60 * 1e3;
348
+ function toAgentRow(row) {
349
+ const delivery = parseDeliveryRow(row);
350
+ return {
351
+ agent_id: row.agent_id,
352
+ client: row.client,
353
+ client_name: row.client_name,
354
+ team: row.team,
355
+ role: row.role,
356
+ name: row.name,
357
+ model: row.model,
358
+ tmux_pane_id: row.tmux_pane_id,
359
+ delivery,
360
+ channel_session_id: delivery.kind === "claude-channel" ? delivery.channel_session_id : null,
361
+ last_seen_at: row.last_seen_at
362
+ };
363
+ }
364
+ var AgentsRepo = class {
365
+ constructor(db) {
366
+ this.db = db;
367
+ }
368
+ db;
369
+ findByIdentity(args) {
370
+ return this.db.prepare(
371
+ `SELECT agent_id FROM agents WHERE team=? AND name=?`
372
+ ).get(args.team, args.name);
373
+ }
374
+ register(input) {
375
+ const team = input.team ?? "default";
376
+ const role = input.role ?? "default";
377
+ const name = input.name;
378
+ const now = (/* @__PURE__ */ new Date()).toISOString();
379
+ const newId = randomUUID();
380
+ const delivery = input.delivery ?? { kind: "none" };
381
+ const serialized = serializeDelivery(delivery);
382
+ const preserveExistingDelivery = input.delivery === void 0 ? 1 : 0;
383
+ const tx = this.db.transaction(() => {
384
+ this.writeAgentRow({
385
+ newId,
386
+ input,
387
+ team,
388
+ role,
389
+ name,
390
+ now,
391
+ serialized,
392
+ preserveExistingDelivery
393
+ });
394
+ const rebindCsid = role === "__channel_proxy__" && input.claude_ui_pid !== void 0 && delivery.kind === "claude-channel" ? delivery.channel_session_id : void 0;
395
+ if (rebindCsid !== void 0) {
396
+ this.reactiveRebindHosts({
397
+ team,
398
+ claude_ui_pid: input.claude_ui_pid,
399
+ new_csid: rebindCsid
400
+ });
401
+ }
402
+ });
403
+ tx();
404
+ const row = this.db.prepare(`SELECT agent_id FROM agents WHERE team=? AND name=?`).get(team, name);
405
+ return { agent_id: row.agent_id, team };
406
+ }
407
+ writeAgentRow(args) {
408
+ const { newId, input, team, role, name, now, serialized, preserveExistingDelivery } = args;
409
+ this.db.prepare(
410
+ `INSERT INTO agents (
411
+ agent_id, client, client_name, team, role, name, model, registered_at, last_seen_at,
412
+ tmux_pane_id, claude_ui_pid, runtime_ui_pid, delivery_kind, delivery_payload
413
+ )
414
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
415
+ ON CONFLICT (team, name) DO UPDATE SET
416
+ client = excluded.client,
417
+ client_name = excluded.client_name,
418
+ role = excluded.role,
419
+ model = excluded.model,
420
+ last_seen_at = excluded.last_seen_at,
421
+ tmux_pane_id = COALESCE(excluded.tmux_pane_id, tmux_pane_id),
422
+ claude_ui_pid = COALESCE(excluded.claude_ui_pid, claude_ui_pid),
423
+ runtime_ui_pid = COALESCE(excluded.runtime_ui_pid, runtime_ui_pid),
424
+ delivery_kind = CASE
425
+ WHEN ? THEN delivery_kind
426
+ ELSE excluded.delivery_kind
427
+ END,
428
+ delivery_payload = CASE
429
+ WHEN ? THEN delivery_payload
430
+ ELSE excluded.delivery_payload
431
+ END`
432
+ ).run(
433
+ newId,
434
+ input.client ?? null,
435
+ input.client_name ?? null,
436
+ team,
437
+ role,
438
+ name,
439
+ input.model,
440
+ now,
441
+ now,
442
+ input.tmux_pane_id ?? null,
443
+ input.claude_ui_pid ?? null,
444
+ input.runtime_ui_pid ?? null,
445
+ serialized.delivery_kind,
446
+ serialized.delivery_payload,
447
+ preserveExistingDelivery,
448
+ preserveExistingDelivery
449
+ );
450
+ }
451
+ reactiveRebindHosts(args) {
452
+ this.db.prepare(
453
+ `UPDATE agents
454
+ SET delivery_kind = 'claude-channel',
455
+ delivery_payload = json_object('channel_session_id', ?)
456
+ WHERE role != '__channel_proxy__'
457
+ AND runtime_ui_pid IS NOT NULL
458
+ AND runtime_ui_pid = ?
459
+ AND team = ?
460
+ AND (
461
+ delivery_kind = 'none'
462
+ OR (delivery_kind = 'claude-channel'
463
+ AND json_extract(delivery_payload,'$.channel_session_id') != ?)
464
+ )`
465
+ ).run(args.new_csid, args.claude_ui_pid, args.team, args.new_csid);
466
+ }
467
+ setDelivery(agent_id, spec) {
468
+ const serialized = serializeDelivery(spec);
469
+ this.db.prepare(
470
+ `UPDATE agents
471
+ SET delivery_kind=?, delivery_payload=?
472
+ WHERE agent_id=?`
473
+ ).run(serialized.delivery_kind, serialized.delivery_payload, agent_id);
474
+ }
475
+ setClient(agent_id, client, client_name) {
476
+ this.db.prepare(
477
+ `UPDATE agents
478
+ SET client=?,
479
+ client_name=?
480
+ WHERE agent_id=?`
481
+ ).run(client, client_name ?? null, agent_id);
482
+ }
483
+ setRuntimeBinding(agent_id, args) {
484
+ this.db.prepare(
485
+ `UPDATE agents
486
+ SET tmux_pane_id=?,
487
+ runtime_ui_pid=?,
488
+ runtime_tty=?,
489
+ runtime_verification_mode=?,
490
+ runtime_bound_at=?
491
+ WHERE agent_id=?`
492
+ ).run(
493
+ args.tmux_pane_id,
494
+ args.runtime_ui_pid,
495
+ args.runtime_tty,
496
+ args.runtime_verification_mode,
497
+ args.runtime_bound_at ?? (/* @__PURE__ */ new Date()).toISOString(),
498
+ agent_id
499
+ );
500
+ }
501
+ list(args) {
502
+ const rows = this.db.prepare(
503
+ `SELECT
504
+ agent_id,
505
+ client,
506
+ client_name,
507
+ team,
508
+ role,
509
+ name,
510
+ model,
511
+ tmux_pane_id,
512
+ delivery_kind,
513
+ delivery_payload,
514
+ last_seen_at
515
+ FROM agents
516
+ WHERE team=?
517
+ ORDER BY registered_at ASC`
518
+ ).all(args.team);
519
+ const nowMs = Date.now();
520
+ return rows.map((row) => {
521
+ const agent = toAgentRow(row);
522
+ return {
523
+ ...agent,
524
+ online: nowMs - new Date(agent.last_seen_at).getTime() < ONLINE_MS
525
+ };
526
+ });
527
+ }
528
+ touch(agent_id) {
529
+ this.db.prepare(`UPDATE agents SET last_seen_at=? WHERE agent_id=?`).run((/* @__PURE__ */ new Date()).toISOString(), agent_id);
530
+ }
531
+ listClaimedInProgressTaskIds(args) {
532
+ const rows = this.db.prepare(
533
+ `SELECT id
534
+ FROM tasks
535
+ WHERE team=? AND claimed_by=? AND status='in_progress'
536
+ ORDER BY id ASC`
537
+ ).all(args.team, args.agent_id);
538
+ return rows.map((row) => row.id);
539
+ }
540
+ deleteContractSubscriptions(args) {
541
+ const result = this.db.prepare(
542
+ `DELETE FROM contract_subscriptions
543
+ WHERE agent_id=? AND team=?`
544
+ ).run(args.agent_id, args.team);
545
+ return result.changes;
546
+ }
547
+ deleteById(agent_id) {
548
+ const result = this.db.prepare(
549
+ `DELETE FROM agents
550
+ WHERE agent_id=?`
551
+ ).run(agent_id);
552
+ return result.changes === 1;
553
+ }
554
+ getById(agent_id) {
555
+ const row = this.db.prepare(
556
+ `SELECT
557
+ agent_id,
558
+ client,
559
+ client_name,
560
+ team,
561
+ role,
562
+ name,
563
+ model,
564
+ tmux_pane_id,
565
+ delivery_kind,
566
+ delivery_payload,
567
+ last_seen_at
568
+ FROM agents
569
+ WHERE agent_id=?`
570
+ ).get(agent_id);
571
+ if (!row) return void 0;
572
+ return toAgentRow(row);
573
+ }
574
+ findById(agent_id) {
575
+ return this.getById(agent_id);
576
+ }
577
+ };
578
+
579
+ // src/storage/events-outbox.ts
580
+ var EventsOutbox = class {
581
+ constructor(db) {
582
+ this.db = db;
583
+ }
584
+ db;
585
+ append(args) {
586
+ const stmt = this.db.prepare(
587
+ `INSERT INTO events (from_team, to_team, event_type, actor_agent_id, payload, created_at)
588
+ VALUES (?, ?, ?, ?, ?, ?)`
589
+ );
590
+ const info = stmt.run(
591
+ args.from_team,
592
+ args.to_team,
593
+ args.event_type,
594
+ args.actor_agent_id ?? null,
595
+ JSON.stringify(args.payload),
596
+ (/* @__PURE__ */ new Date()).toISOString()
597
+ );
598
+ return Number(info.lastInsertRowid);
599
+ }
600
+ since(args) {
601
+ const limit = Math.min(args.limit ?? 100, 500);
602
+ return this.db.prepare(
603
+ `SELECT * FROM events WHERE to_team = ? AND event_id > ? ORDER BY event_id ASC LIMIT ?`
604
+ ).all(args.team, args.since_event_id, limit);
605
+ }
606
+ };
607
+
608
+ // src/lib/default-team.ts
609
+ import { basename } from "path";
610
+ function deriveDefaultTeam(input) {
611
+ const explicitTeam = input.team?.trim();
612
+ if (explicitTeam) return explicitTeam;
613
+ if (input.project_dir !== void 0) {
614
+ const projectTeam = basename(input.project_dir).trim().toLowerCase();
615
+ if (projectTeam) return projectTeam;
616
+ }
617
+ return "default";
618
+ }
619
+
620
+ // src/mcp/register-agent.ts
621
+ function identityKey(team, name) {
622
+ return `${team}\0${name}`;
623
+ }
624
+ var RegisterAgentService = class {
625
+ repo;
626
+ connections = /* @__PURE__ */ new Map();
627
+ constructor(db) {
628
+ this.repo = new AgentsRepo(db);
629
+ }
630
+ register(input) {
631
+ const validated = input.delivery === void 0 ? void 0 : validateDeliveryForWrite(input.delivery);
632
+ if (validated && "error" in validated) return validated;
633
+ const role = input.role ?? "default";
634
+ if (input.claude_ui_pid !== void 0 && role !== "__channel_proxy__") {
635
+ return { error: "claude_ui_pid_requires_channel_proxy" };
636
+ }
637
+ const team = deriveDefaultTeam({
638
+ team: input.team,
639
+ project_dir: input.project_dir
640
+ });
641
+ const key = identityKey(team, input.name);
642
+ const bound = this.connections.get(key);
643
+ if (bound && bound !== input.connection_id) return { error: "agent_id_collision" };
644
+ this.connections.set(key, input.connection_id);
645
+ return this.repo.register({
646
+ client: input.client,
647
+ client_name: input.client_name,
648
+ model: input.model,
649
+ name: input.name,
650
+ role,
651
+ team,
652
+ tmux_pane_id: input.tmux_pane_id,
653
+ delivery: validated?.ok,
654
+ claude_ui_pid: input.claude_ui_pid,
655
+ runtime_ui_pid: input.runtime_ui_pid
656
+ });
657
+ }
658
+ releaseConnection(agent_id, connection_id) {
659
+ for (const [k, cid] of this.connections) {
660
+ if (cid === connection_id) this.connections.delete(k);
661
+ }
662
+ void agent_id;
663
+ }
664
+ };
665
+
666
+ // src/mcp/send-message.ts
667
+ import { randomUUID as randomUUID2 } from "crypto";
668
+
669
+ // src/daemon/tmux-cli.ts
670
+ import { execFile, spawn } from "child_process";
671
+ import { promisify } from "util";
672
+ var pExecFile = promisify(execFile);
673
+ var _isTmuxAvailable = null;
674
+ async function isTmuxAvailable() {
675
+ if (_isTmuxAvailable !== null) return _isTmuxAvailable;
676
+ try {
677
+ await pExecFile("tmux", ["-V"]);
678
+ _isTmuxAvailable = true;
679
+ } catch {
680
+ _isTmuxAvailable = false;
681
+ }
682
+ return _isTmuxAvailable;
683
+ }
684
+ var TMUX_CAPTURE_TIMEOUT_MS = 5e3;
685
+ async function capturePaneTail(paneId, lines = 8) {
686
+ const { stdout } = await pExecFile(
687
+ "tmux",
688
+ ["capture-pane", "-t", paneId, "-p", "-S", `-${lines}`],
689
+ { timeout: TMUX_CAPTURE_TIMEOUT_MS }
690
+ );
691
+ return stdout;
692
+ }
693
+ function loadBuffer(bufferName, prompt) {
694
+ return new Promise((resolve, reject) => {
695
+ const child = spawn("tmux", ["load-buffer", "-b", bufferName, "-"]);
696
+ let stderr = "";
697
+ child.on("error", reject);
698
+ if (child.stderr) {
699
+ child.stderr.on("data", (b) => {
700
+ stderr += b.toString("utf8");
701
+ });
702
+ }
703
+ child.on("close", (code) => {
704
+ if (code === 0) resolve();
705
+ else reject(new Error(`load-buffer exit ${code}: ${stderr}`));
706
+ });
707
+ child.stdin.write(Buffer.from(prompt, "utf8"));
708
+ child.stdin.end();
709
+ });
710
+ }
711
+ async function pasteBuffer(bufferName, paneId) {
712
+ await pExecFile("tmux", ["paste-buffer", "-b", bufferName, "-t", paneId, "-p", "-d"]);
713
+ }
714
+ async function sendEnter(paneId) {
715
+ await pExecFile("tmux", ["send-keys", "-t", paneId, "Enter"]);
716
+ }
717
+
718
+ // src/mcp/poke-guard.ts
719
+ var DEFAULT_QUIET_MS = 2e3;
720
+ var GUARD_TAIL_LINES = 8;
721
+ var _captureImpl = capturePaneTail;
722
+ function resolveQuietMs(opt) {
723
+ if (typeof opt === "number" && Number.isInteger(opt) && opt > 0) return opt;
724
+ const raw = process.env.POKE_QUIET_MS;
725
+ if (raw === void 0) return DEFAULT_QUIET_MS;
726
+ const n = Number(raw);
727
+ return Number.isInteger(n) && n > 0 ? n : DEFAULT_QUIET_MS;
728
+ }
729
+ async function runQuietGuard(paneId, quietMs) {
730
+ const ms = resolveQuietMs(quietMs);
731
+ const before = await _captureImpl(paneId, GUARD_TAIL_LINES);
732
+ await new Promise((r) => setTimeout(r, ms));
733
+ const after = await _captureImpl(paneId, GUARD_TAIL_LINES);
734
+ return before === after ? "pass" : "fail";
735
+ }
736
+
737
+ // src/mcp/poke-retry.ts
738
+ var RETRY_DELAYS_MS = [3e4, 18e4, 6e5];
739
+ var RETRY_DELAYS_S = [30, 180, 600];
740
+ var retryMap = /* @__PURE__ */ new Map();
741
+ function keyOf(ctx) {
742
+ return `${ctx.messageId}:${ctx.agentId}`;
743
+ }
744
+ function scheduleRetry(ctx) {
745
+ const key = keyOf(ctx);
746
+ cancelRetry(key);
747
+ retryMap.set(key, { attempt: 0, ctx });
748
+ enqueueNext(key);
749
+ }
750
+ function enqueueNext(key) {
751
+ const entry = retryMap.get(key);
752
+ if (!entry) return;
753
+ if (entry.attempt >= RETRY_DELAYS_MS.length) {
754
+ retryMap.delete(key);
755
+ return;
756
+ }
757
+ const delay2 = RETRY_DELAYS_MS[entry.attempt];
758
+ entry.timer = setTimeout(() => {
759
+ void tick(key);
760
+ }, delay2);
761
+ }
762
+ async function tick(key) {
763
+ const entry = retryMap.get(key);
764
+ if (!entry) return;
765
+ const { ctx } = entry;
766
+ try {
767
+ const agent = ctx.lookupAgentFn(ctx.agentId);
768
+ if (!agent || !agent.tmux_pane_id) {
769
+ ctx.updateStatusFn?.({
770
+ agentId: ctx.agentId,
771
+ wake_status: "failed",
772
+ skip_reason: "no_pane",
773
+ retry_attempts: entry.attempt
774
+ });
775
+ retryMap.delete(key);
776
+ return;
777
+ }
778
+ if (new Date(agent.last_seen_at).getTime() > new Date(ctx.sentAt).getTime()) {
779
+ ctx.updateStatusFn?.({
780
+ agentId: ctx.agentId,
781
+ wake_status: "skipped",
782
+ skip_reason: "recipient_active",
783
+ retry_attempts: entry.attempt
784
+ });
785
+ retryMap.delete(key);
786
+ return;
787
+ }
788
+ const guard = await ctx.paneGuardFn(agent.tmux_pane_id);
789
+ if (guard === "pass") {
790
+ await ctx.pokeFn({
791
+ team: ctx.team,
792
+ fromAgentId: ctx.fromAgentId,
793
+ targetAgentId: ctx.agentId,
794
+ paneId: agent.tmux_pane_id,
795
+ body: ctx.body
796
+ });
797
+ ctx.updateStatusFn?.({
798
+ agentId: ctx.agentId,
799
+ wake_status: "delivered",
800
+ skip_reason: null,
801
+ retry_attempts: entry.attempt + 1,
802
+ delivered_at: (/* @__PURE__ */ new Date()).toISOString()
803
+ });
804
+ retryMap.delete(key);
805
+ return;
806
+ }
807
+ entry.attempt += 1;
808
+ if (entry.attempt >= RETRY_DELAYS_MS.length) {
809
+ ctx.updateStatusFn?.({
810
+ agentId: ctx.agentId,
811
+ wake_status: "failed",
812
+ skip_reason: "retry_exhausted",
813
+ retry_attempts: entry.attempt
814
+ });
815
+ retryMap.delete(key);
816
+ return;
817
+ }
818
+ ctx.updateStatusFn?.({
819
+ agentId: ctx.agentId,
820
+ wake_status: "retrying",
821
+ skip_reason: "guard_failed",
822
+ retry_attempts: entry.attempt
823
+ });
824
+ enqueueNext(key);
825
+ } catch {
826
+ ctx.updateStatusFn?.({
827
+ agentId: ctx.agentId,
828
+ wake_status: "failed",
829
+ skip_reason: "retry_exhausted",
830
+ retry_attempts: entry.attempt
831
+ });
832
+ retryMap.delete(key);
833
+ }
834
+ }
835
+ function cancelRetry(key) {
836
+ const entry = retryMap.get(key);
837
+ if (!entry) return;
838
+ if (entry.timer) clearTimeout(entry.timer);
839
+ retryMap.delete(key);
840
+ }
841
+ function clearAllRetries() {
842
+ for (const [, v] of retryMap) if (v.timer) clearTimeout(v.timer);
843
+ retryMap.clear();
844
+ }
845
+
846
+ // src/mcp/auto-poke-fanout.ts
847
+ function hasNonTmuxTransport(recipient) {
848
+ return recipient.delivery !== void 0 && recipient.delivery.kind !== "none";
849
+ }
850
+ async function fanoutAutoPoke(args) {
851
+ const pokeFn = args.deps.poke;
852
+ const tmuxAvail = args.deps.tmuxAvailable ?? isTmuxAvailable;
853
+ const results = await Promise.all(args.recipients.map(async (r) => {
854
+ try {
855
+ const nonTmuxTransport = hasNonTmuxTransport(r);
856
+ if (r.agent_id === args.fromAgentId) {
857
+ return { agent_id: r.agent_id, poked: false, reason: "self", paneId: null };
858
+ }
859
+ if (!nonTmuxTransport && !r.tmux_pane_id) {
860
+ return { agent_id: r.agent_id, poked: false, reason: "no_pane", paneId: null };
861
+ }
862
+ if (!nonTmuxTransport && !await tmuxAvail()) {
863
+ return { agent_id: r.agent_id, poked: false, reason: "tmux_unavailable", paneId: r.tmux_pane_id };
864
+ }
865
+ if (!pokeFn) {
866
+ return { agent_id: r.agent_id, poked: false, reason: "tmux_unavailable", paneId: r.tmux_pane_id };
867
+ }
868
+ if (!nonTmuxTransport) {
869
+ const guard = await runQuietGuard(r.tmux_pane_id);
870
+ if (guard === "fail") {
871
+ return { agent_id: r.agent_id, poked: false, reason: "guard_failed", paneId: r.tmux_pane_id };
872
+ }
873
+ }
874
+ const out = await pokeFn({
875
+ team: args.team,
876
+ fromAgentId: args.fromAgentId,
877
+ targetAgentId: r.agent_id,
878
+ paneId: r.tmux_pane_id,
879
+ body: args.body
880
+ });
881
+ if (out.ok) return { agent_id: r.agent_id, poked: true, reason: void 0, paneId: r.tmux_pane_id };
882
+ return {
883
+ agent_id: r.agent_id,
884
+ poked: false,
885
+ reason: out.reason ?? "guard_failed",
886
+ paneId: r.tmux_pane_id
887
+ };
888
+ } catch {
889
+ return { agent_id: r.agent_id, poked: false, reason: "guard_failed", paneId: r.tmux_pane_id };
890
+ }
891
+ }));
892
+ let retryScheduledCount = 0;
893
+ if (args.retry && pokeFn) {
894
+ const scheduleFn = args.retry.scheduleRetryFn ?? scheduleRetry;
895
+ for (const res of results) {
896
+ if (!res.poked && res.reason === "guard_failed" && res.paneId) {
897
+ scheduleFn({
898
+ agentId: res.agent_id,
899
+ messageId: args.retry.messageId,
900
+ fromAgentId: args.fromAgentId,
901
+ body: args.body,
902
+ team: args.team,
903
+ sentAt: args.retry.sentAt,
904
+ paneId: res.paneId,
905
+ paneGuardFn: runQuietGuard,
906
+ pokeFn: async (pokeArgs) => {
907
+ await pokeFn(pokeArgs);
908
+ },
909
+ lookupAgentFn: args.retry.lookupAgentFn,
910
+ updateStatusFn: args.retry.updateStatusFn
911
+ });
912
+ retryScheduledCount += 1;
913
+ }
914
+ }
915
+ }
916
+ const poked = results.some((x) => x.poked);
917
+ const skipReasons = results.filter((x) => !x.poked && x.reason !== void 0).map((x) => ({ agent_id: x.agent_id, reason: x.reason }));
918
+ const deliveredAgentIds = results.filter((x) => x.poked).map((x) => x.agent_id);
919
+ return { poked, skipReasons, deliveredAgentIds, retryScheduledCount };
920
+ }
921
+
922
+ // src/mcp/delivery-status.ts
923
+ function recordInitialDeliveryStatuses(db, args) {
924
+ const now = (/* @__PURE__ */ new Date()).toISOString();
925
+ const skipped = new Map(args.skipped.map((x) => [x.agent_id, x.reason]));
926
+ const stmt = db.prepare(
927
+ `INSERT INTO message_delivery_status
928
+ (message_id, agent_id, wake_status, skip_reason, retry_attempts, updated_at, delivered_at)
929
+ VALUES (?, ?, ?, ?, ?, ?, ?)
930
+ ON CONFLICT(message_id, agent_id) DO UPDATE SET
931
+ wake_status=excluded.wake_status,
932
+ skip_reason=excluded.skip_reason,
933
+ retry_attempts=excluded.retry_attempts,
934
+ updated_at=excluded.updated_at,
935
+ delivered_at=excluded.delivered_at`
936
+ );
937
+ const tx = db.transaction(() => {
938
+ for (const agentId of args.recipients) {
939
+ const reason = args.autoPokeDisabled ? "auto_poke_disabled" : skipped.get(agentId);
940
+ const delivered = args.delivered.has(agentId);
941
+ const status = delivered ? "delivered" : reason === "guard_failed" ? "retrying" : "skipped";
942
+ stmt.run(
943
+ args.messageId,
944
+ agentId,
945
+ status,
946
+ delivered ? null : reason,
947
+ 0,
948
+ now,
949
+ delivered ? now : null
950
+ );
951
+ }
952
+ });
953
+ tx();
954
+ }
955
+ function updateDeliveryStatus(db, messageId, agentId, args) {
956
+ const now = (/* @__PURE__ */ new Date()).toISOString();
957
+ db.prepare(
958
+ `UPDATE message_delivery_status
959
+ SET wake_status=?,
960
+ skip_reason=?,
961
+ retry_attempts=COALESCE(?, retry_attempts),
962
+ updated_at=?,
963
+ delivered_at=?
964
+ WHERE message_id=? AND agent_id=?`
965
+ ).run(
966
+ args.wake_status,
967
+ args.skip_reason ?? null,
968
+ args.retry_attempts ?? null,
969
+ now,
970
+ args.delivered_at === void 0 ? null : args.delivered_at,
971
+ messageId,
972
+ agentId
973
+ );
974
+ }
975
+ var GetDeliveryStatusService = class {
976
+ constructor(db) {
977
+ this.db = db;
978
+ }
979
+ db;
980
+ get(args) {
981
+ const owned = this.db.prepare(
982
+ `SELECT 1 AS ok FROM messages WHERE id=? AND from_agent_id=? LIMIT 1`
983
+ ).get(args.message_id, args.caller);
984
+ if (!owned) return { error: "unknown_message" };
985
+ const rows = this.db.prepare(
986
+ `SELECT agent_id, wake_status, skip_reason, retry_attempts, updated_at, delivered_at
987
+ FROM message_delivery_status
988
+ WHERE message_id=?
989
+ ORDER BY agent_id ASC`
990
+ ).all(args.message_id);
991
+ return { message_id: args.message_id, statuses: rows };
992
+ }
993
+ };
994
+
995
+ // src/mcp/fanout-with-retry.ts
996
+ async function runFanoutWithRetry(args) {
997
+ const { db } = args;
998
+ const fanout = await fanoutAutoPoke({
999
+ team: args.team,
1000
+ fromAgentId: args.fromAgentId,
1001
+ recipients: args.recipients,
1002
+ body: args.body,
1003
+ deps: args.deps,
1004
+ retry: {
1005
+ messageId: args.messageId,
1006
+ sentAt: args.sentAt,
1007
+ lookupAgentFn: (agentId) => db.prepare(
1008
+ "SELECT agent_id, tmux_pane_id, last_seen_at FROM agents WHERE agent_id=?"
1009
+ ).get(agentId),
1010
+ updateStatusFn: (status) => {
1011
+ updateDeliveryStatus(db, args.messageId, status.agentId, status);
1012
+ }
1013
+ }
1014
+ });
1015
+ recordInitialDeliveryStatuses(db, {
1016
+ messageId: args.messageId,
1017
+ recipients: args.recipients.map((r) => r.agent_id),
1018
+ delivered: new Set(fanout.deliveredAgentIds),
1019
+ skipped: fanout.skipReasons
1020
+ });
1021
+ const retry_scheduled = fanout.retryScheduledCount > 0;
1022
+ return {
1023
+ poked: fanout.poked,
1024
+ poke_skip_reasons: fanout.skipReasons,
1025
+ retry_scheduled,
1026
+ ...retry_scheduled ? { retry_delays_s: [...RETRY_DELAYS_S] } : {}
1027
+ };
1028
+ }
1029
+
1030
+ // src/mcp/send-message.ts
1031
+ var SendMessageService = class {
1032
+ constructor(db, agents, events, deps = {}) {
1033
+ this.db = db;
1034
+ this.agents = agents;
1035
+ this.events = events;
1036
+ this.deps = deps;
1037
+ }
1038
+ db;
1039
+ agents;
1040
+ events;
1041
+ deps;
1042
+ async send(input) {
1043
+ const hasId = typeof input.to_agent_id === "string" && input.to_agent_id.length > 0;
1044
+ const hasName = typeof input.to_agent_name === "string" && input.to_agent_name.length > 0;
1045
+ if (!hasId && !hasName) return { error: "missing_recipient" };
1046
+ if (hasId && hasName) return { error: "ambiguous_recipient" };
1047
+ const fromRow = this.agents.findById(input.from);
1048
+ if (!fromRow) return { error: "unknown_recipient" };
1049
+ const fromTeam = fromRow.team;
1050
+ const toTeam = input.to_team ?? fromTeam;
1051
+ let resolvedId;
1052
+ if (hasId) {
1053
+ resolvedId = input.to_agent_id;
1054
+ } else {
1055
+ const hit = this.agents.findByIdentity({ team: toTeam, name: input.to_agent_name });
1056
+ if (!hit) return { error: "unknown_recipient" };
1057
+ resolvedId = hit.agent_id;
1058
+ }
1059
+ const rcpt = this.db.prepare(
1060
+ `SELECT
1061
+ agent_id,
1062
+ team,
1063
+ tmux_pane_id,
1064
+ delivery_kind,
1065
+ delivery_payload
1066
+ FROM agents
1067
+ WHERE agent_id=?`
1068
+ ).get(resolvedId);
1069
+ if (!rcpt || rcpt.team !== toTeam) return { error: "unknown_recipient" };
1070
+ const recipientRow = {
1071
+ agent_id: rcpt.agent_id,
1072
+ tmux_pane_id: rcpt.tmux_pane_id,
1073
+ delivery: parseDeliveryRow(rcpt)
1074
+ };
1075
+ const baseResult = this.insert({ fromTeam, toTeam, from: input.from, toAgentId: rcpt.agent_id, input });
1076
+ const autoPokeEnabled = input.auto_poke !== false;
1077
+ if (!autoPokeEnabled) {
1078
+ recordInitialDeliveryStatuses(this.db, {
1079
+ messageId: baseResult.message_id,
1080
+ recipients: [rcpt.agent_id],
1081
+ delivered: /* @__PURE__ */ new Set(),
1082
+ skipped: [],
1083
+ autoPokeDisabled: true
1084
+ });
1085
+ return { ...baseResult, poked: false, retry_scheduled: false };
1086
+ }
1087
+ const envelope = await runFanoutWithRetry({
1088
+ db: this.db,
1089
+ team: toTeam,
1090
+ fromAgentId: input.from,
1091
+ recipients: [recipientRow],
1092
+ body: input.body,
1093
+ deps: this.deps,
1094
+ messageId: baseResult.message_id,
1095
+ sentAt: baseResult.sent_at
1096
+ });
1097
+ return {
1098
+ message_id: baseResult.message_id,
1099
+ event_id: baseResult.event_id,
1100
+ recipients: baseResult.recipients,
1101
+ ...envelope
1102
+ };
1103
+ }
1104
+ insert(args) {
1105
+ const tx = this.db.transaction(() => {
1106
+ const needReply = args.input.need_reply !== false ? 1 : 0;
1107
+ const event_id2 = this.events.append({
1108
+ from_team: args.fromTeam,
1109
+ to_team: args.toTeam,
1110
+ event_type: "message_sent",
1111
+ actor_agent_id: args.from,
1112
+ payload: {
1113
+ recipients: [args.toAgentId],
1114
+ subject: args.input.subject ?? null,
1115
+ need_reply: needReply === 1
1116
+ }
1117
+ });
1118
+ const sent_at2 = (/* @__PURE__ */ new Date()).toISOString();
1119
+ const id = randomUUID2();
1120
+ this.db.prepare(
1121
+ `INSERT INTO messages (id, event_id, from_team, to_team, from_agent_id, to_agent_id, to_role, subject, body, need_reply, sent_at)
1122
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`
1123
+ ).run(
1124
+ id,
1125
+ event_id2,
1126
+ args.fromTeam,
1127
+ args.toTeam,
1128
+ args.from,
1129
+ args.toAgentId,
1130
+ null,
1131
+ args.input.subject ?? null,
1132
+ args.input.body,
1133
+ needReply,
1134
+ sent_at2
1135
+ );
1136
+ return { message_id: id, event_id: event_id2, sent_at: sent_at2 };
1137
+ });
1138
+ const { message_id, event_id, sent_at } = tx();
1139
+ return { message_id, event_id, recipients: [args.toAgentId], sent_at };
1140
+ }
1141
+ };
1142
+
1143
+ // src/mcp/broadcast.ts
1144
+ import { randomUUID as randomUUID3 } from "crypto";
1145
+ var BroadcastService = class {
1146
+ constructor(db, agents, deps = {}) {
1147
+ this.db = db;
1148
+ this.agents = agents;
1149
+ this.deps = deps;
1150
+ }
1151
+ db;
1152
+ agents;
1153
+ deps;
1154
+ async broadcast(input) {
1155
+ const fromRow = this.agents.findById(input.from);
1156
+ if (!fromRow) return { error: "unknown_recipient" };
1157
+ const cutoffIso = new Date(Date.now() - ONLINE_MS).toISOString();
1158
+ const rawRows = this.db.prepare(
1159
+ `SELECT
1160
+ agent_id,
1161
+ tmux_pane_id,
1162
+ delivery_kind,
1163
+ delivery_payload
1164
+ FROM agents
1165
+ WHERE team=? AND agent_id != ? AND last_seen_at > ?`
1166
+ ).all(fromRow.team, input.from, cutoffIso);
1167
+ const rows = rawRows.map((row) => ({
1168
+ agent_id: row.agent_id,
1169
+ tmux_pane_id: row.tmux_pane_id,
1170
+ delivery: parseDeliveryRow(row)
1171
+ }));
1172
+ if (rows.length === 0) return { error: "unknown_recipient" };
1173
+ const recipients = rows.map((r) => r.agent_id);
1174
+ const baseId = randomUUID3();
1175
+ const inserted = this.insertBroadcast(fromRow.team, input.from, recipients, input.body, input.subject, baseId);
1176
+ if (input.auto_poke === false) {
1177
+ recordInitialDeliveryStatuses(this.db, {
1178
+ messageId: inserted.message_id,
1179
+ recipients,
1180
+ delivered: /* @__PURE__ */ new Set(),
1181
+ skipped: [],
1182
+ autoPokeDisabled: true
1183
+ });
1184
+ return { ...inserted, recipients, poked: false, retry_scheduled: false };
1185
+ }
1186
+ const envelope = await runFanoutWithRetry({
1187
+ db: this.db,
1188
+ team: fromRow.team,
1189
+ fromAgentId: input.from,
1190
+ recipients: rows,
1191
+ body: input.body,
1192
+ deps: this.deps,
1193
+ messageId: inserted.message_id,
1194
+ sentAt: inserted.sent_at
1195
+ });
1196
+ return {
1197
+ message_id: inserted.message_id,
1198
+ event_id: inserted.event_id,
1199
+ recipients,
1200
+ ...envelope
1201
+ };
1202
+ }
1203
+ insertBroadcast(team, from, recipients, body, subject, baseId) {
1204
+ const tx = this.db.transaction(() => {
1205
+ const event_id = Number(this.db.prepare(
1206
+ `INSERT INTO events (from_team, to_team, event_type, actor_agent_id, payload, created_at) VALUES (?,?,?,?,?,?)`
1207
+ ).run(
1208
+ team,
1209
+ team,
1210
+ "message_sent",
1211
+ from,
1212
+ JSON.stringify({ to_role: "*broadcast*", recipients, subject: subject ?? null }),
1213
+ (/* @__PURE__ */ new Date()).toISOString()
1214
+ ).lastInsertRowid);
1215
+ const sent_at = (/* @__PURE__ */ new Date()).toISOString();
1216
+ const insert = this.db.prepare(
1217
+ `INSERT INTO messages (id, event_id, from_team, to_team, from_agent_id, to_agent_id, to_role, subject, body, need_reply, sent_at)
1218
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`
1219
+ );
1220
+ for (let i = 0; i < recipients.length; i++) {
1221
+ const id = i === 0 ? baseId : `${baseId}-${i}`;
1222
+ insert.run(id, event_id, team, team, from, recipients[i], "*broadcast*", subject ?? null, body, 0, sent_at);
1223
+ }
1224
+ return { message_id: baseId, event_id, sent_at };
1225
+ });
1226
+ return tx();
1227
+ }
1228
+ };
1229
+
1230
+ // src/mcp/broadcast-to-role.ts
1231
+ import { randomUUID as randomUUID4 } from "crypto";
1232
+ var BroadcastToRoleService = class {
1233
+ constructor(db, agents, events, deps = {}) {
1234
+ this.db = db;
1235
+ this.agents = agents;
1236
+ this.events = events;
1237
+ this.deps = deps;
1238
+ }
1239
+ db;
1240
+ agents;
1241
+ events;
1242
+ deps;
1243
+ async broadcast(input) {
1244
+ const fromRow = this.agents.findById(input.from);
1245
+ if (!fromRow) return { error: "unknown_recipient" };
1246
+ const cutoffIso = new Date(Date.now() - ONLINE_MS).toISOString();
1247
+ const rawRows = this.db.prepare(
1248
+ `SELECT
1249
+ agent_id,
1250
+ tmux_pane_id,
1251
+ delivery_kind,
1252
+ delivery_payload
1253
+ FROM agents
1254
+ WHERE team=? AND role=? AND agent_id != ? AND last_seen_at > ?`
1255
+ ).all(fromRow.team, input.to_role, input.from, cutoffIso);
1256
+ const rows = rawRows.map((row) => ({
1257
+ agent_id: row.agent_id,
1258
+ tmux_pane_id: row.tmux_pane_id,
1259
+ delivery: parseDeliveryRow(row)
1260
+ }));
1261
+ if (rows.length === 0) return { error: "unknown_recipient" };
1262
+ const recipients = rows.map((r) => r.agent_id);
1263
+ const baseId = randomUUID4();
1264
+ const inserted = this.insert(fromRow.team, input, recipients, baseId);
1265
+ if (input.auto_poke === false) {
1266
+ recordInitialDeliveryStatuses(this.db, {
1267
+ messageId: inserted.message_id,
1268
+ recipients,
1269
+ delivered: /* @__PURE__ */ new Set(),
1270
+ skipped: [],
1271
+ autoPokeDisabled: true
1272
+ });
1273
+ return {
1274
+ message_id: inserted.message_id,
1275
+ event_id: inserted.event_id,
1276
+ recipients,
1277
+ poked: false,
1278
+ retry_scheduled: false
1279
+ };
1280
+ }
1281
+ const envelope = await runFanoutWithRetry({
1282
+ db: this.db,
1283
+ team: fromRow.team,
1284
+ fromAgentId: input.from,
1285
+ recipients: rows,
1286
+ body: input.body,
1287
+ deps: this.deps,
1288
+ messageId: inserted.message_id,
1289
+ sentAt: inserted.sent_at
1290
+ });
1291
+ return {
1292
+ message_id: inserted.message_id,
1293
+ event_id: inserted.event_id,
1294
+ recipients,
1295
+ ...envelope
1296
+ };
1297
+ }
1298
+ insert(team, input, recipients, baseId) {
1299
+ const tx = this.db.transaction(() => {
1300
+ const event_id = this.events.append({
1301
+ from_team: team,
1302
+ to_team: team,
1303
+ event_type: "message_sent",
1304
+ actor_agent_id: input.from,
1305
+ payload: { to_role: input.to_role, recipients, subject: input.subject ?? null }
1306
+ });
1307
+ const sent_at = (/* @__PURE__ */ new Date()).toISOString();
1308
+ const stmt = this.db.prepare(
1309
+ `INSERT INTO messages (id, event_id, from_team, to_team, from_agent_id, to_agent_id, to_role, subject, body, need_reply, sent_at)
1310
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`
1311
+ );
1312
+ for (let i = 0; i < recipients.length; i++) {
1313
+ const id = i === 0 ? baseId : `${baseId}-${i}`;
1314
+ stmt.run(id, event_id, team, team, input.from, recipients[i], input.to_role, input.subject ?? null, input.body, 0, sent_at);
1315
+ }
1316
+ return { message_id: baseId, event_id, sent_at };
1317
+ });
1318
+ return tx();
1319
+ }
1320
+ };
1321
+
1322
+ // src/mcp/get-inbox.ts
1323
+ var GetInboxService = class {
1324
+ constructor(db, agents) {
1325
+ this.db = db;
1326
+ this.agents = agents;
1327
+ }
1328
+ db;
1329
+ agents;
1330
+ get(args) {
1331
+ const caller = this.agents.findById(args.caller);
1332
+ if (!caller) return { messages: [], has_more: false, last_event_id: args.since_event_id ?? 0 };
1333
+ const callerTeam = caller.team;
1334
+ const callerRole = this.db.prepare("SELECT role FROM agents WHERE agent_id=?").get(args.caller);
1335
+ const limit = Math.min(args.limit ?? 50, 200);
1336
+ const since = args.since_event_id ?? 0;
1337
+ const rows = this.db.prepare(
1338
+ `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,
1339
+ a.role as from_role
1340
+ FROM messages m
1341
+ LEFT JOIN agents a ON a.agent_id = m.from_agent_id
1342
+ WHERE m.to_team = ?
1343
+ AND m.event_id > ?
1344
+ AND ( m.to_agent_id = ? OR (m.to_role IS NOT NULL AND m.to_role = ?) )
1345
+ ORDER BY m.event_id ASC
1346
+ LIMIT ?`
1347
+ ).all(callerTeam, since, args.caller, callerRole?.role ?? "__none__", limit + 1);
1348
+ const has_more = rows.length > limit;
1349
+ const trimmed = (has_more ? rows.slice(0, limit) : rows).map((row) => ({
1350
+ ...row,
1351
+ need_reply: row.need_reply === 1
1352
+ }));
1353
+ const last_event_id = trimmed.length > 0 ? trimmed[trimmed.length - 1].event_id : since;
1354
+ return { messages: trimmed, has_more, last_event_id };
1355
+ }
1356
+ };
1357
+
1358
+ // src/mcp/task-add.ts
1359
+ import { randomUUID as randomUUID5 } from "crypto";
1360
+ var TaskAddService = class {
1361
+ constructor(db, agents, events) {
1362
+ this.db = db;
1363
+ this.agents = agents;
1364
+ this.events = events;
1365
+ }
1366
+ db;
1367
+ agents;
1368
+ events;
1369
+ add(args) {
1370
+ const caller = this.agents.findById(args.caller);
1371
+ if (!caller) return { error: "unknown_agent" };
1372
+ const id = randomUUID5();
1373
+ const depends_on = JSON.stringify(args.depends_on ?? []);
1374
+ const created_at = (/* @__PURE__ */ new Date()).toISOString();
1375
+ const tx = this.db.transaction(() => {
1376
+ this.db.prepare(
1377
+ `INSERT INTO tasks (id, team, title, description, status, depends_on, created_at)
1378
+ VALUES (?,?,?,?, 'pending', ?, ?)`
1379
+ ).run(id, caller.team, args.title, args.description ?? null, depends_on, created_at);
1380
+ this.events.append({
1381
+ from_team: caller.team,
1382
+ to_team: caller.team,
1383
+ event_type: "task_added",
1384
+ actor_agent_id: args.caller,
1385
+ payload: { task_id: id, title: args.title }
1386
+ });
1387
+ });
1388
+ tx();
1389
+ return { task_id: id };
1390
+ }
1391
+ };
1392
+
1393
+ // src/mcp/task-claim.ts
1394
+ var TaskClaimService = class {
1395
+ constructor(db, agents, events) {
1396
+ this.db = db;
1397
+ this.agents = agents;
1398
+ this.events = events;
1399
+ }
1400
+ db;
1401
+ agents;
1402
+ events;
1403
+ claim(args) {
1404
+ const caller = this.agents.findById(args.caller);
1405
+ if (!caller) return { error: "unknown_agent" };
1406
+ const row = this.db.prepare(
1407
+ `SELECT status, claimed_by, depends_on FROM tasks WHERE id=? AND team=?`
1408
+ ).get(args.task_id, caller.team);
1409
+ if (!row) return { error: "unknown_task" };
1410
+ if (row.status !== "pending") {
1411
+ if (row.claimed_by) return { error: "already_claimed", owner: row.claimed_by };
1412
+ return { error: "already_claimed", owner: "" };
1413
+ }
1414
+ const deps = JSON.parse(row.depends_on);
1415
+ if (deps.length > 0) {
1416
+ const pending = this.db.prepare(
1417
+ `SELECT COUNT(*) as c FROM tasks WHERE id IN (${deps.map(() => "?").join(",")}) AND team=? AND status != 'completed'`
1418
+ ).get(...deps, caller.team);
1419
+ if (pending.c > 0) return { error: "dependencies_pending" };
1420
+ }
1421
+ const upd = this.db.prepare(
1422
+ `UPDATE tasks SET status='in_progress', claimed_by=?, claimed_at=?
1423
+ WHERE id=? AND team=? AND status='pending'`
1424
+ ).run(args.caller, (/* @__PURE__ */ new Date()).toISOString(), args.task_id, caller.team);
1425
+ if (upd.changes !== 1) {
1426
+ const post = this.db.prepare(`SELECT claimed_by FROM tasks WHERE id=?`).get(args.task_id);
1427
+ return { error: "already_claimed", owner: post?.claimed_by ?? "" };
1428
+ }
1429
+ this.events.append({
1430
+ from_team: caller.team,
1431
+ to_team: caller.team,
1432
+ event_type: "task_claimed",
1433
+ actor_agent_id: args.caller,
1434
+ payload: { task_id: args.task_id }
1435
+ });
1436
+ return { ok: true };
1437
+ }
1438
+ };
1439
+
1440
+ // src/mcp/task-complete.ts
1441
+ var TaskCompleteService = class {
1442
+ constructor(db, agents, events) {
1443
+ this.db = db;
1444
+ this.agents = agents;
1445
+ this.events = events;
1446
+ }
1447
+ db;
1448
+ agents;
1449
+ events;
1450
+ complete(args) {
1451
+ const caller = this.agents.findById(args.caller);
1452
+ if (!caller) return { error: "unknown_agent" };
1453
+ const row = this.db.prepare(`SELECT status, claimed_by FROM tasks WHERE id=? AND team=?`).get(args.task_id, caller.team);
1454
+ if (!row) return { error: "unknown_task" };
1455
+ if (row.status !== "in_progress") return { error: "invalid_status" };
1456
+ if (row.claimed_by !== args.caller) return { error: "not_owner" };
1457
+ const upd = this.db.prepare(
1458
+ `UPDATE tasks SET status='completed', completed_at=?, result=?
1459
+ WHERE id=? AND team=? AND claimed_by=? AND status='in_progress'`
1460
+ ).run((/* @__PURE__ */ new Date()).toISOString(), args.result ?? null, args.task_id, caller.team, args.caller);
1461
+ if (upd.changes !== 1) return { error: "invalid_status" };
1462
+ this.events.append({
1463
+ from_team: caller.team,
1464
+ to_team: caller.team,
1465
+ event_type: "task_completed",
1466
+ actor_agent_id: args.caller,
1467
+ payload: { task_id: args.task_id, result: args.result ?? null }
1468
+ });
1469
+ return { ok: true };
1470
+ }
1471
+ };
1472
+
1473
+ // src/mcp/task-list.ts
1474
+ var TaskListService = class {
1475
+ constructor(db, agents) {
1476
+ this.db = db;
1477
+ this.agents = agents;
1478
+ }
1479
+ db;
1480
+ agents;
1481
+ list(args) {
1482
+ const caller = this.agents.findById(args.caller);
1483
+ if (!caller) return { tasks: [] };
1484
+ const sql = args.status ? `SELECT * FROM tasks WHERE team=? AND status=? ORDER BY created_at ASC` : `SELECT * FROM tasks WHERE team=? ORDER BY created_at ASC`;
1485
+ const rows = args.status ? this.db.prepare(sql).all(caller.team, args.status) : this.db.prepare(sql).all(caller.team);
1486
+ const tasks = rows.map((r) => ({ ...r, depends_on: JSON.parse(r.depends_on) }));
1487
+ return { tasks };
1488
+ }
1489
+ };
1490
+
1491
+ // src/lib/schema-diff.ts
1492
+ function typeSummary(s) {
1493
+ if (!s) return "unknown";
1494
+ if (typeof s.type === "string") return s.type;
1495
+ return "unknown";
1496
+ }
1497
+ function isRequired(parent, key) {
1498
+ return Array.isArray(parent.required) && parent.required.includes(key);
1499
+ }
1500
+ function walk(fromParent, toParent, basePath, added, removed, changed) {
1501
+ const fp = fromParent.properties ?? {};
1502
+ const tp = toParent.properties ?? {};
1503
+ const keys = /* @__PURE__ */ new Set([...Object.keys(fp), ...Object.keys(tp)]);
1504
+ for (const key of keys) {
1505
+ const path = `${basePath}/properties/${key}`;
1506
+ const fromChild = fp[key];
1507
+ const toChild = tp[key];
1508
+ if (fromChild && !toChild) {
1509
+ removed.push({ path, type_summary: typeSummary(fromChild) });
1510
+ continue;
1511
+ }
1512
+ if (!fromChild && toChild) {
1513
+ added.push({ path, type_summary: typeSummary(toChild) });
1514
+ continue;
1515
+ }
1516
+ if (fromChild && toChild) {
1517
+ const fromType = typeof fromChild.type === "string" ? fromChild.type : void 0;
1518
+ const toType = typeof toChild.type === "string" ? toChild.type : void 0;
1519
+ const fromReq = isRequired(fromParent, key);
1520
+ const toReq = isRequired(toParent, key);
1521
+ const typeDiff = fromType !== toType;
1522
+ const reqDiff = fromReq !== toReq;
1523
+ if (typeDiff || reqDiff) {
1524
+ changed.push({
1525
+ path,
1526
+ from: { type: fromType, required: fromReq, enum: fromChild.enum, raw: fromChild },
1527
+ to: { type: toType, required: toReq, enum: toChild.enum, raw: toChild }
1528
+ });
1529
+ }
1530
+ if (toChild.type === "object" || fromChild.type === "object") {
1531
+ walk(fromChild, toChild, path, added, removed, changed);
1532
+ }
1533
+ }
1534
+ }
1535
+ }
1536
+ function diffSchema(from, to) {
1537
+ const added = [];
1538
+ const removed = [];
1539
+ const changed = [];
1540
+ walk(from, to, "", added, removed, changed);
1541
+ const breaking = removed.length > 0 || changed.some((c) => c.from.required === false && c.to.required === true) || changed.some((c) => !!c.from.type && !!c.to.type && c.from.type !== c.to.type);
1542
+ return { added_fields: added, removed_fields: removed, changed_fields: changed, breaking };
1543
+ }
1544
+
1545
+ // src/mcp/register-contract.ts
1546
+ var RegisterContractService = class {
1547
+ constructor(db, agents, events) {
1548
+ this.db = db;
1549
+ this.agents = agents;
1550
+ this.events = events;
1551
+ }
1552
+ db;
1553
+ agents;
1554
+ events;
1555
+ register(args) {
1556
+ const caller = this.agents.findById(args.caller);
1557
+ if (!caller) return { error: "unknown_agent" };
1558
+ const format = args.format ?? "jsonschema";
1559
+ if (format !== "jsonschema") return { error: "invalid_format" };
1560
+ const txFn = this.db.transaction(() => {
1561
+ const prev = this.db.prepare(
1562
+ `SELECT schema, version FROM contracts WHERE team=? AND name=? ORDER BY version DESC LIMIT 1`
1563
+ ).get(caller.team, args.name);
1564
+ const version = prev ? prev.version + 1 : 1;
1565
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1566
+ this.db.prepare(
1567
+ `INSERT INTO contracts (team, name, version, format, schema, note, registered_by, registered_at)
1568
+ VALUES (?,?,?,?,?,?,?,?)`
1569
+ ).run(caller.team, args.name, version, format, JSON.stringify(args.schema), args.note ?? null, args.caller, now);
1570
+ let diff;
1571
+ if (prev) diff = diffSchema(JSON.parse(prev.schema), args.schema);
1572
+ const event_id = this.events.append({
1573
+ from_team: caller.team,
1574
+ to_team: caller.team,
1575
+ event_type: "contract_registered",
1576
+ actor_agent_id: args.caller,
1577
+ payload: { name: args.name, version, diff: diff ?? null }
1578
+ });
1579
+ const meta = { team: caller.team, event_id, diff: diff ?? null };
1580
+ return prev ? { name: args.name, version, diff, _meta: meta } : { name: args.name, version, _meta: meta };
1581
+ });
1582
+ return txFn.immediate();
1583
+ }
1584
+ };
1585
+
1586
+ // src/mcp/subscribe-contract.ts
1587
+ var SubscribeContractService = class {
1588
+ constructor(db, agents) {
1589
+ this.db = db;
1590
+ this.agents = agents;
1591
+ }
1592
+ db;
1593
+ agents;
1594
+ subscribe(args) {
1595
+ const caller = this.agents.findById(args.caller);
1596
+ if (!caller) return { error: "unknown_agent" };
1597
+ this.db.prepare(
1598
+ `INSERT INTO contract_subscriptions (agent_id, team, contract_name, subscribed_at)
1599
+ VALUES (?,?,?,?)
1600
+ ON CONFLICT(agent_id, team, contract_name) DO UPDATE SET subscribed_at=excluded.subscribed_at`
1601
+ ).run(args.caller, caller.team, args.name, (/* @__PURE__ */ new Date()).toISOString());
1602
+ const latest = this.db.prepare(
1603
+ "SELECT MAX(version) AS v FROM contracts WHERE team=? AND name=?"
1604
+ ).get(caller.team, args.name);
1605
+ return { ok: true, current_version: latest.v ?? null };
1606
+ }
1607
+ };
1608
+
1609
+ // src/mcp/get-contract.ts
1610
+ var GetContractService = class {
1611
+ constructor(db, agents) {
1612
+ this.db = db;
1613
+ this.agents = agents;
1614
+ }
1615
+ db;
1616
+ agents;
1617
+ get(args) {
1618
+ const caller = this.agents.findById(args.caller);
1619
+ if (!caller) return { error: "unknown_agent" };
1620
+ const row = args.version ? this.db.prepare("SELECT * FROM contracts WHERE team=? AND name=? AND version=?").get(caller.team, args.name, args.version) : this.db.prepare("SELECT * FROM contracts WHERE team=? AND name=? ORDER BY version DESC LIMIT 1").get(caller.team, args.name);
1621
+ if (!row) {
1622
+ const exists = this.db.prepare("SELECT 1 FROM contracts WHERE team=? AND name=? LIMIT 1").get(caller.team, args.name);
1623
+ return exists ? { error: "unknown_version" } : { error: "unknown_contract" };
1624
+ }
1625
+ const r = row;
1626
+ return { name: r.name, version: r.version, schema: JSON.parse(r.schema), format: r.format, note: r.note, registered_at: r.registered_at };
1627
+ }
1628
+ };
1629
+
1630
+ // src/mcp/diff-contracts.ts
1631
+ var DiffContractsService = class {
1632
+ constructor(db, agents) {
1633
+ this.db = db;
1634
+ this.agents = agents;
1635
+ }
1636
+ db;
1637
+ agents;
1638
+ diff(args) {
1639
+ const caller = this.agents.findById(args.caller);
1640
+ if (!caller) return { error: "unknown_agent" };
1641
+ const from = this.db.prepare("SELECT schema FROM contracts WHERE team=? AND name=? AND version=?").get(caller.team, args.name, args.from_version);
1642
+ const to = this.db.prepare("SELECT schema FROM contracts WHERE team=? AND name=? AND version=?").get(caller.team, args.name, args.to_version);
1643
+ if (!from || !to) {
1644
+ const exists = this.db.prepare("SELECT 1 FROM contracts WHERE team=? AND name=? LIMIT 1").get(caller.team, args.name);
1645
+ return exists ? { error: "unknown_version" } : { error: "unknown_contract" };
1646
+ }
1647
+ return diffSchema(JSON.parse(from.schema), JSON.parse(to.schema));
1648
+ }
1649
+ };
1650
+
1651
+ // src/mcp/pending-contract-events.ts
1652
+ var PendingContractEventsService = class {
1653
+ constructor(db, agents) {
1654
+ this.db = db;
1655
+ this.agents = agents;
1656
+ }
1657
+ db;
1658
+ agents;
1659
+ poll(args) {
1660
+ const caller = this.agents.findById(args.caller);
1661
+ if (!caller) return { events: [], has_more: false, last_event_id: args.since_event_id ?? 0 };
1662
+ const limit = Math.min(args.limit ?? 100, 500);
1663
+ const since = args.since_event_id ?? 0;
1664
+ const rows = this.db.prepare(
1665
+ `SELECT event_id, payload, created_at FROM events
1666
+ WHERE to_team=? AND event_type='contract_registered' AND event_id > ?
1667
+ ORDER BY event_id ASC LIMIT ?`
1668
+ ).all(caller.team, since, limit + 1);
1669
+ const has_more = rows.length > limit;
1670
+ const trimmed = has_more ? rows.slice(0, limit) : rows;
1671
+ const events = trimmed.map((r) => {
1672
+ const p = JSON.parse(r.payload);
1673
+ return { event_id: r.event_id, contract_name: p.name, version: p.version, diff: p.diff, registered_at: r.created_at };
1674
+ });
1675
+ const last_event_id = trimmed.length > 0 ? trimmed[trimmed.length - 1].event_id : since;
1676
+ return { events, has_more, last_event_id };
1677
+ }
1678
+ };
1679
+
1680
+ // src/mcp/poke.ts
1681
+ import { randomBytes } from "crypto";
1682
+
1683
+ // src/daemon/channel-wake-send.ts
1684
+ var META_KEY_RE = /^[A-Za-z0-9_]+$/;
1685
+ function sanitizeMeta(meta) {
1686
+ const out = {};
1687
+ for (const [k, v] of Object.entries(meta)) {
1688
+ if (META_KEY_RE.test(k)) out[k] = v;
1689
+ }
1690
+ return out;
1691
+ }
1692
+ function sendChannelWake(fanout, channel_session_id, input) {
1693
+ if (!fanout.has(channel_session_id)) return { ok: false, reason: "no_subscriber" };
1694
+ const payload = {
1695
+ jsonrpc: "2.0",
1696
+ method: "notifications/channel_wake",
1697
+ params: {
1698
+ content: input.content,
1699
+ meta: sanitizeMeta(input.meta)
1700
+ }
1701
+ };
1702
+ fanout.send(channel_session_id, payload);
1703
+ return { ok: true };
1704
+ }
1705
+
1706
+ // src/mcp/codex-appserver-rpc.ts
1707
+ function defaultWebSocketFactory(args) {
1708
+ const ctor = globalThis.WebSocket;
1709
+ return new ctor(
1710
+ args.url,
1711
+ args.headers === void 0 ? void 0 : { headers: args.headers }
1712
+ );
1713
+ }
1714
+ function describeError(error) {
1715
+ if (error instanceof Error && error.message.length > 0) return error.message;
1716
+ if (typeof error === "string" && error.length > 0) return error;
1717
+ if (error && typeof error === "object") {
1718
+ const record = error;
1719
+ const message = record.message;
1720
+ if (typeof message === "string" && message.length > 0) return message;
1721
+ const reason = record.reason;
1722
+ if (typeof reason === "string" && reason.length > 0) return reason;
1723
+ }
1724
+ return String(error);
1725
+ }
1726
+ function closeDetail(event) {
1727
+ const code = typeof event.code === "number" ? event.code : "unknown";
1728
+ const reason = typeof event.reason === "string" && event.reason.length > 0 ? event.reason : "socket_closed";
1729
+ return `close ${code}: ${reason}`;
1730
+ }
1731
+ function decodeMessageData(data) {
1732
+ if (typeof data === "string") return data;
1733
+ if (data instanceof ArrayBuffer) {
1734
+ return new TextDecoder().decode(new Uint8Array(data));
1735
+ }
1736
+ if (ArrayBuffer.isView(data)) {
1737
+ return new TextDecoder().decode(data);
1738
+ }
1739
+ return String(data);
1740
+ }
1741
+ function safeClose(ws) {
1742
+ try {
1743
+ ws.close();
1744
+ } catch {
1745
+ return;
1746
+ }
1747
+ }
1748
+ function resolveAuthToken(authTokenRef, env) {
1749
+ if (authTokenRef === void 0) return { ok: void 0 };
1750
+ const token = env[authTokenRef]?.trim();
1751
+ if (!token) {
1752
+ return {
1753
+ error: "missing_auth_token",
1754
+ detail: { ref: authTokenRef }
1755
+ };
1756
+ }
1757
+ return { ok: token };
1758
+ }
1759
+ var JsonRpcSocketClient = class {
1760
+ constructor(ws) {
1761
+ this.ws = ws;
1762
+ this.openState = {
1763
+ kind: "pending",
1764
+ promise: new Promise((resolve, reject) => {
1765
+ const onOpen = () => {
1766
+ cleanup();
1767
+ this.openState = { kind: "open" };
1768
+ resolve();
1769
+ };
1770
+ const onError = (event) => {
1771
+ cleanup();
1772
+ const detail = event;
1773
+ const error = detail.error ?? detail.message ?? "websocket_error";
1774
+ this.openState = { kind: "failed", error };
1775
+ reject(error);
1776
+ };
1777
+ const onClose = (event) => {
1778
+ cleanup();
1779
+ const closeEvent = event;
1780
+ const error = closeDetail(closeEvent);
1781
+ this.openState = { kind: "failed", error };
1782
+ reject(error);
1783
+ };
1784
+ const cleanup = () => {
1785
+ this.ws.removeEventListener?.("open", onOpen);
1786
+ this.ws.removeEventListener?.("error", onError);
1787
+ this.ws.removeEventListener?.("close", onClose);
1788
+ };
1789
+ this.ws.addEventListener("open", onOpen);
1790
+ this.ws.addEventListener("error", onError);
1791
+ this.ws.addEventListener("close", onClose);
1792
+ })
1793
+ };
1794
+ this.ws.addEventListener("message", (event) => {
1795
+ let message;
1796
+ try {
1797
+ message = JSON.parse(decodeMessageData(event.data));
1798
+ } catch {
1799
+ return;
1800
+ }
1801
+ if (typeof message.id !== "number") return;
1802
+ const pending = this.pending.get(message.id);
1803
+ if (!pending) return;
1804
+ this.pending.delete(message.id);
1805
+ pending.resolve(message);
1806
+ });
1807
+ this.ws.addEventListener("error", (event) => {
1808
+ if (this.openState.kind !== "open") return;
1809
+ const detail = event;
1810
+ const error = detail.error ?? detail.message ?? "websocket_error";
1811
+ this.rejectAll(error);
1812
+ });
1813
+ this.ws.addEventListener("close", (event) => {
1814
+ if (this.openState.kind !== "open") return;
1815
+ this.rejectAll(closeDetail(event));
1816
+ });
1817
+ }
1818
+ ws;
1819
+ nextId = 1;
1820
+ pending = /* @__PURE__ */ new Map();
1821
+ openState;
1822
+ async waitForOpen() {
1823
+ if (this.openState.kind === "open") return;
1824
+ if (this.openState.kind === "failed") throw this.openState.error;
1825
+ await this.openState.promise;
1826
+ }
1827
+ request(method, params) {
1828
+ const id = this.nextId++;
1829
+ const request = { jsonrpc: "2.0", id, method, params };
1830
+ return new Promise((resolve, reject) => {
1831
+ this.pending.set(id, { resolve, reject });
1832
+ try {
1833
+ this.ws.send(JSON.stringify(request));
1834
+ } catch (error) {
1835
+ this.pending.delete(id);
1836
+ reject(error);
1837
+ }
1838
+ });
1839
+ }
1840
+ notify(method, params) {
1841
+ const notification = {
1842
+ jsonrpc: "2.0",
1843
+ method,
1844
+ ...params === void 0 ? {} : { params }
1845
+ };
1846
+ this.ws.send(JSON.stringify(notification));
1847
+ }
1848
+ rejectAll(error) {
1849
+ const pending = [...this.pending.values()];
1850
+ this.pending.clear();
1851
+ for (const entry of pending) {
1852
+ entry.reject(error);
1853
+ }
1854
+ }
1855
+ };
1856
+
1857
+ // src/mcp/codex-appserver-dispatch.ts
1858
+ async function requestStep(client, method, params) {
1859
+ try {
1860
+ const response = await client.request(method, params);
1861
+ if (response.error) {
1862
+ const mappedError = method === "initialize" ? "codex_initialize_failed" : method === "thread/resume" ? "codex_resume_failed" : "codex_turn_start_failed";
1863
+ return { error: mappedError, detail: response.error };
1864
+ }
1865
+ return { ok: response };
1866
+ } catch (error) {
1867
+ const mappedError = method === "initialize" ? "codex_initialize_failed" : method === "thread/resume" ? "codex_resume_failed" : "codex_turn_start_failed";
1868
+ return { error: mappedError, detail: describeError(error) };
1869
+ }
1870
+ }
1871
+ async function dispatchCodexAppserverPoke(input, deps = {}) {
1872
+ const authToken = resolveAuthToken(
1873
+ input.delivery.auth_token_ref,
1874
+ deps.env ?? process.env
1875
+ );
1876
+ if ("error" in authToken) return authToken;
1877
+ const headers = authToken.ok === void 0 ? void 0 : { Authorization: `Bearer ${authToken.ok}` };
1878
+ let ws;
1879
+ try {
1880
+ ws = (deps.webSocketFactory ?? defaultWebSocketFactory)({
1881
+ url: input.delivery.ws_url,
1882
+ headers
1883
+ });
1884
+ } catch (error) {
1885
+ return {
1886
+ error: "codex_connect_failed",
1887
+ detail: describeError(error),
1888
+ transport_used: "codex-appserver"
1889
+ };
1890
+ }
1891
+ const client = new JsonRpcSocketClient(ws);
1892
+ try {
1893
+ await client.waitForOpen();
1894
+ const init = await requestStep(client, "initialize", {
1895
+ clientInfo: {
1896
+ name: "cross-agent-teams-mcp",
1897
+ title: null,
1898
+ version: "0.1.0"
1899
+ },
1900
+ capabilities: {
1901
+ experimentalApi: true,
1902
+ optOutNotificationMethods: null
1903
+ }
1904
+ });
1905
+ if ("error" in init) {
1906
+ return {
1907
+ error: init.error,
1908
+ detail: init.detail,
1909
+ transport_used: "codex-appserver"
1910
+ };
1911
+ }
1912
+ client.notify("initialized");
1913
+ const resume = await requestStep(client, "thread/resume", {
1914
+ threadId: input.delivery.thread_id,
1915
+ persistExtendedHistory: false
1916
+ });
1917
+ if ("error" in resume) {
1918
+ return {
1919
+ error: resume.error,
1920
+ detail: resume.detail,
1921
+ transport_used: "codex-appserver"
1922
+ };
1923
+ }
1924
+ const turnStart = await requestStep(client, "turn/start", {
1925
+ threadId: input.delivery.thread_id,
1926
+ input: [{ type: "text", text: input.content, text_elements: [] }]
1927
+ });
1928
+ if ("error" in turnStart) {
1929
+ return {
1930
+ error: turnStart.error,
1931
+ detail: turnStart.detail,
1932
+ transport_used: "codex-appserver"
1933
+ };
1934
+ }
1935
+ return {
1936
+ ok: true,
1937
+ transport_used: "codex-appserver",
1938
+ thread_id: input.delivery.thread_id
1939
+ };
1940
+ } catch (error) {
1941
+ return {
1942
+ error: "codex_connect_failed",
1943
+ detail: describeError(error),
1944
+ transport_used: "codex-appserver"
1945
+ };
1946
+ } finally {
1947
+ safeClose(ws);
1948
+ }
1949
+ }
1950
+
1951
+ // src/mcp/transport-dispatch.ts
1952
+ async function dispatchPoke(deps, target, input) {
1953
+ const client = resolveClient(target);
1954
+ if (client === "claude-code") return dispatchClaude(deps, target, input);
1955
+ if (client === "codex") return dispatchCodex(deps, target, input);
1956
+ return dispatchUnknown(deps, target, input);
1957
+ }
1958
+ function resolveClient(target) {
1959
+ if (target.client) return target.client;
1960
+ if (target.delivery.kind === "claude-channel") return "claude-code";
1961
+ if (target.delivery.kind === "codex-appserver") return "codex";
1962
+ return null;
1963
+ }
1964
+ async function dispatchTmux(deps, paneId, content) {
1965
+ const tmuxResult = await deps.tmuxPoke({ pane_id: paneId, content });
1966
+ if ("ok" in tmuxResult && tmuxResult.ok) {
1967
+ return {
1968
+ ok: true,
1969
+ transport_used: "tmux-poke",
1970
+ pane_id: paneId,
1971
+ pane_tail_before: tmuxResult.pane_tail_before,
1972
+ pane_tail_after: tmuxResult.pane_tail_after
1973
+ };
1974
+ }
1975
+ return {
1976
+ ...tmuxResult,
1977
+ transport_used: "tmux-poke"
1978
+ };
1979
+ }
1980
+ async function dispatchClaude(deps, target, input) {
1981
+ const paneId = target.tmux_pane_id;
1982
+ const channelSubscribed = target.delivery.kind === "claude-channel" && (deps.channelWakeFanout?.has(target.delivery.channel_session_id) ?? false);
1983
+ if (target.delivery.kind === "claude-channel" && channelSubscribed && deps.channelWakeFanout) {
1984
+ const result = sendChannelWake(
1985
+ deps.channelWakeFanout,
1986
+ target.delivery.channel_session_id,
1987
+ input
1988
+ );
1989
+ if (result.ok) {
1990
+ return {
1991
+ ok: true,
1992
+ transport_used: "claude-channel",
1993
+ channel_session_id: target.delivery.channel_session_id
1994
+ };
1995
+ }
1996
+ }
1997
+ if (paneId) return dispatchTmux(deps, paneId, input.content);
1998
+ return {
1999
+ error: "no_transport_available",
2000
+ detail: {
2001
+ channel_subscribed: channelSubscribed,
2002
+ tmux_pane_set: false
2003
+ }
2004
+ };
2005
+ }
2006
+ async function dispatchCodex(deps, target, input) {
2007
+ const paneId = target.tmux_pane_id;
2008
+ if (target.delivery.kind === "codex-appserver") {
2009
+ const result = await (deps.codexAppserverDispatch ?? dispatchCodexAppserverPoke)({
2010
+ delivery: target.delivery,
2011
+ content: input.content
2012
+ });
2013
+ if ("ok" in result && result.ok) return result;
2014
+ if (paneId) return dispatchTmux(deps, paneId, input.content);
2015
+ return result;
2016
+ }
2017
+ if (paneId) return dispatchTmux(deps, paneId, input.content);
2018
+ return {
2019
+ error: "no_transport_available",
2020
+ detail: {
2021
+ codex_bound: false,
2022
+ tmux_pane_set: false
2023
+ }
2024
+ };
2025
+ }
2026
+ async function dispatchUnknown(deps, target, input) {
2027
+ const paneId = target.tmux_pane_id;
2028
+ if (paneId) return dispatchTmux(deps, paneId, input.content);
2029
+ return {
2030
+ error: "no_transport_available",
2031
+ detail: {
2032
+ channel_subscribed: false,
2033
+ tmux_pane_set: false
2034
+ }
2035
+ };
2036
+ }
2037
+
2038
+ // src/mcp/poke.ts
2039
+ var PROMPT_MAX_BYTES = 8192;
2040
+ var PASTE_SETTLE_MS = 400;
2041
+ var TAIL_LINES = 8;
2042
+ function delay(ms) {
2043
+ return new Promise((resolve) => setTimeout(resolve, ms));
2044
+ }
2045
+ function errorMessage(cause) {
2046
+ if (cause && typeof cause === "object") {
2047
+ const err = cause;
2048
+ if (err.stderr) {
2049
+ const s = typeof err.stderr === "string" ? err.stderr : err.stderr.toString("utf8");
2050
+ if (s.length > 0) return s;
2051
+ }
2052
+ if (err.message) return err.message;
2053
+ }
2054
+ return String(cause);
2055
+ }
2056
+ function classifyTmuxError(err) {
2057
+ const msg = errorMessage(err.cause);
2058
+ const lower = msg.toLowerCase();
2059
+ if (lower.includes("can't find pane") || lower.includes("pane not found") || lower.includes("no such pane")) {
2060
+ return { error: "pane_dead", detail: msg };
2061
+ }
2062
+ return { error: "tmux_cmd_failed", detail: { stage: err.stage, stderr: msg } };
2063
+ }
2064
+ async function runStage(stage, fn) {
2065
+ try {
2066
+ return await fn();
2067
+ } catch (cause) {
2068
+ throw { stage, cause };
2069
+ }
2070
+ }
2071
+ async function tmuxPokeImpl(args) {
2072
+ if (!await isTmuxAvailable()) {
2073
+ return { error: "tmux_unavailable", detail: "tmux binary not available on PATH" };
2074
+ }
2075
+ const bufName = `poke-${randomBytes(3).toString("hex")}`;
2076
+ try {
2077
+ const pane_tail_before = await runStage("capture_before", () => capturePaneTail(args.pane_id, TAIL_LINES));
2078
+ await runStage("load_buffer", () => loadBuffer(bufName, args.content));
2079
+ await runStage("paste_buffer", () => pasteBuffer(bufName, args.pane_id));
2080
+ await delay(PASTE_SETTLE_MS);
2081
+ await runStage("send_keys", () => sendEnter(args.pane_id));
2082
+ await delay(PASTE_SETTLE_MS);
2083
+ const pane_tail_after = await runStage("capture_after", () => capturePaneTail(args.pane_id, TAIL_LINES));
2084
+ return { ok: true, pane_tail_before, pane_tail_after };
2085
+ } catch (e) {
2086
+ return classifyTmuxError(e);
2087
+ }
2088
+ }
2089
+ async function poke(deps, input) {
2090
+ if (!deps.callerAgentId) return { error: "unknown_agent" };
2091
+ const promptLen = Buffer.byteLength(input.prompt, "utf8");
2092
+ if (promptLen > PROMPT_MAX_BYTES) {
2093
+ return { error: "prompt_too_long", detail: { max: PROMPT_MAX_BYTES, got: promptLen } };
2094
+ }
2095
+ const target = deps.db.prepare(
2096
+ `SELECT
2097
+ agent_id,
2098
+ client,
2099
+ team,
2100
+ tmux_pane_id,
2101
+ delivery_kind,
2102
+ delivery_payload
2103
+ FROM agents
2104
+ WHERE agent_id = ?`
2105
+ ).get(input.target_agent_id);
2106
+ if (!target) return { error: "unknown_target" };
2107
+ if (target.agent_id === deps.callerAgentId) return { error: "self_poke_denied" };
2108
+ const callerRow = deps.db.prepare(`SELECT team FROM agents WHERE agent_id = ?`).get(deps.callerAgentId);
2109
+ if (!callerRow) return { error: "unknown_agent" };
2110
+ if (callerRow.team !== target.team && !deps.allowCrossTeam) {
2111
+ return { error: "cross_team_denied" };
2112
+ }
2113
+ const fanout = deps.channelWakeFanout;
2114
+ const delivery = parseDeliveryRow(target);
2115
+ if (!fanout) {
2116
+ if (delivery.kind === "codex-appserver") {
2117
+ return dispatchPoke(
2118
+ { tmuxPoke: tmuxPokeImpl },
2119
+ { client: target.client, delivery, tmux_pane_id: target.tmux_pane_id },
2120
+ { content: input.prompt, meta: {} }
2121
+ );
2122
+ }
2123
+ if (!target.tmux_pane_id) return { error: "tmux_pane_not_set" };
2124
+ const tr = await tmuxPokeImpl({ pane_id: target.tmux_pane_id, content: input.prompt });
2125
+ if ("ok" in tr && tr.ok) {
2126
+ return {
2127
+ ok: true,
2128
+ transport_used: "tmux-poke",
2129
+ pane_id: target.tmux_pane_id,
2130
+ pane_tail_before: tr.pane_tail_before,
2131
+ pane_tail_after: tr.pane_tail_after
2132
+ };
2133
+ }
2134
+ return { ...tr, transport_used: "tmux-poke" };
2135
+ }
2136
+ return dispatchPoke(
2137
+ { channelWakeFanout: fanout, tmuxPoke: tmuxPokeImpl },
2138
+ { client: target.client, delivery, tmux_pane_id: target.tmux_pane_id },
2139
+ { content: input.prompt, meta: {} }
2140
+ );
2141
+ }
2142
+
2143
+ // src/daemon/errors.ts
2144
+ var STORAGE_CODES = /* @__PURE__ */ new Set(["SQLITE_FULL", "SQLITE_BUSY", "SQLITE_IOERR", "SQLITE_LOCKED", "SQLITE_READONLY"]);
2145
+ function isStorageError(err) {
2146
+ if (!err || typeof err !== "object") return false;
2147
+ const anyErr = err;
2148
+ if (anyErr.name === "SqliteError") return true;
2149
+ if (anyErr.code && STORAGE_CODES.has(anyErr.code)) return true;
2150
+ return false;
2151
+ }
2152
+ async function wrapStorage(fn) {
2153
+ try {
2154
+ return await fn();
2155
+ } catch (err) {
2156
+ if (isStorageError(err)) return { error: "storage_unavailable" };
2157
+ throw err;
2158
+ }
2159
+ }
2160
+
2161
+ // src/mcp/subscribe-channel-wake.ts
2162
+ var CHANNEL_PROXY_ROLE = "__channel_proxy__";
2163
+ var SubscribeChannelWakeService = class {
2164
+ constructor(db, fanout) {
2165
+ this.db = db;
2166
+ this.fanout = fanout;
2167
+ }
2168
+ db;
2169
+ fanout;
2170
+ subscribe(input) {
2171
+ const csid = input.channel_session_id?.trim();
2172
+ if (!csid) return { error: "invalid_channel_session_id" };
2173
+ const row = this.db.prepare(`SELECT role FROM agents WHERE agent_id=?`).get(input.callerAgentId);
2174
+ if (!row) return { error: "unknown_agent" };
2175
+ if (row.role !== CHANNEL_PROXY_ROLE) return { error: "forbidden_role" };
2176
+ this.fanout.attach(csid, input.sink, input.sessionId);
2177
+ return { ok: true };
2178
+ }
2179
+ };
2180
+
2181
+ // src/mcp/bind-channel.ts
2182
+ var BindChannelService = class {
2183
+ constructor(db, fanout) {
2184
+ this.fanout = fanout;
2185
+ this.repo = new AgentsRepo(db);
2186
+ }
2187
+ fanout;
2188
+ repo;
2189
+ bind(input) {
2190
+ const csid = input.channel_session_id?.trim();
2191
+ if (!csid) return { error: "invalid_channel_session_id" };
2192
+ const caller = this.repo.getById(input.callerAgentId);
2193
+ if (!caller) return { error: "unknown_agent" };
2194
+ if (caller.role === CHANNEL_PROXY_ROLE) return { error: "forbidden_role" };
2195
+ if (!this.fanout.has(csid)) return { error: "unknown_channel_session" };
2196
+ this.repo.setClient(input.callerAgentId, "claude-code");
2197
+ this.repo.setDelivery(input.callerAgentId, {
2198
+ kind: "claude-channel",
2199
+ channel_session_id: csid
2200
+ });
2201
+ return { ok: true };
2202
+ }
2203
+ };
2204
+
2205
+ // src/mcp/auto-bind-channel.ts
2206
+ var LIVE_WINDOW_MS = 5 * 60 * 1e3;
2207
+ var AutoBindChannelService = class {
2208
+ constructor(db, fanout) {
2209
+ this.db = db;
2210
+ this.fanout = fanout;
2211
+ }
2212
+ db;
2213
+ fanout;
2214
+ lookup(input) {
2215
+ return this.findLiveProxyCsid(input);
2216
+ }
2217
+ run(input) {
2218
+ const found = this.findLiveProxyCsid({ ui_pid: input.ui_pid });
2219
+ if (!found.ok) return found;
2220
+ const csid = found.channel_session_id;
2221
+ if (!this.fanout.has(csid)) return { ok: false, reason: "sink_not_live" };
2222
+ this.db.prepare(
2223
+ `UPDATE agents
2224
+ SET delivery_kind = 'claude-channel',
2225
+ delivery_payload = json_object('channel_session_id', ?)
2226
+ WHERE agent_id = ?`
2227
+ ).run(csid, input.callerAgentId);
2228
+ return { ok: true, channel_session_id: csid };
2229
+ }
2230
+ findLiveProxyCsid(input) {
2231
+ const cutoff = new Date(Date.now() - LIVE_WINDOW_MS).toISOString();
2232
+ const row = this.db.prepare(
2233
+ `SELECT delivery_payload
2234
+ FROM agents
2235
+ WHERE role = ?
2236
+ AND claude_ui_pid = ?
2237
+ AND last_seen_at > ?
2238
+ ORDER BY last_seen_at DESC
2239
+ LIMIT 1`
2240
+ ).get(CHANNEL_PROXY_ROLE, input.ui_pid, cutoff);
2241
+ if (!row) return { ok: false, reason: "no_proxy_row" };
2242
+ const csid = extractCsid(row.delivery_payload);
2243
+ if (!csid) return { ok: false, reason: "proxy_payload_corrupt" };
2244
+ return { ok: true, channel_session_id: csid };
2245
+ }
2246
+ };
2247
+ function extractCsid(payload) {
2248
+ if (payload === null) return null;
2249
+ try {
2250
+ const parsed = JSON.parse(payload);
2251
+ const csid = parsed.channel_session_id;
2252
+ if (typeof csid !== "string" || csid.length === 0) return null;
2253
+ return csid;
2254
+ } catch {
2255
+ return null;
2256
+ }
2257
+ }
2258
+
2259
+ // src/daemon/runtime-identity.ts
2260
+ import { execFile as execFile2 } from "child_process";
2261
+ import { promisify as promisify2 } from "util";
2262
+ var TMUX_LIST_TIMEOUT_MS = 3e3;
2263
+ var PS_LIST_TIMEOUT_MS = 3e3;
2264
+ function normalizeTty(raw) {
2265
+ const value = raw?.trim();
2266
+ if (!value) return void 0;
2267
+ const normalized = value.replace(/^\/dev\//, "");
2268
+ if (!normalized || normalized === "?") return void 0;
2269
+ return normalized;
2270
+ }
2271
+ function commandPattern(args) {
2272
+ if (args.agent === "custom") {
2273
+ const raw = args.process_pattern?.trim();
2274
+ if (!raw) return null;
2275
+ return new RegExp(raw, "i");
2276
+ }
2277
+ if (args.agent === "codex") {
2278
+ return /(^|[\s/])(codex|codex-aarch64-a)([\s]|$)/i;
2279
+ }
2280
+ if (args.agent === "claude-code") {
2281
+ return /(^|[\s/])claude([\s]|$)/i;
2282
+ }
2283
+ return /(^|[\s/])opencode([\s]|$)/i;
2284
+ }
2285
+ async function listPanes(execLike) {
2286
+ const exec = promisify2(execLike);
2287
+ const { stdout } = await exec(
2288
+ "tmux",
2289
+ ["list-panes", "-a", "-F", "#{pane_id} #{pane_tty}"],
2290
+ { timeout: TMUX_LIST_TIMEOUT_MS }
2291
+ );
2292
+ return stdout.split("\n").map((line) => line.trimEnd()).filter(Boolean).map((line) => {
2293
+ const [pane_id, pane_tty] = line.split(" ");
2294
+ return {
2295
+ pane_id,
2296
+ tty: normalizeTty(pane_tty) ?? ""
2297
+ };
2298
+ });
2299
+ }
2300
+ async function readPidInfo(execLike, pid) {
2301
+ const exec = promisify2(execLike);
2302
+ try {
2303
+ const { stdout } = await exec(
2304
+ "ps",
2305
+ ["-p", String(pid), "-o", "tty=,command="],
2306
+ { timeout: PS_LIST_TIMEOUT_MS }
2307
+ );
2308
+ const line = stdout.split("\n").map((value) => value.trim()).find(Boolean);
2309
+ if (!line) return { found: false };
2310
+ const match = line.match(/^(\S+)\s+(.*)$/);
2311
+ if (!match) return { found: false };
2312
+ return {
2313
+ found: true,
2314
+ tty: normalizeTty(match[1]),
2315
+ command: match[2]?.trim()
2316
+ };
2317
+ } catch {
2318
+ return { found: false };
2319
+ }
2320
+ }
2321
+ async function ttyProcesses(execLike, tty) {
2322
+ const exec = promisify2(execLike);
2323
+ const { stdout } = await exec(
2324
+ "ps",
2325
+ ["-t", tty, "-o", "pid=,ppid=,stat=,command="],
2326
+ { timeout: PS_LIST_TIMEOUT_MS }
2327
+ );
2328
+ return stdout.split("\n").map((line) => line.trimEnd()).filter(Boolean);
2329
+ }
2330
+ function matchAgentProcess(agent, lines, pattern) {
2331
+ return lines.some((line) => {
2332
+ if (isHelperProcess(agent, line)) return false;
2333
+ return pattern.test(line);
2334
+ });
2335
+ }
2336
+ function isHelperProcess(agent, command) {
2337
+ if (agent !== "codex") return false;
2338
+ return /codex\s+app-server/i.test(command) || /Codex Computer Use\.app/i.test(command) || /SkyComputerUseClient/i.test(command);
2339
+ }
2340
+ async function bindRuntimeIdentity(input, deps = {}) {
2341
+ const execLike = deps.execFile ?? execFile2;
2342
+ const pattern = commandPattern(input);
2343
+ if (!pattern) return { error: "invalid_process_pattern" };
2344
+ let panes;
2345
+ try {
2346
+ panes = await listPanes(execLike);
2347
+ } catch (error) {
2348
+ return {
2349
+ error: "tmux_unavailable",
2350
+ detail: error instanceof Error ? error.message : String(error)
2351
+ };
2352
+ }
2353
+ if (input.ui_pid !== void 0) {
2354
+ if (!Number.isInteger(input.ui_pid) || input.ui_pid <= 0) {
2355
+ return { error: "invalid_ui_pid" };
2356
+ }
2357
+ const pidInfo = await readPidInfo(execLike, input.ui_pid);
2358
+ if (!pidInfo.found) return { error: "pid_not_found" };
2359
+ if (!pidInfo.command || isHelperProcess(input.agent, pidInfo.command) || !pattern.test(pidInfo.command)) {
2360
+ return { error: "agent_process_mismatch" };
2361
+ }
2362
+ if (!pidInfo.tty) return { error: "pid_has_no_tty" };
2363
+ const candidates = panes.filter((pane2) => pane2.tty === pidInfo.tty);
2364
+ if (candidates.length === 0) return { error: "tmux_pane_not_found" };
2365
+ if (candidates.length > 1) {
2366
+ return {
2367
+ error: "ambiguous_tty_match",
2368
+ candidates: candidates.map((candidate2) => ({
2369
+ pane_id: candidate2.pane_id,
2370
+ tty: candidate2.tty
2371
+ }))
2372
+ };
2373
+ }
2374
+ const candidate = candidates[0];
2375
+ const explicitPane = input.tmux_pane_id?.trim();
2376
+ if (explicitPane && explicitPane !== candidate.pane_id) {
2377
+ return {
2378
+ error: "pid_pane_tty_mismatch",
2379
+ detail: {
2380
+ pid_tty: pidInfo.tty,
2381
+ pane_tty: candidate.tty
2382
+ }
2383
+ };
2384
+ }
2385
+ return {
2386
+ ok: true,
2387
+ tmux_pane_id: candidate.pane_id,
2388
+ verification_mode: "verified_pid_tty_pane",
2389
+ tty: pidInfo.tty,
2390
+ ui_pid: input.ui_pid
2391
+ };
2392
+ }
2393
+ const tty = normalizeTty(input.ui_tty);
2394
+ const paneId = input.tmux_pane_id?.trim();
2395
+ if (!tty || !paneId) return { error: "invalid_runtime_identity" };
2396
+ const pane = panes.find((candidate) => candidate.pane_id === paneId);
2397
+ if (!pane) return { error: "tmux_pane_not_found" };
2398
+ if (pane.tty !== tty) {
2399
+ return {
2400
+ error: "pid_pane_tty_mismatch",
2401
+ detail: {
2402
+ pid_tty: tty,
2403
+ pane_tty: pane.tty
2404
+ }
2405
+ };
2406
+ }
2407
+ const processes = await ttyProcesses(execLike, tty);
2408
+ if (!matchAgentProcess(input.agent, processes, pattern)) {
2409
+ return { error: "tty_maps_to_no_agent_process" };
2410
+ }
2411
+ return {
2412
+ ok: true,
2413
+ tmux_pane_id: paneId,
2414
+ verification_mode: "verified_tty_pane",
2415
+ tty
2416
+ };
2417
+ }
2418
+
2419
+ // src/mcp/bind-runtime-identity.ts
2420
+ var BindRuntimeIdentityService = class {
2421
+ repo;
2422
+ constructor(db) {
2423
+ this.repo = new AgentsRepo(db);
2424
+ }
2425
+ async bind(input) {
2426
+ const caller = this.repo.getById(input.callerAgentId);
2427
+ if (!caller) return { error: "unknown_agent" };
2428
+ const result = await bindRuntimeIdentity(input);
2429
+ if (!("ok" in result) || !result.ok) return result;
2430
+ this.repo.setRuntimeBinding(input.callerAgentId, {
2431
+ tmux_pane_id: result.tmux_pane_id,
2432
+ runtime_ui_pid: result.ui_pid ?? null,
2433
+ runtime_tty: result.tty,
2434
+ runtime_verification_mode: result.verification_mode
2435
+ });
2436
+ return result;
2437
+ }
2438
+ };
2439
+
2440
+ // src/mcp/register-codex-self.ts
2441
+ var DEFAULT_CODEX_WS_URL = "ws://127.0.0.1:8799";
2442
+ async function requestStep2(client, method, params, errorCode) {
2443
+ try {
2444
+ const response = await client.request(method, params);
2445
+ if (response.error) return { error: errorCode, detail: response.error };
2446
+ return { ok: response };
2447
+ } catch (error) {
2448
+ return { error: errorCode, detail: describeError(error) };
2449
+ }
2450
+ }
2451
+ function resolveWsUrl(input, env) {
2452
+ const explicit = input.ws_url?.trim();
2453
+ if (explicit) return explicit;
2454
+ const fromEnv = env.CROSS_AGENT_TEAMS_CODEX_WS_URL?.trim();
2455
+ if (fromEnv) return fromEnv;
2456
+ return DEFAULT_CODEX_WS_URL;
2457
+ }
2458
+ function extractThreadIds(response) {
2459
+ const result = response.result;
2460
+ if (!result || !Array.isArray(result.data)) return [];
2461
+ return result.data.filter((value) => typeof value === "string");
2462
+ }
2463
+ function trimToUndefined(value) {
2464
+ const trimmed = value?.trim();
2465
+ return trimmed ? trimmed : void 0;
2466
+ }
2467
+ var RegisterCodexSelfService = class {
2468
+ constructor(registerSvc, deps = {}) {
2469
+ this.registerSvc = registerSvc;
2470
+ this.deps = deps;
2471
+ }
2472
+ registerSvc;
2473
+ deps;
2474
+ async register(input) {
2475
+ const env = this.deps.env ?? process.env;
2476
+ const wsUrl = resolveWsUrl(input, env);
2477
+ const token = resolveAuthToken(input.auth_token_ref, env);
2478
+ if ("error" in token) return token;
2479
+ const headers = token.ok === void 0 ? void 0 : { Authorization: `Bearer ${token.ok}` };
2480
+ let ws;
2481
+ try {
2482
+ ws = (this.deps.webSocketFactory ?? defaultWebSocketFactory)({
2483
+ url: wsUrl,
2484
+ headers
2485
+ });
2486
+ } catch (error) {
2487
+ return {
2488
+ error: "unsupported_client",
2489
+ detail: {
2490
+ expected: "codex",
2491
+ reason: "codex_appserver_unreachable",
2492
+ ws_url: wsUrl,
2493
+ cause: describeError(error)
2494
+ }
2495
+ };
2496
+ }
2497
+ const client = new JsonRpcSocketClient(ws);
2498
+ try {
2499
+ await client.waitForOpen();
2500
+ const init = await requestStep2(
2501
+ client,
2502
+ "initialize",
2503
+ {
2504
+ clientInfo: {
2505
+ name: "cross-agent-teams-mcp",
2506
+ title: null,
2507
+ version: "0.1.0"
2508
+ },
2509
+ capabilities: {
2510
+ experimentalApi: true,
2511
+ optOutNotificationMethods: null
2512
+ }
2513
+ },
2514
+ "codex_initialize_failed"
2515
+ );
2516
+ if ("error" in init) {
2517
+ return {
2518
+ error: "unsupported_client",
2519
+ detail: {
2520
+ expected: "codex",
2521
+ reason: "codex_protocol_unavailable",
2522
+ ws_url: wsUrl,
2523
+ cause: init.detail
2524
+ }
2525
+ };
2526
+ }
2527
+ client.notify("initialized");
2528
+ const explicitThreadId = trimToUndefined(input.thread_id);
2529
+ let threadId = explicitThreadId;
2530
+ if (!threadId) {
2531
+ const list = await requestStep2(
2532
+ client,
2533
+ "thread/loaded/list",
2534
+ { cursor: null, limit: 20 },
2535
+ "codex_loaded_list_failed"
2536
+ );
2537
+ if ("error" in list) return list;
2538
+ const threadIds = extractThreadIds(list.ok);
2539
+ if (threadIds.length === 0) {
2540
+ return {
2541
+ error: "no_loaded_threads",
2542
+ detail: { ws_url: wsUrl }
2543
+ };
2544
+ }
2545
+ const liveThreadIds = [];
2546
+ const failures = [];
2547
+ for (const candidateThreadId of threadIds) {
2548
+ const resume2 = await requestStep2(
2549
+ client,
2550
+ "thread/resume",
2551
+ {
2552
+ threadId: candidateThreadId,
2553
+ persistExtendedHistory: false
2554
+ },
2555
+ "codex_resume_failed"
2556
+ );
2557
+ if ("error" in resume2) {
2558
+ failures.push({ thread_id: candidateThreadId, detail: resume2.detail });
2559
+ continue;
2560
+ }
2561
+ liveThreadIds.push(candidateThreadId);
2562
+ }
2563
+ if (liveThreadIds.length === 0) {
2564
+ return {
2565
+ error: "codex_resume_failed",
2566
+ detail: failures
2567
+ };
2568
+ }
2569
+ return {
2570
+ error: "thread_id_required",
2571
+ detail: {
2572
+ ws_url: wsUrl,
2573
+ thread_ids: liveThreadIds
2574
+ }
2575
+ };
2576
+ }
2577
+ const resume = await requestStep2(
2578
+ client,
2579
+ "thread/resume",
2580
+ {
2581
+ threadId,
2582
+ persistExtendedHistory: false
2583
+ },
2584
+ "codex_resume_failed"
2585
+ );
2586
+ if ("error" in resume) {
2587
+ return {
2588
+ error: "codex_resume_failed",
2589
+ detail: { thread_id: threadId, cause: resume.detail }
2590
+ };
2591
+ }
2592
+ const tmuxPaneId = trimToUndefined(input.tmux_pane_id);
2593
+ const result = this.registerSvc.register({
2594
+ connection_id: input.connection_id,
2595
+ client: "codex",
2596
+ model: input.model ?? "codex",
2597
+ name: input.name,
2598
+ role: input.role,
2599
+ team: input.team,
2600
+ project_dir: input.project_dir,
2601
+ tmux_pane_id: tmuxPaneId,
2602
+ delivery: {
2603
+ kind: "codex-appserver",
2604
+ thread_id: threadId,
2605
+ ws_url: wsUrl,
2606
+ ...input.auth_token_ref === void 0 ? {} : { auth_token_ref: input.auth_token_ref }
2607
+ }
2608
+ });
2609
+ if ("error" in result) return result;
2610
+ return {
2611
+ ...result,
2612
+ thread_id: threadId,
2613
+ ws_url: wsUrl
2614
+ };
2615
+ } catch (error) {
2616
+ return {
2617
+ error: "unsupported_client",
2618
+ detail: {
2619
+ expected: "codex",
2620
+ reason: "codex_appserver_unreachable",
2621
+ ws_url: wsUrl,
2622
+ cause: describeError(error)
2623
+ }
2624
+ };
2625
+ } finally {
2626
+ safeClose(ws);
2627
+ }
2628
+ }
2629
+ };
2630
+
2631
+ // src/mcp/unregister-self.ts
2632
+ var UnregisterSelfService = class {
2633
+ constructor(db, agents) {
2634
+ this.db = db;
2635
+ this.agents = agents;
2636
+ }
2637
+ db;
2638
+ agents;
2639
+ unregister(args) {
2640
+ const caller = this.agents.findById(args.caller);
2641
+ if (!caller) return { error: "unknown_agent" };
2642
+ const task_ids = this.agents.listClaimedInProgressTaskIds({
2643
+ agent_id: caller.agent_id,
2644
+ team: caller.team
2645
+ });
2646
+ if (task_ids.length > 0) {
2647
+ return { error: "tasks_in_progress", task_ids };
2648
+ }
2649
+ let removed = false;
2650
+ const tx = this.db.transaction(() => {
2651
+ removed = this.agents.deleteById(caller.agent_id);
2652
+ if (!removed) return;
2653
+ this.agents.deleteContractSubscriptions({
2654
+ agent_id: caller.agent_id,
2655
+ team: caller.team
2656
+ });
2657
+ });
2658
+ tx();
2659
+ if (!removed) return { error: "unknown_agent" };
2660
+ return {
2661
+ ok: true,
2662
+ team: caller.team,
2663
+ name: caller.name,
2664
+ agent_id: caller.agent_id
2665
+ };
2666
+ }
2667
+ };
2668
+
2669
+ // src/mcp/agent-public-row.ts
2670
+ function projectDelivery(delivery) {
2671
+ if (delivery.kind === "claude-channel") {
2672
+ return {
2673
+ kind: "claude-channel",
2674
+ channel_session_id: delivery.channel_session_id
2675
+ };
2676
+ }
2677
+ return { kind: delivery.kind };
2678
+ }
2679
+ function toPublicAgentRow(row) {
2680
+ return {
2681
+ agent_id: row.agent_id,
2682
+ client: row.client,
2683
+ client_name: row.client_name,
2684
+ team: row.team,
2685
+ role: row.role,
2686
+ name: row.name,
2687
+ model: row.model,
2688
+ tmux_pane_id: row.tmux_pane_id,
2689
+ delivery: projectDelivery(row.delivery),
2690
+ channel_session_id: row.delivery.kind === "claude-channel" ? row.delivery.channel_session_id : null,
2691
+ last_seen_at: row.last_seen_at,
2692
+ online: row.online
2693
+ };
2694
+ }
2695
+
2696
+ // src/daemon/tmux-pane-detect.ts
2697
+ import { execFile as execFile3 } from "child_process";
2698
+ import { normalize, sep } from "path";
2699
+ import { promisify as promisify3 } from "util";
2700
+ var pExecFile2 = promisify3(execFile3);
2701
+ var TMUX_LIST_TIMEOUT_MS2 = 3e3;
2702
+ var PS_LIST_TIMEOUT_MS2 = 3e3;
2703
+ function normalizeTty2(raw) {
2704
+ const value = raw?.trim();
2705
+ if (!value) return void 0;
2706
+ return value.replace(/^\/dev\//, "");
2707
+ }
2708
+ function normalizePath(raw) {
2709
+ const value = raw?.trim();
2710
+ if (!value) return void 0;
2711
+ return normalize(value);
2712
+ }
2713
+ function pathRelated(candidatePath, inputPath) {
2714
+ const candidate = normalize(candidatePath);
2715
+ const input = normalize(inputPath);
2716
+ if (candidate === input) return "exact";
2717
+ if (candidate.startsWith(`${input}${sep}`)) return "descendant";
2718
+ if (input.startsWith(`${candidate}${sep}`)) return "ancestor";
2719
+ return "none";
2720
+ }
2721
+ function commandPattern2(args) {
2722
+ if (args.agent === "custom") {
2723
+ const raw = args.process_pattern?.trim();
2724
+ if (!raw) throw new Error("process_pattern is required when agent=custom");
2725
+ return new RegExp(raw, "i");
2726
+ }
2727
+ if (args.agent === "codex") {
2728
+ return /(^|[\s/])(codex|codex-aarch64-a)([\s]|$)/i;
2729
+ }
2730
+ if (args.agent === "claude-code") {
2731
+ return /(^|[\s/])claude([\s]|$)/i;
2732
+ }
2733
+ return /(^|[\s/])opencode([\s]|$)/i;
2734
+ }
2735
+ function commandHintScore(agent, command) {
2736
+ if (agent === "codex" && /codex/i.test(command)) return 6;
2737
+ if (agent === "opencode" && /opencode/i.test(command)) return 6;
2738
+ if (agent === "claude-code" && /^(\d+\.)+\d+$/.test(command)) return 4;
2739
+ return 0;
2740
+ }
2741
+ function isHelperProcess2(agent, command) {
2742
+ if (agent !== "codex") return false;
2743
+ return /codex\s+app-server/i.test(command) || /Codex Computer Use\.app/i.test(command) || /SkyComputerUseClient/i.test(command);
2744
+ }
2745
+ function parsePaneRows(stdout) {
2746
+ return stdout.split("\n").map((line) => line.trimEnd()).filter(Boolean).map((line) => {
2747
+ const [
2748
+ pane_id,
2749
+ session_name,
2750
+ window_index,
2751
+ pane_index,
2752
+ pane_active,
2753
+ pane_tty,
2754
+ pane_current_path,
2755
+ pane_current_command,
2756
+ pane_title
2757
+ ] = line.split(" ");
2758
+ return {
2759
+ pane_id,
2760
+ session_name,
2761
+ window_index: Number(window_index),
2762
+ pane_index: Number(pane_index),
2763
+ active: pane_active === "1",
2764
+ tty: normalizeTty2(pane_tty) ?? "",
2765
+ current_path: pane_current_path ?? "",
2766
+ current_command: pane_current_command ?? "",
2767
+ title: pane_title ?? ""
2768
+ };
2769
+ });
2770
+ }
2771
+ async function listPanes2(execLike) {
2772
+ const exec = promisify3(execLike);
2773
+ const { stdout } = await exec(
2774
+ "tmux",
2775
+ [
2776
+ "list-panes",
2777
+ "-a",
2778
+ "-F",
2779
+ "#{pane_id} #{session_name} #{window_index} #{pane_index} #{pane_active} #{pane_tty} #{pane_current_path} #{pane_current_command} #{pane_title}"
2780
+ ],
2781
+ { timeout: TMUX_LIST_TIMEOUT_MS2 }
2782
+ );
2783
+ return parsePaneRows(stdout);
2784
+ }
2785
+ async function ttyProcesses2(execLike, tty) {
2786
+ const exec = promisify3(execLike);
2787
+ const { stdout } = await exec(
2788
+ "ps",
2789
+ ["-t", tty, "-o", "pid=,ppid=,stat=,command="],
2790
+ { timeout: PS_LIST_TIMEOUT_MS2 }
2791
+ );
2792
+ return stdout.split("\n").map((line) => line.trimEnd()).filter(Boolean);
2793
+ }
2794
+ function collectCandidates(panes, ttyMap, input) {
2795
+ const ttyFilter = normalizeTty2(input.tty);
2796
+ const cwdFilter = normalizePath(input.cwd);
2797
+ const titleFilter = input.title_contains?.trim().toLowerCase();
2798
+ const pattern = commandPattern2(input);
2799
+ const candidates = [];
2800
+ for (const pane of panes) {
2801
+ if (ttyFilter && pane.tty !== ttyFilter) continue;
2802
+ if (cwdFilter) {
2803
+ const relation = pathRelated(pane.current_path, cwdFilter);
2804
+ if (relation === "none") continue;
2805
+ }
2806
+ if (titleFilter && !pane.title.toLowerCase().includes(titleFilter)) continue;
2807
+ const matched_processes = (ttyMap.get(pane.tty) ?? []).filter((line) => {
2808
+ if (isHelperProcess2(input.agent, line)) return false;
2809
+ return pattern.test(line);
2810
+ });
2811
+ if (matched_processes.length === 0) continue;
2812
+ let score = matched_processes.length * 10;
2813
+ if (pane.active) score += 3;
2814
+ score += commandHintScore(input.agent, pane.current_command);
2815
+ if (ttyFilter) score += 100;
2816
+ if (cwdFilter) {
2817
+ const relation = pathRelated(pane.current_path, cwdFilter);
2818
+ if (relation === "exact") score += 60;
2819
+ else if (relation === "descendant") score += 45;
2820
+ else if (relation === "ancestor") score += 30;
2821
+ }
2822
+ if (titleFilter) score += 15;
2823
+ candidates.push({
2824
+ pane_id: pane.pane_id,
2825
+ session_name: pane.session_name,
2826
+ window_index: pane.window_index,
2827
+ pane_index: pane.pane_index,
2828
+ active: pane.active,
2829
+ tty: pane.tty,
2830
+ current_path: pane.current_path,
2831
+ current_command: pane.current_command,
2832
+ title: pane.title,
2833
+ matched_processes,
2834
+ score
2835
+ });
2836
+ }
2837
+ return candidates.sort((a, b) => {
2838
+ if (b.score !== a.score) return b.score - a.score;
2839
+ if (a.pane_id < b.pane_id) return -1;
2840
+ if (a.pane_id > b.pane_id) return 1;
2841
+ return 0;
2842
+ });
2843
+ }
2844
+ async function detectTmuxPane(input, deps = {}) {
2845
+ const execLike = deps.execFile ?? execFile3;
2846
+ let panes;
2847
+ try {
2848
+ panes = await listPanes2(execLike);
2849
+ } catch (error) {
2850
+ return {
2851
+ error: "tmux_unavailable",
2852
+ detail: error instanceof Error ? error.message : String(error)
2853
+ };
2854
+ }
2855
+ const ttyMap = /* @__PURE__ */ new Map();
2856
+ for (const pane of panes) {
2857
+ if (!pane.tty || ttyMap.has(pane.tty)) continue;
2858
+ try {
2859
+ ttyMap.set(pane.tty, await ttyProcesses2(execLike, pane.tty));
2860
+ } catch {
2861
+ ttyMap.set(pane.tty, []);
2862
+ }
2863
+ }
2864
+ let candidates;
2865
+ try {
2866
+ candidates = collectCandidates(panes, ttyMap, input);
2867
+ } catch (error) {
2868
+ return {
2869
+ error: "not_found",
2870
+ candidates: []
2871
+ };
2872
+ }
2873
+ if (candidates.length === 0) return { error: "not_found", candidates: [] };
2874
+ const topScore = candidates[0].score;
2875
+ const top = candidates.filter((candidate) => candidate.score === topScore);
2876
+ if (top.length > 1) {
2877
+ return {
2878
+ error: "ambiguous_match",
2879
+ candidates
2880
+ };
2881
+ }
2882
+ return {
2883
+ ok: true,
2884
+ pane: candidates[0],
2885
+ candidates
2886
+ };
2887
+ }
2888
+
2889
+ // src/mcp/codex-pane-pre-register-repo.ts
2890
+ var CodexPanePreRegRepo = class {
2891
+ constructor(db) {
2892
+ this.db = db;
2893
+ }
2894
+ db;
2895
+ upsert(input) {
2896
+ this.db.prepare(
2897
+ `INSERT INTO codex_pane_pre_registrations (pane_id, xats_agent_id, expires_at)
2898
+ VALUES (?, ?, ?)
2899
+ ON CONFLICT(pane_id) DO UPDATE SET
2900
+ xats_agent_id = excluded.xats_agent_id,
2901
+ expires_at = excluded.expires_at`
2902
+ ).run(input.pane_id, input.xats_agent_id, input.expires_at);
2903
+ }
2904
+ listUnexpired(now) {
2905
+ return this.db.prepare(
2906
+ `SELECT pane_id, xats_agent_id, expires_at
2907
+ FROM codex_pane_pre_registrations
2908
+ WHERE expires_at > ?`
2909
+ ).all(now);
2910
+ }
2911
+ takeByPaneId(pane_id) {
2912
+ const row = this.db.prepare(
2913
+ `DELETE FROM codex_pane_pre_registrations
2914
+ WHERE pane_id = ?
2915
+ RETURNING pane_id, xats_agent_id, expires_at`
2916
+ ).get(pane_id);
2917
+ return row;
2918
+ }
2919
+ deleteExpired(now) {
2920
+ const res = this.db.prepare(`DELETE FROM codex_pane_pre_registrations WHERE expires_at <= ?`).run(now);
2921
+ return res.changes;
2922
+ }
2923
+ };
2924
+
2925
+ // src/mcp/pre-register-codex-pane.ts
2926
+ import { z as z2 } from "zod";
2927
+ var preRegisterCodexPaneInputSchema = z2.object({
2928
+ pane_id: z2.string().min(1).refine((v) => v.startsWith("%"), {
2929
+ message: 'pane_id must be a tmux pane id starting with "%"'
2930
+ }),
2931
+ xats_agent_id: z2.string().min(1),
2932
+ ttl_seconds: z2.number().int().positive().optional()
2933
+ }).strict();
2934
+ var DEFAULT_TTL_SECONDS = 120;
2935
+ var MIN_TTL_SECONDS = 1;
2936
+ var MAX_TTL_SECONDS = 600;
2937
+ function clampTtl(ttl) {
2938
+ const raw = ttl ?? DEFAULT_TTL_SECONDS;
2939
+ if (raw < MIN_TTL_SECONDS) return MIN_TTL_SECONDS;
2940
+ if (raw > MAX_TTL_SECONDS) return MAX_TTL_SECONDS;
2941
+ return raw;
2942
+ }
2943
+ var PreRegisterCodexPaneService = class {
2944
+ constructor(repo, now = () => /* @__PURE__ */ new Date()) {
2945
+ this.repo = repo;
2946
+ this.now = now;
2947
+ }
2948
+ repo;
2949
+ now;
2950
+ register(args) {
2951
+ const parsed = preRegisterCodexPaneInputSchema.safeParse(args);
2952
+ if (!parsed.success) {
2953
+ return {
2954
+ error: "invalid_arguments",
2955
+ detail: parsed.error.issues.map((issue) => {
2956
+ const path = issue.path.join(".");
2957
+ return path ? `${path}: ${issue.message}` : issue.message;
2958
+ }).join("; ")
2959
+ };
2960
+ }
2961
+ const now = this.now();
2962
+ const ttl = clampTtl(parsed.data.ttl_seconds);
2963
+ const expires_at = new Date(now.getTime() + ttl * 1e3).toISOString();
2964
+ this.repo.deleteExpired(now.toISOString());
2965
+ this.repo.upsert({
2966
+ pane_id: parsed.data.pane_id,
2967
+ xats_agent_id: parsed.data.xats_agent_id,
2968
+ expires_at
2969
+ });
2970
+ return { ok: true, expires_at };
2971
+ }
2972
+ };
2973
+
2974
+ // src/mcp/auto-bind-codex-pane.ts
2975
+ import { execFile as execFile4 } from "child_process";
2976
+ import { promisify as promisify4 } from "util";
2977
+ var TMUX_LIST_TIMEOUT_MS3 = 3e3;
2978
+ var PS_LIST_TIMEOUT_MS3 = 3e3;
2979
+ function normalizeTty3(raw) {
2980
+ const value = raw?.trim();
2981
+ if (!value) return void 0;
2982
+ const normalized = value.replace(/^\/dev\//, "");
2983
+ if (!normalized || normalized === "?") return void 0;
2984
+ return normalized;
2985
+ }
2986
+ async function defaultListPanes() {
2987
+ const exec = promisify4(execFile4);
2988
+ const { stdout } = await exec(
2989
+ "tmux",
2990
+ ["list-panes", "-a", "-F", "#{pane_id} #{pane_tty}"],
2991
+ { timeout: TMUX_LIST_TIMEOUT_MS3 }
2992
+ );
2993
+ return stdout.split("\n").map((line) => line.trimEnd()).filter(Boolean).map((line) => {
2994
+ const [pane_id, pane_tty] = line.split(" ");
2995
+ return {
2996
+ pane_id,
2997
+ tty: normalizeTty3(pane_tty) ?? ""
2998
+ };
2999
+ });
3000
+ }
3001
+ async function defaultTtyProcesses(tty) {
3002
+ const exec = promisify4(execFile4);
3003
+ const { stdout } = await exec(
3004
+ "ps",
3005
+ ["-t", tty, "-o", "pid=,ppid=,stat=,command="],
3006
+ { timeout: PS_LIST_TIMEOUT_MS3 }
3007
+ );
3008
+ return stdout.split("\n").map((line) => line.trimEnd()).filter(Boolean);
3009
+ }
3010
+ function parsePid(line) {
3011
+ const match = line.trim().match(/^(\d+)\s/);
3012
+ if (!match) return void 0;
3013
+ const pid = Number(match[1]);
3014
+ if (!Number.isInteger(pid) || pid <= 0) return void 0;
3015
+ return pid;
3016
+ }
3017
+ function isCodexRemoteProcess(line) {
3018
+ if (!/codex/i.test(line)) return false;
3019
+ if (/codex\s+app-server/i.test(line)) return false;
3020
+ return /codex(?:-aarch64-a)?\s+.*--remote/i.test(line) || /codex(?:-aarch64-a)?\s+--remote/i.test(line);
3021
+ }
3022
+ function argvContainsUuid(line, uuid) {
3023
+ return line.includes(`xats.agent_id="${uuid}"`);
3024
+ }
3025
+ var __testOverrides = {};
3026
+ async function autoBindCodexPane(input, deps = {}) {
3027
+ const listPanes3 = deps.listPanes ?? __testOverrides.listPanes ?? defaultListPanes;
3028
+ const ttyProcesses3 = deps.ttyProcesses ?? __testOverrides.ttyProcesses ?? defaultTtyProcesses;
3029
+ const now = deps.now ?? __testOverrides.now ?? (() => /* @__PURE__ */ new Date());
3030
+ try {
3031
+ const nowIso = now().toISOString();
3032
+ input.repo.deleteExpired(nowIso);
3033
+ const pending = input.repo.listUnexpired(nowIso);
3034
+ if (pending.length === 0) return false;
3035
+ let panes;
3036
+ try {
3037
+ panes = await listPanes3();
3038
+ } catch {
3039
+ return false;
3040
+ }
3041
+ const paneIndex = /* @__PURE__ */ new Map();
3042
+ for (const pane of panes) {
3043
+ if (pane.pane_id) paneIndex.set(pane.pane_id, pane);
3044
+ }
3045
+ const ttyProcessCache = /* @__PURE__ */ new Map();
3046
+ const candidates = [];
3047
+ for (const row of pending) {
3048
+ const pane = paneIndex.get(row.pane_id);
3049
+ if (!pane || !pane.tty) continue;
3050
+ let procs = ttyProcessCache.get(pane.tty);
3051
+ if (procs === void 0) {
3052
+ try {
3053
+ procs = await ttyProcesses3(pane.tty);
3054
+ } catch {
3055
+ procs = [];
3056
+ }
3057
+ ttyProcessCache.set(pane.tty, procs);
3058
+ }
3059
+ const matching = procs.filter(
3060
+ (line) => isCodexRemoteProcess(line) && argvContainsUuid(line, row.xats_agent_id)
3061
+ );
3062
+ if (matching.length !== 1) continue;
3063
+ const pid = parsePid(matching[0]);
3064
+ if (pid === void 0) continue;
3065
+ candidates.push({ row, pane_id: pane.pane_id, ui_pid: pid });
3066
+ }
3067
+ if (candidates.length !== 1) return false;
3068
+ const chosen = candidates[0];
3069
+ const bindResult = await input.bindRuntimeIdentitySvc.bind({
3070
+ callerAgentId: input.callerAgentId,
3071
+ agent: "codex",
3072
+ ui_pid: chosen.ui_pid
3073
+ });
3074
+ if (!("ok" in bindResult) || !bindResult.ok) return false;
3075
+ input.repo.takeByPaneId(chosen.pane_id);
3076
+ return true;
3077
+ } catch {
3078
+ return false;
3079
+ }
3080
+ }
3081
+
3082
+ // src/mcp/tools.ts
3083
+ function toText(value) {
3084
+ return { content: [{ type: "text", text: JSON.stringify(value) }] };
3085
+ }
3086
+ var deliverySchema = z3.object({
3087
+ kind: z3.string()
3088
+ }).passthrough();
3089
+ var clientSchema = z3.enum(["codex", "claude-code", "opencode", "custom"]);
3090
+ var detectTmuxPaneSchema = z3.object({
3091
+ agent: z3.enum(["codex", "claude-code", "opencode", "custom"]),
3092
+ cwd: z3.string().optional(),
3093
+ tty: z3.string().optional(),
3094
+ title_contains: z3.string().optional(),
3095
+ process_pattern: z3.string().optional()
3096
+ });
3097
+ var detectTmuxPaneArgsSchema = detectTmuxPaneSchema.superRefine((value, ctx) => {
3098
+ if (value.agent === "custom" && (!value.process_pattern || value.process_pattern.trim().length === 0)) {
3099
+ ctx.addIssue({
3100
+ code: z3.ZodIssueCode.custom,
3101
+ path: ["process_pattern"],
3102
+ message: "process_pattern is required when agent=custom"
3103
+ });
3104
+ }
3105
+ });
3106
+ var bindRuntimeIdentitySchema = z3.object({
3107
+ agent: z3.enum(["codex", "claude-code", "opencode", "custom"]),
3108
+ ui_pid: z3.number().int().positive().optional(),
3109
+ ui_tty: z3.string().optional(),
3110
+ tmux_pane_id: z3.string().min(1).optional(),
3111
+ process_pattern: z3.string().optional()
3112
+ });
3113
+ var bindRuntimeIdentityArgsSchema = bindRuntimeIdentitySchema.superRefine((value, ctx) => {
3114
+ if (value.agent === "custom" && (!value.process_pattern || value.process_pattern.trim().length === 0)) {
3115
+ ctx.addIssue({
3116
+ code: z3.ZodIssueCode.custom,
3117
+ path: ["process_pattern"],
3118
+ message: "process_pattern is required when agent=custom"
3119
+ });
3120
+ }
3121
+ const hasPid = value.ui_pid !== void 0;
3122
+ const hasTtyPair = value.ui_tty !== void 0 && value.ui_tty.trim().length > 0 && value.tmux_pane_id !== void 0 && value.tmux_pane_id.trim().length > 0;
3123
+ if (!hasPid && !hasTtyPair) {
3124
+ ctx.addIssue({
3125
+ code: z3.ZodIssueCode.custom,
3126
+ message: "provide ui_pid, or ui_tty together with tmux_pane_id"
3127
+ });
3128
+ }
3129
+ });
3130
+ var SEND_MESSAGE_DESC = [
3131
+ "Private 1\u21921 message to another agent by name. By default auto-poke=true with quiet-guard (auto_poke:false opts out), and need_reply=true.",
3132
+ "Set need_reply:false for FYI/no-response-needed messages; recipients see need_reply in get_inbox.",
3133
+ "to_agent_name is the target's `name` within its team; this is the preferred addressing form. For UUID-based sends use send_message_by_id.",
3134
+ "For multi-recipient use broadcast (same-team) or broadcast_to_role (same-team, by role).",
3135
+ "\u9664\u975E\u7528\u6237\u660E\u786E\u6307\u5B9A to_team, \u4E0D\u8981\u8DE8 team \u6C9F\u901A (explicitly set to_team only when user asks).",
3136
+ "Reports poked, poke_skip_reasons (no_pane, guard_failed, tmux_unavailable, self); on guard_failed daemon retries at 30s/180s/600s (retry_scheduled, retry_delays_s); stops early on poked.",
3137
+ "Auto-poke injects only a SHORT wake-up hint (\u65B0\u90AE\u4EF6 from <sender>, \u8BF7\u8C03 get_inbox \u67E5\u770B), NOT the body \u2014 read bodies via get_inbox.",
3138
+ "Delivery is NOT filtered by online/idle (unlike broadcast's 5 min idle skip) \u2014 offline targets still receive the mailbox row."
3139
+ ].join(" ");
3140
+ var SEND_MESSAGE_BY_ID_DESC = [
3141
+ "Private 1\u21921 message to another agent by agent_id (UUID). Use this when you already hold the target's agent_id; prefer send_message (by name) otherwise.",
3142
+ "Same-team only: the recipient must belong to the caller's team. For cross-team sends use send_message with to_team.",
3143
+ "By default auto-poke=true with quiet-guard (auto_poke:false opts out), and need_reply=true. Set need_reply:false for FYI/no-response-needed messages.",
3144
+ "Reports poked, poke_skip_reasons (no_pane, guard_failed, tmux_unavailable, self); on guard_failed daemon retries at 30s/180s/600s (retry_scheduled, retry_delays_s); stops early on poked.",
3145
+ "Auto-poke injects only a SHORT wake-up hint (\u65B0\u90AE\u4EF6 from <sender>, \u8BF7\u8C03 get_inbox \u67E5\u770B), NOT the body \u2014 read bodies via get_inbox.",
3146
+ "Delivery is NOT filtered by online/idle \u2014 offline targets still receive the mailbox row."
3147
+ ].join(" ");
3148
+ var BROADCAST_DESC = [
3149
+ "Same-team broadcast to every other agent in the caller team.",
3150
+ "Auto-poke default true (quiet-guard + 30s/180s/600s retry; reports poked, poke_skip_reasons, retry_scheduled, retry_delays_s). auto_poke:false opts out.",
3151
+ "For role filter use broadcast_to_role. For cross-team 1\u21921 use send_message({to_team}).",
3152
+ "Auto-poke injects only a SHORT wake-up hint (\u65B0\u90AE\u4EF6 from <sender>, \u8BF7\u8C03 get_inbox \u67E5\u770B) \u2014 never the body. Read via get_inbox.",
3153
+ "Skips agents idle > 5 min (offline)."
3154
+ ].join(" ");
3155
+ var BROADCAST_TO_ROLE_DESC = [
3156
+ "Same-team broadcast filtered by role. Strictly same-team \u2014 no cross-team variant.",
3157
+ "For cross-team private 1\u21921 use send_message({to_team}).",
3158
+ "Auto-poke default true with quiet-guard + 30s/180s/600s retry (auto_poke:false opts out); injects only a SHORT wake-up hint, not the message body. Recipients read via get_inbox.",
3159
+ "Returns unknown_recipient when no same-team agent matches to_role."
3160
+ ].join(" ");
3161
+ function suppressTmuxHint(args) {
3162
+ return args.delivery?.kind !== void 0 && args.delivery.kind !== "none";
3163
+ }
3164
+ function defaultClaudeSelfModel(clientInfo) {
3165
+ const raw = `${clientInfo?.name ?? ""} ${clientInfo?.version ?? ""}`.trim();
3166
+ if (/claude/i.test(raw)) return raw;
3167
+ return "claude-code";
3168
+ }
3169
+ function buildAutoPokeHint(row, fromAgentId) {
3170
+ const dn = row?.name;
3171
+ const sender = typeof dn === "string" && dn.length > 0 ? `${dn} (${fromAgentId})` : fromAgentId.slice(0, 8);
3172
+ return `\u65B0\u90AE\u4EF6 from ${sender}, \u8BF7\u8C03 get_inbox \u67E5\u770B`;
3173
+ }
3174
+ function createAutoPokeImpl(db, _agents, channelWakeFanout) {
3175
+ return async (args) => {
3176
+ const row = db.prepare("SELECT name FROM agents WHERE agent_id=?").get(args.fromAgentId);
3177
+ const hint = buildAutoPokeHint(row, args.fromAgentId);
3178
+ const res = await poke(
3179
+ { db, callerAgentId: args.fromAgentId, allowCrossTeam: true, channelWakeFanout },
3180
+ { target_agent_id: args.targetAgentId, prompt: hint }
3181
+ );
3182
+ if ("ok" in res && res.ok) return { ok: true };
3183
+ const err = res.error;
3184
+ if (err === "tmux_unavailable") return { ok: false, reason: "tmux_unavailable" };
3185
+ if (err === "tmux_pane_not_set") return { ok: false, reason: "no_pane" };
3186
+ if (err === "no_transport_available") return { ok: false, reason: "no_pane" };
3187
+ if (err === "self_poke_denied") return { ok: false, reason: "self" };
3188
+ return { ok: false, reason: "guard_failed" };
3189
+ };
3190
+ }
3191
+ function inferRuntimeAgentKind(args, clientInfo) {
3192
+ if (args.client === "custom") return void 0;
3193
+ if (args.client) return args.client;
3194
+ if (args.delivery?.kind === "codex-appserver") return "codex";
3195
+ const raw = `${clientInfo?.name ?? ""} ${clientInfo?.version ?? ""} ${args.model}`.toLowerCase();
3196
+ if (raw.includes("codex")) return "codex";
3197
+ if (raw.includes("gpt-")) return "codex";
3198
+ if (raw.includes("claude")) return "claude-code";
3199
+ if (raw.includes("opus") || raw.includes("sonnet")) return "claude-code";
3200
+ if (raw.includes("opencode")) return "opencode";
3201
+ return void 0;
3202
+ }
3203
+ function registerBusinessTools(server, db, getCallerAgentId, fanout, onRegisterSuccess, getSessionId, channelWakeFanout, getTransport, getSessionClientInfo, onUnregisterSuccess) {
3204
+ const agents = new AgentsRepo(db);
3205
+ const events = new EventsOutbox(db);
3206
+ const registerSvc = new RegisterAgentService(db);
3207
+ const bindRuntimeIdentitySvc = new BindRuntimeIdentityService(db);
3208
+ const registerCodexSelfSvc = new RegisterCodexSelfService(registerSvc);
3209
+ const unregisterSelfSvc = new UnregisterSelfService(db, agents);
3210
+ const autoPokeImpl = createAutoPokeImpl(db, agents, channelWakeFanout);
3211
+ const sendSvc = new SendMessageService(db, agents, events, { poke: autoPokeImpl });
3212
+ const broadcastSvc = new BroadcastService(db, agents, { poke: autoPokeImpl });
3213
+ const broadcastToRoleSvc = new BroadcastToRoleService(db, agents, events, { poke: autoPokeImpl });
3214
+ const inboxSvc = new GetInboxService(db, agents);
3215
+ const deliveryStatusSvc = new GetDeliveryStatusService(db);
3216
+ const taskAddSvc = new TaskAddService(db, agents, events);
3217
+ const taskClaimSvc = new TaskClaimService(db, agents, events);
3218
+ const taskCompleteSvc = new TaskCompleteService(db, agents, events);
3219
+ const taskListSvc = new TaskListService(db, agents);
3220
+ const regContractSvc = new RegisterContractService(db, agents, events);
3221
+ const subContractSvc = new SubscribeContractService(db, agents);
3222
+ const getContractSvc = new GetContractService(db, agents);
3223
+ const diffContractsSvc = new DiffContractsService(db, agents);
3224
+ const pendingEventsSvc = new PendingContractEventsService(db, agents);
3225
+ const codexPanePreRegRepo = new CodexPanePreRegRepo(db);
3226
+ const preRegisterCodexPaneSvc = new PreRegisterCodexPaneService(codexPanePreRegRepo);
3227
+ function caller() {
3228
+ return getCallerAgentId();
3229
+ }
3230
+ async function run(fn) {
3231
+ const out = await wrapStorage(() => fn());
3232
+ touchIfRegistered();
3233
+ return toText(out);
3234
+ }
3235
+ function touchIfRegistered() {
3236
+ const c = caller();
3237
+ if (!c) return;
3238
+ try {
3239
+ if (agents.findById(c)) agents.touch(c);
3240
+ } catch {
3241
+ }
3242
+ }
3243
+ function requireAgent() {
3244
+ const c = caller();
3245
+ if (!c) return { error: "unknown_agent" };
3246
+ const row = agents.findById(c);
3247
+ if (!row) return { error: "unknown_agent" };
3248
+ return c;
3249
+ }
3250
+ async function autoBindRuntimeIdentity(args, callerAgentId) {
3251
+ const inferredAgent = inferRuntimeAgentKind(args, getSessionClientInfo?.());
3252
+ if (!inferredAgent) return false;
3253
+ if (args.ui_pid !== void 0) {
3254
+ const boundByPid = await bindRuntimeIdentitySvc.bind({
3255
+ callerAgentId,
3256
+ agent: inferredAgent,
3257
+ ui_pid: args.ui_pid
3258
+ });
3259
+ return "ok" in boundByPid && boundByPid.ok;
3260
+ }
3261
+ if (inferredAgent === "codex") {
3262
+ const auto = await autoBindCodexPane({
3263
+ callerAgentId,
3264
+ repo: codexPanePreRegRepo,
3265
+ bindRuntimeIdentitySvc
3266
+ });
3267
+ if (auto) return true;
3268
+ }
3269
+ const detected = await detectTmuxPane({ agent: inferredAgent });
3270
+ if (!("ok" in detected) || !detected.ok) return false;
3271
+ const bound = await bindRuntimeIdentitySvc.bind({
3272
+ callerAgentId,
3273
+ agent: inferredAgent,
3274
+ ui_tty: detected.pane.tty,
3275
+ tmux_pane_id: detected.pane.pane_id
3276
+ });
3277
+ return "ok" in bound && bound.ok;
3278
+ }
3279
+ async function preflightUiPidClient(args) {
3280
+ if (args.ui_pid === void 0) return void 0;
3281
+ const inferredAgent = inferRuntimeAgentKind(args, getSessionClientInfo?.());
3282
+ if (!inferredAgent) return void 0;
3283
+ const validated = await bindRuntimeIdentity({
3284
+ agent: inferredAgent,
3285
+ ui_pid: args.ui_pid
3286
+ });
3287
+ if (!("error" in validated) || validated.error !== "agent_process_mismatch") {
3288
+ return void 0;
3289
+ }
3290
+ return {
3291
+ error: "ui_pid_client_mismatch",
3292
+ detail: `ui_pid ${args.ui_pid} does not belong to client="${inferredAgent}". Pass the runtime kind for the process behind ui_pid; for example, use client="opencode" when ui_pid points at an opencode process.`
3293
+ };
3294
+ }
3295
+ const registerAgentInputSchema = z3.object({
3296
+ model: z3.string(),
3297
+ name: z3.string().min(1).refine((v) => v.trim().length > 0, { message: "name must not be empty" }),
3298
+ role: z3.string().optional(),
3299
+ team: z3.string().optional(),
3300
+ project_dir: z3.string().min(1).optional(),
3301
+ client: clientSchema,
3302
+ client_name: z3.string().min(1).optional(),
3303
+ ui_pid: z3.number().int().positive().optional().describe(
3304
+ "STRONGLY RECOMMENDED. Visible agent UI process pid (e.g. Claude Code CLI pid \u2014 `$PPID` from a Bash tool call inside Claude Code). Enables one-shot pid \u2192 tty \u2192 pane binding at registration; without it, tmux-based cross-agent poke delivery typically stays off."
3305
+ ),
3306
+ channel_session_id: z3.string().min(1).optional(),
3307
+ thread_id: z3.string().min(1).refine((v) => v.trim().length > 0, { message: "thread_id must not be empty" }).optional(),
3308
+ ws_url: z3.string().optional(),
3309
+ auth_token_ref: z3.string().min(1).optional(),
3310
+ claude_ui_pid: z3.number().int().positive().optional().describe(
3311
+ "Internal field for the cross-agent-teams-mcp channel proxy. Stores the proxy's parent Claude Code UI pid (`process.ppid`) so that Claude Code hosts registering in the same lineage can auto-bind their claude-channel delivery. Only valid when role='__channel_proxy__'; rejected otherwise."
3312
+ ),
3313
+ delivery: deliverySchema.optional()
3314
+ }).strict();
3315
+ const registerAgentArgsSchema = registerAgentInputSchema.superRefine((value, ctx) => {
3316
+ const hasCodexFields = value.thread_id !== void 0 || value.ws_url !== void 0 || value.auth_token_ref !== void 0;
3317
+ if (hasCodexFields && value.client !== "codex") {
3318
+ ctx.addIssue({
3319
+ code: z3.ZodIssueCode.custom,
3320
+ path: ["client"],
3321
+ message: "client=codex is required when thread_id, ws_url, or auth_token_ref is provided"
3322
+ });
3323
+ }
3324
+ if (value.channel_session_id !== void 0 && value.client !== "claude-code") {
3325
+ ctx.addIssue({
3326
+ code: z3.ZodIssueCode.custom,
3327
+ path: ["client"],
3328
+ message: "client=claude-code is required when channel_session_id is provided"
3329
+ });
3330
+ }
3331
+ if (value.client_name !== void 0 && value.client !== "custom") {
3332
+ ctx.addIssue({
3333
+ code: z3.ZodIssueCode.custom,
3334
+ path: ["client_name"],
3335
+ message: "client_name is only allowed when client=custom"
3336
+ });
3337
+ }
3338
+ if (value.claude_ui_pid !== void 0 && value.role !== "__channel_proxy__") {
3339
+ ctx.addIssue({
3340
+ code: z3.ZodIssueCode.custom,
3341
+ path: ["claude_ui_pid"],
3342
+ message: "claude_ui_pid is only allowed when role='__channel_proxy__'"
3343
+ });
3344
+ }
3345
+ });
3346
+ const registerClaudeSelfInputSchema = z3.object({
3347
+ name: z3.string().min(1).refine((v) => v.trim().length > 0, { message: "name must not be empty" }),
3348
+ model: z3.string().optional(),
3349
+ role: z3.string().optional(),
3350
+ team: z3.string().optional(),
3351
+ project_dir: z3.string().min(1).optional(),
3352
+ ui_pid: z3.number().int().positive().optional().describe(
3353
+ "STRONGLY RECOMMENDED. The Claude Code CLI pid (obtainable as `$PPID` from a Bash tool call inside Claude Code). Enables one-shot pid \u2192 tty \u2192 pane binding at registration; without it, tmux-based cross-agent poke delivery typically stays off until a separate `bind_runtime_identity(...)` call."
3354
+ ),
3355
+ channel_session_id: z3.string().min(1).optional()
3356
+ }).strict();
3357
+ const registerCodexSelfInputSchema = z3.object({
3358
+ name: z3.string().min(1).refine((v) => v.trim().length > 0, { message: "name must not be empty" }),
3359
+ model: z3.string().optional(),
3360
+ role: z3.string().optional(),
3361
+ team: z3.string().optional(),
3362
+ project_dir: z3.string().min(1).optional(),
3363
+ thread_id: z3.string().min(1).refine((v) => v.trim().length > 0, { message: "thread_id must not be empty" }).optional(),
3364
+ ws_url: z3.string().min(1).optional(),
3365
+ auth_token_ref: z3.string().min(1).optional()
3366
+ }).strict();
3367
+ async function executeRegister(args) {
3368
+ let nativeDeliveryBound = suppressTmuxHint(args);
3369
+ let autoBoundChannelCsid;
3370
+ const bindChannelSvc = channelWakeFanout ? new BindChannelService(db, channelWakeFanout) : void 0;
3371
+ const autoBindChannelSvc = channelWakeFanout ? new AutoBindChannelService(db, channelWakeFanout) : void 0;
3372
+ const connectionId = getSessionId?.() ?? caller();
3373
+ if (!connectionId) return { error: "unknown_agent" };
3374
+ const uiPidClientError = await preflightUiPidClient(args);
3375
+ if (uiPidClientError) return uiPidClientError;
3376
+ if (args.client === "claude-code" && args.channel_session_id !== void 0 && args.ui_pid !== void 0 && autoBindChannelSvc) {
3377
+ const proxyLookup = autoBindChannelSvc.lookup({
3378
+ ui_pid: args.ui_pid
3379
+ });
3380
+ if (proxyLookup.ok && proxyLookup.channel_session_id !== args.channel_session_id) {
3381
+ return {
3382
+ error: "channel_session_id_ui_pid_mismatch",
3383
+ detail: {
3384
+ ui_pid_matched_csid: proxyLookup.channel_session_id,
3385
+ supplied_csid: args.channel_session_id
3386
+ }
3387
+ };
3388
+ }
3389
+ }
3390
+ const hasCodexTransportFields = args.thread_id !== void 0 || args.ws_url !== void 0 || args.auth_token_ref !== void 0;
3391
+ const res = args.client === "codex" && args.delivery === void 0 && hasCodexTransportFields ? await registerCodexSelfSvc.register({
3392
+ connection_id: connectionId,
3393
+ name: args.name,
3394
+ model: args.model,
3395
+ role: args.role,
3396
+ team: args.team,
3397
+ project_dir: args.project_dir,
3398
+ thread_id: args.thread_id,
3399
+ ws_url: args.ws_url,
3400
+ auth_token_ref: args.auth_token_ref
3401
+ }) : registerSvc.register({
3402
+ connection_id: connectionId,
3403
+ client: args.client,
3404
+ client_name: args.client_name,
3405
+ model: args.model,
3406
+ name: args.name,
3407
+ role: args.role,
3408
+ team: args.team,
3409
+ project_dir: args.project_dir,
3410
+ delivery: args.delivery,
3411
+ claude_ui_pid: args.claude_ui_pid,
3412
+ runtime_ui_pid: args.client === "claude-code" ? args.ui_pid : void 0
3413
+ });
3414
+ if ("thread_id" in res && "agent_id" in res) {
3415
+ nativeDeliveryBound = true;
3416
+ }
3417
+ if ("agent_id" in res) {
3418
+ if (onRegisterSuccess) {
3419
+ try {
3420
+ onRegisterSuccess(res.agent_id, res.team);
3421
+ } catch {
3422
+ }
3423
+ } else if (fanout) {
3424
+ try {
3425
+ fanout.rebind(res.agent_id, res.team);
3426
+ } catch {
3427
+ }
3428
+ }
3429
+ if (args.client === "claude-code" && args.channel_session_id !== void 0) {
3430
+ const channelBind = bindChannelSvc ? bindChannelSvc.bind({
3431
+ callerAgentId: res.agent_id,
3432
+ channel_session_id: args.channel_session_id
3433
+ }) : { error: "unknown_channel_session" };
3434
+ if ("ok" in channelBind && channelBind.ok) {
3435
+ nativeDeliveryBound = true;
3436
+ } else {
3437
+ return channelBind;
3438
+ }
3439
+ }
3440
+ if (args.client === "claude-code" && args.channel_session_id === void 0 && args.ui_pid !== void 0 && autoBindChannelSvc) {
3441
+ const autoBind = autoBindChannelSvc.run({
3442
+ callerAgentId: res.agent_id,
3443
+ ui_pid: args.ui_pid
3444
+ });
3445
+ if (autoBind.ok) {
3446
+ autoBoundChannelCsid = autoBind.channel_session_id;
3447
+ nativeDeliveryBound = true;
3448
+ }
3449
+ }
3450
+ const autoBound = await autoBindRuntimeIdentity(args, res.agent_id);
3451
+ const envelope = autoBoundChannelCsid !== void 0 ? { ...res, channel_session_id: autoBoundChannelCsid } : res;
3452
+ if (autoBound) return envelope;
3453
+ if (!nativeDeliveryBound) {
3454
+ return {
3455
+ ...envelope,
3456
+ hint: "No usable tmux_pane_id is bound yet \u2014 automatic runtime binding did not converge for this session, so cross-agent poke delivery via tmux is still off. Call `bind_runtime_identity(...)` to bind explicitly, or use `detect_tmux_pane(...)` for debugging. Claude Code users who loaded the cross-agent-teams-mcp channel plugin can also route pokes via channel_session_id \u2014 that path does not require tmux binding."
3457
+ };
3458
+ }
3459
+ return envelope;
3460
+ }
3461
+ return res;
3462
+ }
3463
+ function releaseRegisteredState(agentId) {
3464
+ const connectionId = getSessionId?.();
3465
+ if (connectionId) registerSvc.releaseConnection(agentId, connectionId);
3466
+ if (onUnregisterSuccess) {
3467
+ try {
3468
+ onUnregisterSuccess(agentId);
3469
+ } catch {
3470
+ }
3471
+ return;
3472
+ }
3473
+ if (fanout) {
3474
+ try {
3475
+ fanout.detach(agentId);
3476
+ } catch {
3477
+ }
3478
+ }
3479
+ }
3480
+ server.registerTool(
3481
+ "pre_register_codex_pane",
3482
+ {
3483
+ title: "Pre-register codex tmux pane",
3484
+ description: [
3485
+ "Pre-register a pending tmux-pane claim so the launcher can claim a tmux pane before starting codex.",
3486
+ 'The launcher should call this with `$TMUX_PANE` and a freshly generated UUID, then `exec codex --remote ... -c xats.agent_id="\\"<uuid>\\""`.',
3487
+ 'When the codex agent later calls `register_agent({client:"codex"})` without `ui_pid`, the daemon uses the pending row to resolve the correct UI pid and auto-bind the pane.',
3488
+ "Callable without a prior `register_agent` \u2014 launchers have no agent identity yet.",
3489
+ "TTL defaults to 120 seconds and is capped at 600; pending rows are garbage-collected opportunistically."
3490
+ ].join(" "),
3491
+ inputSchema: preRegisterCodexPaneInputSchema
3492
+ },
3493
+ async (args) => run(async () => preRegisterCodexPaneSvc.register(args))
3494
+ );
3495
+ server.registerTool(
3496
+ "detect_tmux_pane",
3497
+ {
3498
+ title: "Detect tmux pane",
3499
+ description: [
3500
+ "Detect the tmux pane that is actually hosting a coding agent UI, even when the shell calling tools lives in a different pane.",
3501
+ "The detector scans tmux panes globally, maps each pane to its tty, then inspects real tty processes instead of trusting `$TMUX_PANE` or tmux focus state alone.",
3502
+ "Use `agent` to pick a built-in matcher for Codex, Claude Code, or opencode.",
3503
+ "Optional `cwd`, `tty`, and `title_contains` narrow the search and make cross-directory multi-agent sessions much more reliable.",
3504
+ "Returns either a single best pane, or an ambiguity/not-found result with candidates for debugging."
3505
+ ].join(" "),
3506
+ inputSchema: detectTmuxPaneSchema
3507
+ },
3508
+ async (args) => run(async () => {
3509
+ const parsed = detectTmuxPaneArgsSchema.safeParse(args);
3510
+ if (!parsed.success) {
3511
+ return {
3512
+ error: "invalid_arguments",
3513
+ detail: parsed.error.issues.map((issue) => issue.message).join("; ")
3514
+ };
3515
+ }
3516
+ return detectTmuxPane({
3517
+ agent: parsed.data.agent,
3518
+ cwd: parsed.data.cwd,
3519
+ tty: parsed.data.tty,
3520
+ title_contains: parsed.data.title_contains,
3521
+ process_pattern: parsed.data.process_pattern
3522
+ });
3523
+ })
3524
+ );
3525
+ server.registerTool(
3526
+ "register_agent",
3527
+ {
3528
+ title: "Register agent",
3529
+ description: [
3530
+ "Register this session as an agent in a team.",
3531
+ "This is the unified registration entry point.",
3532
+ "Calling this tool again with the same `(team, name, role)` identity reuses the existing",
3533
+ "`agent_id` and refreshes `tmux_pane_id` and `model`; no duplicate row is created.",
3534
+ "Callers MUST pass `client` explicitly.",
3535
+ 'Use `client="custom"` for unsupported agent harnesses; optionally provide `client_name` for observability.',
3536
+ "Claude Code sessions can pass `client=\"claude-code\"` together with `channel_session_id` to bind channel delivery through this same tool. PREFERRED: pass only `ui_pid` (from `$PPID`) and let the daemon auto-bind channel delivery \u2014 do not pass `channel_session_id` explicitly on register. When BOTH `ui_pid` AND `channel_session_id` are supplied, the daemon runs a consistency check against the caller `ui_pid`'s live channel proxy; if the proxy's csid does not match the supplied `channel_session_id`, the call is rejected with `channel_session_id_ui_pid_mismatch` before any agent row is written. Use `bind_channel` for low-level rebind after registration instead of supplying csid here.",
3537
+ 'Codex sessions can pass `client="codex"` together with `thread_id` to register Codex app-server delivery through this same tool.',
3538
+ "Codex clients SHOULD prefer `register_codex_self` instead \u2014 it is the codex-specific convenience entry point. Do NOT pass `ui_pid` from codex agents: the launcher's `pre_register_codex_pane` pre-reg flow handles tmux pane binding, and supplying `ui_pid` from codex disables that auto-bind path.",
3539
+ 'Requests such as "register to xats" or "register to cross-agent-teams" refer to this MCP service, not to the `team` field; do not set `team` to `xats` or `cross-agent-teams` from those phrases.',
3540
+ 'Do not treat the bare word "register" as a request for this tool unless the current conversation is already about cross-agent-teams registration.',
3541
+ "When the end user has not explicitly specified `team`, callers should pass `project_dir` as the current working directory so the daemon derives a project-scoped default team from its basename; if omitted, it falls back to `default`.",
3542
+ '`client` must describe the runtime behind `ui_pid`, not merely the current MCP caller. For example, if `ui_pid` points at an opencode process, pass `client="opencode"` even when the registration request is issued from Claude Code.',
3543
+ "STRONGLY RECOMMENDED: pass `ui_pid` unless it is truly unobtainable. Without it, automatic runtime binding usually fails to converge and tmux-based cross-agent poke delivery stays off until a separate `bind_runtime_identity(...)` call. From Claude Code, `$PPID` inside a Bash tool call is the `claude` CLI pid; for Codex/opencode/other harnesses, discover the UI pid from the host harness. With `ui_pid` the daemon binds via verified pid \u2192 tty \u2192 pane evidence in one shot.",
3544
+ "After registration, the daemon best-effort attempts runtime binding for recognized local clients so tmux-based poke delivery can come up without a second tool call.",
3545
+ "If automatic runtime binding does not converge, call `bind_runtime_identity(...)` explicitly so the daemon can verify and persist your pane binding.",
3546
+ "`detect_tmux_pane(...)` remains available as a debugging aid for ambiguous or missing matches, but it does not write registry state by itself.",
3547
+ "When registration still has no usable `tmux_pane_id`, tmux-based poke delivery stays unavailable until automatic or explicit runtime binding succeeds."
3548
+ ].join(" "),
3549
+ inputSchema: registerAgentInputSchema
3550
+ },
3551
+ async (args) => {
3552
+ return run(async () => executeRegister(registerAgentArgsSchema.parse(args)));
3553
+ }
3554
+ );
3555
+ server.registerTool(
3556
+ "register_claude_self",
3557
+ {
3558
+ title: "Register Claude Code session",
3559
+ description: [
3560
+ "Register the current Claude Code MCP session as an agent.",
3561
+ "Prefer this helper inside Claude Code when you want to avoid session-mismatch issues caused by external HTTP or curl registration.",
3562
+ "This tool always writes on the caller's current MCP session, so follow-up tools like get_inbox use the same identity immediately.",
3563
+ 'AUTO-BIND: when `ui_pid` is supplied AND `channel_session_id` is omitted, the daemon best-effort looks up a live `__channel_proxy__` row whose `claude_ui_pid` matches the caller `ui_pid` and auto-binds `delivery.kind="claude-channel"` to the proxy\'s current csid. On success the response includes `channel_session_id`. No match \u2192 delivery stays `none` (no error surfaced). This makes `ui_pid` sufficient for channel delivery \u2014 the LLM does not need to read or pass csid explicitly.',
3564
+ "If `channel_session_id` is supplied explicitly, the explicit value wins and auto-bind is skipped (identical semantics to `bind_channel`).",
3565
+ "When BOTH `ui_pid` AND `channel_session_id` are supplied, the daemon runs a consistency check: it looks up the live `__channel_proxy__` row matching the caller `ui_pid` and team, and if that proxy's persisted csid differs from the supplied `channel_session_id`, the call is rejected with `channel_session_id_ui_pid_mismatch` BEFORE any agent row is written. Prefer ui_pid-only registration (no `channel_session_id`) to avoid this class of stale-csid drift.",
3566
+ 'Requests such as "register to xats" or "register to cross-agent-teams" refer to this MCP service, not to the `team` field; do not set `team` to `xats` or `cross-agent-teams` from those phrases.',
3567
+ 'Do not treat the bare word "register" as a request for this tool unless the current conversation is already about cross-agent-teams registration.',
3568
+ "When the end user has not explicitly specified `team`, callers should pass `project_dir` as the current working directory so the daemon derives a project-scoped default team from its basename; if omitted, it falls back to `default`.",
3569
+ "STRONGLY RECOMMENDED: pass `ui_pid` (the Claude Code CLI pid \u2014 obtainable as `$PPID` from a Bash tool call). Without it, both channel auto-bind AND automatic tmux runtime binding stay off until a separate `bind_runtime_identity(...)` call. With `ui_pid` the daemon can auto-bind the channel delivery AND verify pid \u2192 tty \u2192 pane evidence in one shot.",
3570
+ "model is optional here; when omitted it falls back to a Claude-specific default."
3571
+ ].join(" "),
3572
+ inputSchema: registerClaudeSelfInputSchema
3573
+ },
3574
+ async (args) => run(async () => executeRegister({
3575
+ client: "claude-code",
3576
+ name: args.name,
3577
+ model: args.model ?? defaultClaudeSelfModel(getSessionClientInfo?.()),
3578
+ role: args.role,
3579
+ team: args.team,
3580
+ project_dir: args.project_dir,
3581
+ ui_pid: args.ui_pid,
3582
+ channel_session_id: args.channel_session_id
3583
+ }))
3584
+ );
3585
+ server.registerTool(
3586
+ "register_codex_self",
3587
+ {
3588
+ title: "Register codex MCP session",
3589
+ description: [
3590
+ "Register the current codex MCP session as an agent bound to `codex-appserver` delivery.",
3591
+ 'Prefer this helper inside codex over the generic `register_agent` \u2014 it locks `client="codex"` and keeps the input surface minimal.',
3592
+ "THREAD_ID: read `$CODEX_THREAD_ID` from your tool shell environment (codex 0.124.0+ exports it for every MCP tool subprocess) and pass its value as `thread_id`. When `thread_id` is omitted, the daemon returns the existing `thread_id_required` envelope listing resumable thread_ids as a candidate fallback.",
3593
+ "DO NOT pass `ui_pid`: this tool's schema rejects it outright. UI pid discovery and tmux pane binding are handled automatically by the launcher's `pre_register_codex_pane` pre-reg flow \u2014 passing `ui_pid` would silently disable that auto-bind path.",
3594
+ 'The daemon connects to `ws_url` (default `ws://127.0.0.1:8799`, env override `CROSS_AGENT_TEAMS_CODEX_WS_URL`), runs the codex `initialize` + `thread/resume` handshake, and writes `delivery.kind="codex-appserver"`.',
3595
+ 'Requests such as "register to xats" or "register to cross-agent-teams" refer to this MCP service, not to the `team` field; do not set `team` to `xats` or `cross-agent-teams` from those phrases.',
3596
+ "When the end user has not explicitly specified `team`, callers should pass `project_dir` as the current working directory so the daemon derives a project-scoped default team from its basename; if omitted, it falls back to `default`.",
3597
+ "model is optional here; when omitted it falls back to `gpt`."
3598
+ ].join(" "),
3599
+ inputSchema: registerCodexSelfInputSchema
3600
+ },
3601
+ async (args) => run(async () => executeRegister({
3602
+ client: "codex",
3603
+ name: args.name,
3604
+ model: args.model ?? "gpt",
3605
+ role: args.role,
3606
+ team: args.team,
3607
+ project_dir: args.project_dir,
3608
+ thread_id: args.thread_id,
3609
+ // Ensure the codex-appserver path is always taken even when thread_id is
3610
+ // omitted: callers of register_codex_self expect the thread_id_required
3611
+ // candidate-list fallback, not a plain delivery=none registration.
3612
+ // RegisterCodexSelfService resolves the empty string to env/default ws_url.
3613
+ ws_url: args.ws_url ?? "",
3614
+ auth_token_ref: args.auth_token_ref
3615
+ }))
3616
+ );
3617
+ server.registerTool(
3618
+ "unregister_self",
3619
+ {
3620
+ title: "Unregister current agent",
3621
+ description: [
3622
+ "Remove the caller session's current agent registration.",
3623
+ "This tool only unregisters the currently bound agent identity; it does not delete other agents.",
3624
+ "If the caller still owns any in-progress task, it returns `tasks_in_progress` and leaves all state unchanged.",
3625
+ "On success it deletes the agent row, removes the caller's contract subscriptions, and immediately releases the current MCP session back to an unregistered state."
3626
+ ].join(" "),
3627
+ inputSchema: z3.object({}).strict()
3628
+ },
3629
+ async () => {
3630
+ const who = requireAgent();
3631
+ if (typeof who !== "string") return toText(who);
3632
+ const result = await wrapStorage(() => unregisterSelfSvc.unregister({ caller: who }));
3633
+ if (typeof result === "object" && result !== null && "ok" in result && result.ok === true && "agent_id" in result && typeof result.agent_id === "string") {
3634
+ releaseRegisteredState(result.agent_id);
3635
+ return toText(result);
3636
+ }
3637
+ touchIfRegistered();
3638
+ return toText(result);
3639
+ }
3640
+ );
3641
+ server.registerTool(
3642
+ "list_agents",
3643
+ {
3644
+ title: "List agents",
3645
+ description: "List agents in the caller's team",
3646
+ inputSchema: {}
3647
+ },
3648
+ async () => {
3649
+ const who = requireAgent();
3650
+ if (typeof who !== "string") return toText(who);
3651
+ const row = agents.findById(who);
3652
+ return run(() => ({
3653
+ agents: agents.list({ team: row.team }).map(toPublicAgentRow)
3654
+ }));
3655
+ }
3656
+ );
3657
+ server.registerTool(
3658
+ "send_message",
3659
+ {
3660
+ title: "Send message",
3661
+ description: SEND_MESSAGE_DESC,
3662
+ inputSchema: z3.object({
3663
+ to_agent_name: z3.string().min(1),
3664
+ to_team: z3.string().min(1).optional(),
3665
+ subject: z3.string().optional(),
3666
+ body: z3.string().min(1),
3667
+ auto_poke: z3.boolean().optional(),
3668
+ need_reply: z3.boolean().optional()
3669
+ }).strict()
3670
+ },
3671
+ async (args) => {
3672
+ const who = requireAgent();
3673
+ if (typeof who !== "string") return toText(who);
3674
+ return run(() => sendSvc.send({ from: who, ...args }));
3675
+ }
3676
+ );
3677
+ server.registerTool(
3678
+ "send_message_by_id",
3679
+ {
3680
+ title: "Send message by id",
3681
+ description: SEND_MESSAGE_BY_ID_DESC,
3682
+ inputSchema: z3.object({
3683
+ to_agent_id: z3.string().min(1),
3684
+ subject: z3.string().optional(),
3685
+ body: z3.string().min(1),
3686
+ auto_poke: z3.boolean().optional(),
3687
+ need_reply: z3.boolean().optional()
3688
+ }).strict()
3689
+ },
3690
+ async (args) => {
3691
+ const who = requireAgent();
3692
+ if (typeof who !== "string") return toText(who);
3693
+ return run(() => sendSvc.send({ from: who, ...args }));
3694
+ }
3695
+ );
3696
+ server.registerTool(
3697
+ "broadcast",
3698
+ {
3699
+ title: "Broadcast message",
3700
+ description: BROADCAST_DESC,
3701
+ inputSchema: {
3702
+ subject: z3.string().optional(),
3703
+ body: z3.string(),
3704
+ auto_poke: z3.boolean().optional()
3705
+ }
3706
+ },
3707
+ async (args) => {
3708
+ const who = requireAgent();
3709
+ if (typeof who !== "string") return toText(who);
3710
+ return run(() => broadcastSvc.broadcast({ from: who, ...args }));
3711
+ }
3712
+ );
3713
+ server.registerTool(
3714
+ "broadcast_to_role",
3715
+ {
3716
+ title: "Broadcast to role",
3717
+ description: BROADCAST_TO_ROLE_DESC,
3718
+ inputSchema: z3.object({
3719
+ to_role: z3.string().min(1),
3720
+ subject: z3.string().optional(),
3721
+ body: z3.string().min(1),
3722
+ auto_poke: z3.boolean().optional()
3723
+ }).strict()
3724
+ },
3725
+ async (args) => {
3726
+ const who = requireAgent();
3727
+ if (typeof who !== "string") return toText(who);
3728
+ return run(() => broadcastToRoleSvc.broadcast({ from: who, ...args }));
3729
+ }
3730
+ );
3731
+ server.registerTool(
3732
+ "get_inbox",
3733
+ {
3734
+ title: "Get inbox",
3735
+ description: "Return messages addressed to caller after since_event_id",
3736
+ inputSchema: {
3737
+ since_event_id: z3.number().int().optional(),
3738
+ limit: z3.number().int().optional()
3739
+ }
3740
+ },
3741
+ async (args) => {
3742
+ const who = requireAgent();
3743
+ if (typeof who !== "string") return toText(who);
3744
+ return run(() => inboxSvc.get({ caller: who, ...args }));
3745
+ }
3746
+ );
3747
+ server.registerTool(
3748
+ "get_delivery_status",
3749
+ {
3750
+ title: "Get delivery status",
3751
+ description: [
3752
+ "Return wake-hint delivery status for a message sent by caller.",
3753
+ "Status describes auto-poke delivery only; mailbox persistence is already complete.",
3754
+ "Only the original sender can read a message delivery status."
3755
+ ].join(" "),
3756
+ inputSchema: {
3757
+ message_id: z3.string()
3758
+ }
3759
+ },
3760
+ async (args) => {
3761
+ const who = requireAgent();
3762
+ if (typeof who !== "string") return toText(who);
3763
+ return run(() => deliveryStatusSvc.get({ caller: who, ...args }));
3764
+ }
3765
+ );
3766
+ server.registerTool(
3767
+ "task_add",
3768
+ {
3769
+ title: "Add task",
3770
+ description: [
3771
+ "Add a new task to the team's task list. Any team member can claim it via `task_claim`",
3772
+ "on their next turn. The task will sit in the pending queue until someone pulls `task_list`.",
3773
+ "`task_add` itself does not wake or target any specific agent; use normal mailbox messaging",
3774
+ "when coordination is needed, then inspect that message with `get_delivery_status`."
3775
+ ].join(" "),
3776
+ inputSchema: {
3777
+ title: z3.string(),
3778
+ description: z3.string().optional(),
3779
+ depends_on: z3.array(z3.string()).optional()
3780
+ }
3781
+ },
3782
+ async (args) => {
3783
+ const who = requireAgent();
3784
+ if (typeof who !== "string") return toText(who);
3785
+ return run(() => taskAddSvc.add({ caller: who, ...args }));
3786
+ }
3787
+ );
3788
+ server.registerTool(
3789
+ "task_claim",
3790
+ {
3791
+ title: "Claim task",
3792
+ description: "Claim a pending task as caller",
3793
+ inputSchema: { task_id: z3.string() }
3794
+ },
3795
+ async (args) => {
3796
+ const who = requireAgent();
3797
+ if (typeof who !== "string") return toText(who);
3798
+ return run(() => taskClaimSvc.claim({ caller: who, task_id: args.task_id }));
3799
+ }
3800
+ );
3801
+ server.registerTool(
3802
+ "task_complete",
3803
+ {
3804
+ title: "Complete task",
3805
+ description: "Mark the caller's in-progress task as completed",
3806
+ inputSchema: {
3807
+ task_id: z3.string(),
3808
+ result: z3.string().optional()
3809
+ }
3810
+ },
3811
+ async (args) => {
3812
+ const who = requireAgent();
3813
+ if (typeof who !== "string") return toText(who);
3814
+ return run(() => taskCompleteSvc.complete({ caller: who, ...args }));
3815
+ }
3816
+ );
3817
+ server.registerTool(
3818
+ "task_list",
3819
+ {
3820
+ title: "List tasks",
3821
+ description: "List tasks in the caller's team, optionally filtered by status",
3822
+ inputSchema: {
3823
+ status: z3.enum(["pending", "in_progress", "completed"]).optional()
3824
+ }
3825
+ },
3826
+ async (args) => {
3827
+ const who = requireAgent();
3828
+ if (typeof who !== "string") return toText(who);
3829
+ return run(() => taskListSvc.list({ caller: who, status: args.status }));
3830
+ }
3831
+ );
3832
+ server.registerTool(
3833
+ "register_contract",
3834
+ {
3835
+ title: "Register contract",
3836
+ description: "Register or upgrade a contract version",
3837
+ inputSchema: {
3838
+ name: z3.string(),
3839
+ schema: z3.record(z3.unknown()),
3840
+ format: z3.literal("jsonschema").optional(),
3841
+ note: z3.string().optional()
3842
+ }
3843
+ },
3844
+ async (args) => {
3845
+ const who = requireAgent();
3846
+ if (typeof who !== "string") return toText(who);
3847
+ return run(() => {
3848
+ const res = regContractSvc.register({ caller: who, ...args });
3849
+ if ("version" in res && res._meta && fanout) {
3850
+ try {
3851
+ fanout.emitContractEvent(db, {
3852
+ to_team: res._meta.team,
3853
+ contract_name: res.name,
3854
+ version: res.version,
3855
+ event_id: res._meta.event_id,
3856
+ diff: res._meta.diff
3857
+ });
3858
+ } catch {
3859
+ }
3860
+ }
3861
+ if ("version" in res) {
3862
+ const { _meta: _omit, ...publicRes } = res;
3863
+ return publicRes;
3864
+ }
3865
+ return res;
3866
+ });
3867
+ }
3868
+ );
3869
+ server.registerTool(
3870
+ "subscribe_contract",
3871
+ {
3872
+ title: "Subscribe contract",
3873
+ description: "Subscribe the caller to a contract name's updates",
3874
+ inputSchema: { name: z3.string() }
3875
+ },
3876
+ async (args) => {
3877
+ const who = requireAgent();
3878
+ if (typeof who !== "string") return toText(who);
3879
+ return run(() => subContractSvc.subscribe({ caller: who, name: args.name }));
3880
+ }
3881
+ );
3882
+ server.registerTool(
3883
+ "get_contract",
3884
+ {
3885
+ title: "Get contract",
3886
+ description: "Fetch a contract version (latest by default)",
3887
+ inputSchema: {
3888
+ name: z3.string(),
3889
+ version: z3.number().int().optional()
3890
+ }
3891
+ },
3892
+ async (args) => {
3893
+ const who = requireAgent();
3894
+ if (typeof who !== "string") return toText(who);
3895
+ return run(() => getContractSvc.get({ caller: who, ...args }));
3896
+ }
3897
+ );
3898
+ server.registerTool(
3899
+ "diff_contracts",
3900
+ {
3901
+ title: "Diff contracts",
3902
+ description: "Compute diff between two versions of a contract",
3903
+ inputSchema: {
3904
+ name: z3.string(),
3905
+ from_version: z3.number().int(),
3906
+ to_version: z3.number().int()
3907
+ }
3908
+ },
3909
+ async (args) => {
3910
+ const who = requireAgent();
3911
+ if (typeof who !== "string") return toText(who);
3912
+ return run(() => diffContractsSvc.diff({ caller: who, ...args }));
3913
+ }
3914
+ );
3915
+ if (channelWakeFanout) {
3916
+ const bindSvc = new BindChannelService(db, channelWakeFanout);
3917
+ server.registerTool(
3918
+ "bind_channel",
3919
+ {
3920
+ title: "Bind channel_session_id to caller",
3921
+ description: [
3922
+ "Low-level rebind tool for Claude channel delivery.",
3923
+ "Bind the caller session's agent row to a channel_session_id produced by the cross-agent-teams-mcp channel proxy.",
3924
+ 'Most callers should prefer `register_agent({ client: "claude-code", channel_session_id, ... })` on the unified registration path.',
3925
+ "Call this when you need to rebind an already-registered row after the proxy announces a new csid.",
3926
+ "Rejects proxy callers (role=__channel_proxy__).",
3927
+ "Rejects unknown csid (no live proxy sink attached)."
3928
+ ].join(" "),
3929
+ inputSchema: {
3930
+ channel_session_id: z3.string().min(1)
3931
+ }
3932
+ },
3933
+ async (args) => {
3934
+ const who = requireAgent();
3935
+ if (typeof who !== "string") return toText(who);
3936
+ return run(() => bindSvc.bind({
3937
+ callerAgentId: who,
3938
+ channel_session_id: args.channel_session_id
3939
+ }));
3940
+ }
3941
+ );
3942
+ }
3943
+ server.registerTool(
3944
+ "bind_runtime_identity",
3945
+ {
3946
+ title: "Bind runtime identity to caller",
3947
+ description: [
3948
+ "Bind the caller session's agent row to a verified tmux runtime identity.",
3949
+ "Pass `agent` to choose the built-in process matcher (`codex`, `claude-code`, `opencode`), or use `custom` together with `process_pattern`.",
3950
+ "Prefer passing `ui_pid` for the visible agent UI process; the daemon verifies pid \u2192 tty \u2192 pane before persisting `tmux_pane_id`.",
3951
+ "If `ui_pid` is unavailable, pass `ui_tty` together with `tmux_pane_id` for a weaker but still verified binding path.",
3952
+ "This tool writes registry state; `detect_tmux_pane` is for debugging only."
3953
+ ].join(" "),
3954
+ inputSchema: bindRuntimeIdentitySchema
3955
+ },
3956
+ async (args) => {
3957
+ const parsed = bindRuntimeIdentityArgsSchema.safeParse(args);
3958
+ if (!parsed.success) {
3959
+ return toText({
3960
+ error: "invalid_arguments",
3961
+ detail: parsed.error.issues.map((issue) => issue.message).join("; ")
3962
+ });
3963
+ }
3964
+ const who = requireAgent();
3965
+ if (typeof who !== "string") return toText(who);
3966
+ return run(() => bindRuntimeIdentitySvc.bind({
3967
+ callerAgentId: who,
3968
+ agent: parsed.data.agent,
3969
+ ui_pid: parsed.data.ui_pid,
3970
+ ui_tty: parsed.data.ui_tty,
3971
+ tmux_pane_id: parsed.data.tmux_pane_id,
3972
+ process_pattern: parsed.data.process_pattern
3973
+ }));
3974
+ }
3975
+ );
3976
+ if (channelWakeFanout) {
3977
+ const subscribeSvc = new SubscribeChannelWakeService(db, channelWakeFanout);
3978
+ server.registerTool(
3979
+ "subscribe_channel_wake",
3980
+ {
3981
+ title: "Subscribe channel wake",
3982
+ description: [
3983
+ "Internal tool reserved for the cross-agent-teams-mcp channel proxy.",
3984
+ "Attaches the caller's MCP session notification sink to a channel_session_id so the",
3985
+ "daemon can emit notifications/channel_wake to it. Requires role=__channel_proxy__."
3986
+ ].join(" "),
3987
+ inputSchema: { channel_session_id: z3.string().min(1) }
3988
+ },
3989
+ async (args) => {
3990
+ const who = requireAgent();
3991
+ if (typeof who !== "string") return toText(who);
3992
+ const sid = getSessionId?.();
3993
+ if (!sid) return toText({ error: "unknown_session" });
3994
+ const sink = (payload) => {
3995
+ const t = getTransport?.();
3996
+ if (!t) return;
3997
+ try {
3998
+ void Promise.resolve(t.send(payload)).catch(() => {
3999
+ });
4000
+ } catch {
4001
+ }
4002
+ };
4003
+ return run(() => subscribeSvc.subscribe({
4004
+ callerAgentId: who,
4005
+ channel_session_id: args.channel_session_id,
4006
+ sessionId: sid,
4007
+ sink
4008
+ }));
4009
+ }
4010
+ );
4011
+ }
4012
+ server.registerTool(
4013
+ "pending_contract_events",
4014
+ {
4015
+ title: "Pending contract events",
4016
+ description: "Poll contract_registered events not yet seen",
4017
+ inputSchema: {
4018
+ since_event_id: z3.number().int().optional(),
4019
+ limit: z3.number().int().optional()
4020
+ }
4021
+ },
4022
+ async (args) => {
4023
+ const who = requireAgent();
4024
+ if (typeof who !== "string") return toText(who);
4025
+ return run(() => pendingEventsSvc.poll({ caller: who, ...args }));
4026
+ }
4027
+ );
4028
+ }
4029
+
4030
+ // src/mcp/transport.ts
4031
+ function mountMcp(app, db, fanout, channelWakeFanout) {
4032
+ const sessions = /* @__PURE__ */ new Map();
4033
+ const sessionOwners = /* @__PURE__ */ new Map();
4034
+ function createSession() {
4035
+ const server = new McpServer(
4036
+ { name: "cross-agent-teams-mcp", version: "0.1.0" },
4037
+ {
4038
+ instructions: 'xats \u662F cross-agent-teams \u7684\u7F29\u5199. \u5F53\u7528\u6237\u6216\u5176\u4ED6 agent \u63D0\u5230 xats \u65F6, \u6307\u7684\u662F\u672C MCP server (cross-agent-teams-mcp) \u53CA\u5176\u8DE8 agent \u534F\u4F5C\u5DE5\u5177 (register_agent, send_message, task_* \u7B49). \u7528\u6237\u8BF4"\u6CE8\u518C\u5230 xats"\u6216"\u6CE8\u518C\u5230 cross-agent-teams"\u65F6, \u8868\u793A\u6CE8\u518C\u5230\u672C MCP \u670D\u52A1, xats \u548C cross-agent-teams \u4E0D\u662F team \u540D, \u4E0D\u8981\u628A\u5B83\u4EEC\u4F20\u7ED9 team. \u4E0D\u8981\u628A\u5355\u72EC\u7684\u5E38\u7528\u8BCD"\u6CE8\u518C"\u9ED8\u8BA4\u52AB\u6301\u4E3A\u672C\u5DE5\u5177; \u53EA\u6709\u5F53\u524D\u4E0A\u4E0B\u6587\u5DF2\u7ECF\u5728\u8BA8\u8BBA cross-agent-teams \u6CE8\u518C\u65F6\u624D\u53EF\u7EED\u63A5. \u6CE8\u518C\u65F6 (register_agent / register_claude_self) \u5982\u679C\u7528\u6237\u672A\u663E\u5F0F\u6307\u5B9A team, \u9ED8\u8BA4\u4F20 project_dir=\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55 (cwd), daemon \u4F1A\u7528\u5B83\u7684 basename \u4F5C\u4E3A team \u9ED8\u8BA4\u503C; \u90FD\u4E0D\u4F20\u65F6\u56DE\u843D default. Codex clients: if your tool shell env has CODEX_THREAD_ID set (codex 0.124.0+ exports it), pass its value as `thread_id` and prefer `register_codex_self` over `register_agent`. Do NOT attempt to discover or pass `ui_pid` from codex \u2014 the launcher\'s `pre_register_codex_pane` pre-reg flow handles tmux pane binding automatically; supplying `ui_pid` manually disables that auto-bind path.'
4039
+ }
4040
+ );
4041
+ const agentIdHolder = { current: void 0 };
4042
+ server.registerTool("echo", { title: "Echo", description: "Return the input", inputSchema: echoSchema }, echoHandler);
4043
+ let sessionIdForCaller;
4044
+ const getCallerAgentId = () => agentIdHolder.current ?? sessionIdForCaller;
4045
+ const sink = {
4046
+ send(msg) {
4047
+ const payload = {
4048
+ jsonrpc: "2.0",
4049
+ method: "notifications/contract_event",
4050
+ params: msg
4051
+ };
4052
+ void transport.send(payload).catch(() => {
4053
+ });
4054
+ },
4055
+ sendHeartbeat() {
4056
+ void transport.send({
4057
+ jsonrpc: "2.0",
4058
+ method: "notifications/heartbeat",
4059
+ params: {}
4060
+ }).catch(() => {
4061
+ });
4062
+ },
4063
+ close() {
4064
+ }
4065
+ };
4066
+ const onRegisterSuccess = (agent_id, team) => {
4067
+ try {
4068
+ fanout.detach(agent_id);
4069
+ } catch {
4070
+ }
4071
+ if (agentIdHolder.current && agentIdHolder.current !== agent_id) {
4072
+ try {
4073
+ fanout.detach(agentIdHolder.current);
4074
+ } catch {
4075
+ }
4076
+ }
4077
+ fanout.attach(agent_id, team, sink);
4078
+ agentIdHolder.current = agent_id;
4079
+ };
4080
+ const onUnregisterSuccess = (agent_id) => {
4081
+ try {
4082
+ fanout.detach(agent_id);
4083
+ } catch {
4084
+ }
4085
+ if (sessionIdForCaller && channelWakeFanout) {
4086
+ try {
4087
+ channelWakeFanout.detachBySession(sessionIdForCaller);
4088
+ } catch {
4089
+ }
4090
+ }
4091
+ if (agentIdHolder.current === agent_id) agentIdHolder.current = void 0;
4092
+ };
4093
+ const transport = new StreamableHTTPServerTransport({
4094
+ sessionIdGenerator: () => randomUUID6(),
4095
+ onsessioninitialized: (sid) => {
4096
+ sessionIdForCaller = sid;
4097
+ sessions.set(sid, { transport, server, sessionId: sid, agentIdHolder, clientInfo: void 0 });
4098
+ }
4099
+ });
4100
+ transport.onclose = () => {
4101
+ if (agentIdHolder.current) {
4102
+ try {
4103
+ fanout.detach(agentIdHolder.current);
4104
+ } catch {
4105
+ }
4106
+ }
4107
+ if (transport.sessionId && channelWakeFanout) {
4108
+ try {
4109
+ channelWakeFanout.detachBySession(transport.sessionId);
4110
+ } catch {
4111
+ }
4112
+ }
4113
+ if (transport.sessionId) {
4114
+ sessions.delete(transport.sessionId);
4115
+ sessionOwners.delete(transport.sessionId);
4116
+ }
4117
+ };
4118
+ registerBusinessTools(
4119
+ server,
4120
+ db,
4121
+ getCallerAgentId,
4122
+ fanout,
4123
+ onRegisterSuccess,
4124
+ () => sessionIdForCaller,
4125
+ channelWakeFanout,
4126
+ () => transport,
4127
+ () => {
4128
+ const sid = sessionIdForCaller;
4129
+ if (!sid) return void 0;
4130
+ return sessions.get(sid)?.clientInfo;
4131
+ },
4132
+ onUnregisterSuccess
4133
+ );
4134
+ server.connect(transport);
4135
+ return { transport, server, sessionId: "", agentIdHolder };
4136
+ }
4137
+ function authHashFor(req) {
4138
+ const raw = req.headers["authorization"];
4139
+ if (typeof raw !== "string") return null;
4140
+ const trimmed = raw.trim();
4141
+ if (trimmed.length === 0) return null;
4142
+ return createHash("sha256").update(trimmed).digest("hex");
4143
+ }
4144
+ app.post("/mcp", async (req, reply) => {
4145
+ const sid = req.headers["mcp-session-id"];
4146
+ const body = req.body;
4147
+ const isInit = body?.method === "initialize";
4148
+ let session = sid ? sessions.get(sid) : void 0;
4149
+ if (!session && !isInit) {
4150
+ return reply.code(400).send({ error: "unknown_session" });
4151
+ }
4152
+ if (session && body?.method === "tools/call" && body.params?.name === "register_agent") {
4153
+ const authHash = authHashFor(req);
4154
+ if (authHash !== null) {
4155
+ const owner = sessionOwners.get(session.sessionId);
4156
+ if (owner && owner !== authHash) {
4157
+ return reply.code(409).send({ error: "agent_id_collision" });
4158
+ }
4159
+ if (!owner) sessionOwners.set(session.sessionId, authHash);
4160
+ }
4161
+ }
4162
+ if (session && body?.method === "tools/call") {
4163
+ const claimed = body.params?.arguments?.from_agent_id;
4164
+ if (typeof claimed === "string") {
4165
+ const current = session.agentIdHolder.current;
4166
+ if (current === void 0 || claimed !== current) {
4167
+ return reply.code(403).send({ error: "identity_mismatch" });
4168
+ }
4169
+ }
4170
+ }
4171
+ if (!session) {
4172
+ session = createSession();
4173
+ }
4174
+ if (body?.method === "initialize") {
4175
+ const params = body.params;
4176
+ const clientInfo = params?.clientInfo;
4177
+ session.clientInfo = {
4178
+ name: typeof clientInfo?.name === "string" ? clientInfo.name : void 0,
4179
+ version: typeof clientInfo?.version === "string" ? clientInfo.version : void 0
4180
+ };
4181
+ }
4182
+ await session.transport.handleRequest(req.raw, reply.raw, body);
4183
+ return reply;
4184
+ });
4185
+ app.get("/mcp", async (req, reply) => {
4186
+ const sid = req.headers["mcp-session-id"];
4187
+ const session = sid ? sessions.get(sid) : void 0;
4188
+ if (!session) return reply.code(400).send({ error: "unknown_session" });
4189
+ await session.transport.handleRequest(req.raw, reply.raw);
4190
+ return reply;
4191
+ });
4192
+ app.delete("/mcp", async (req, reply) => {
4193
+ const sid = req.headers["mcp-session-id"];
4194
+ const session = sid ? sessions.get(sid) : void 0;
4195
+ if (!session) return reply.code(400).send({ error: "unknown_session" });
4196
+ await session.transport.handleRequest(req.raw, reply.raw);
4197
+ return reply;
4198
+ });
4199
+ }
4200
+
4201
+ // src/daemon/cleanup.ts
4202
+ var DELETE_AGED_EVENTS_SQL = `
4203
+ WITH online_cursor AS (
4204
+ SELECT team AS to_team, MIN(last_processed_event_id) AS min_cursor
4205
+ FROM agents
4206
+ WHERE last_seen_at >= :cutoffOnline
4207
+ GROUP BY team
4208
+ )
4209
+ DELETE FROM events
4210
+ WHERE created_at < :ageCutoff
4211
+ AND (
4212
+ events.to_team NOT IN (SELECT to_team FROM online_cursor)
4213
+ OR events.event_id < (
4214
+ SELECT min_cursor FROM online_cursor WHERE online_cursor.to_team = events.to_team
4215
+ )
4216
+ )
4217
+ `;
4218
+ function runCleanup(db, opts = {}) {
4219
+ const now = opts.now ?? /* @__PURE__ */ new Date();
4220
+ const maxAgeDays = opts.maxAgeDays ?? 7;
4221
+ const onlineWindowMs = opts.onlineWindowMs ?? 5 * 60 * 1e3;
4222
+ const ageCutoff = new Date(now.getTime() - maxAgeDays * 86400 * 1e3).toISOString();
4223
+ const cutoffOnline = new Date(now.getTime() - onlineWindowMs).toISOString();
4224
+ const info = db.prepare(DELETE_AGED_EVENTS_SQL).run({ ageCutoff, cutoffOnline });
4225
+ return { deleted: Number(info.changes) };
4226
+ }
4227
+
4228
+ // src/daemon/sse-fanout.ts
4229
+ var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
4230
+ function resolveHeartbeatIntervalMs(opt) {
4231
+ if (typeof opt === "number" && opt > 0) return opt;
4232
+ const n = Number(process.env.HEARTBEAT_INTERVAL_MS);
4233
+ return Number.isInteger(n) && n > 0 ? n : DEFAULT_HEARTBEAT_INTERVAL_MS;
4234
+ }
4235
+ var SseFanout = class {
4236
+ sessions = /* @__PURE__ */ new Map();
4237
+ heartbeatTimer;
4238
+ heartbeatIntervalMs;
4239
+ constructor(opts = {}) {
4240
+ this.heartbeatIntervalMs = resolveHeartbeatIntervalMs(opts.heartbeatIntervalMs);
4241
+ }
4242
+ attach(agent_id, team, sink) {
4243
+ const prior = this.sessions.get(agent_id);
4244
+ if (prior && prior.sink !== sink) {
4245
+ try {
4246
+ prior.sink.close();
4247
+ } catch {
4248
+ }
4249
+ }
4250
+ const wasEmpty = this.sessions.size === 0;
4251
+ this.sessions.set(agent_id, { agent_id, team, sink });
4252
+ if (wasEmpty) this.startHeartbeat();
4253
+ }
4254
+ rebind(agent_id, team) {
4255
+ const s = this.sessions.get(agent_id);
4256
+ if (!s) return;
4257
+ this.sessions.set(agent_id, { agent_id, team, sink: s.sink });
4258
+ }
4259
+ detach(agent_id) {
4260
+ const s = this.sessions.get(agent_id);
4261
+ if (s) {
4262
+ try {
4263
+ s.sink.close();
4264
+ } catch {
4265
+ }
4266
+ this.sessions.delete(agent_id);
4267
+ }
4268
+ if (this.sessions.size === 0) this.stopHeartbeat();
4269
+ }
4270
+ stopAll() {
4271
+ this.stopHeartbeat();
4272
+ for (const s of this.sessions.values()) {
4273
+ try {
4274
+ s.sink.close();
4275
+ } catch {
4276
+ }
4277
+ }
4278
+ this.sessions.clear();
4279
+ }
4280
+ peek() {
4281
+ return Array.from(this.sessions.values()).map((s) => ({ agent_id: s.agent_id, team: s.team }));
4282
+ }
4283
+ emitContractEvent(db, args) {
4284
+ const subs = db.prepare(
4285
+ `SELECT agent_id FROM contract_subscriptions WHERE team=? AND contract_name=?`
4286
+ ).all(args.to_team, args.contract_name);
4287
+ const subscribedSet = new Set(subs.map((s) => s.agent_id));
4288
+ for (const session of this.sessions.values()) {
4289
+ if (session.team !== args.to_team) continue;
4290
+ if (!subscribedSet.has(session.agent_id)) continue;
4291
+ try {
4292
+ session.sink.send({
4293
+ type: "contract_event",
4294
+ event_id: args.event_id,
4295
+ contract_name: args.contract_name,
4296
+ version: args.version,
4297
+ diff: args.diff
4298
+ });
4299
+ } catch {
4300
+ }
4301
+ }
4302
+ }
4303
+ startHeartbeat() {
4304
+ if (this.heartbeatTimer) return;
4305
+ this.heartbeatTimer = setInterval(() => {
4306
+ for (const s of this.sessions.values()) {
4307
+ try {
4308
+ s.sink.sendHeartbeat();
4309
+ } catch {
4310
+ }
4311
+ }
4312
+ }, this.heartbeatIntervalMs);
4313
+ if (typeof this.heartbeatTimer.unref === "function") this.heartbeatTimer.unref();
4314
+ }
4315
+ stopHeartbeat() {
4316
+ if (this.heartbeatTimer) {
4317
+ clearInterval(this.heartbeatTimer);
4318
+ this.heartbeatTimer = void 0;
4319
+ }
4320
+ }
4321
+ };
4322
+
4323
+ // src/daemon/channel-wake-fanout.ts
4324
+ var ChannelWakeFanout = class {
4325
+ entries = /* @__PURE__ */ new Map();
4326
+ attach(channel_session_id, sink, sessionId) {
4327
+ this.entries.set(channel_session_id, { sessionId, sink });
4328
+ }
4329
+ detach(channel_session_id) {
4330
+ this.entries.delete(channel_session_id);
4331
+ }
4332
+ detachBySession(sessionId) {
4333
+ for (const [csid, entry] of this.entries) {
4334
+ if (entry.sessionId === sessionId) this.entries.delete(csid);
4335
+ }
4336
+ }
4337
+ send(channel_session_id, payload) {
4338
+ const entry = this.entries.get(channel_session_id);
4339
+ if (!entry) return false;
4340
+ try {
4341
+ entry.sink(payload);
4342
+ } catch {
4343
+ }
4344
+ return true;
4345
+ }
4346
+ has(channel_session_id) {
4347
+ return this.entries.has(channel_session_id);
4348
+ }
4349
+ };
4350
+
4351
+ // src/daemon/server.ts
4352
+ var DEFAULT_KEEP_ALIVE_TIMEOUT_MS = 12e4;
4353
+ function parsePositiveInt(raw, fallback) {
4354
+ const n = Number(raw);
4355
+ return Number.isInteger(n) && n > 0 ? n : fallback;
4356
+ }
4357
+ async function buildServer(opts) {
4358
+ const keepAliveTimeout = parsePositiveInt(process.env.KEEP_ALIVE_TIMEOUT_MS, DEFAULT_KEEP_ALIVE_TIMEOUT_MS);
4359
+ const app = Fastify({ logger: false, keepAliveTimeout });
4360
+ app.server.headersTimeout = keepAliveTimeout + 1e3;
4361
+ const db = openDb(opts.dbPath);
4362
+ applySchema(db);
4363
+ const startedAt = Date.now();
4364
+ const version = "0.1.0";
4365
+ const fanout = opts.fanout ?? new SseFanout();
4366
+ const channelWakeFanout = opts.channelWakeFanout ?? new ChannelWakeFanout();
4367
+ app.addHook("onRequest", makeAuthHook(opts.token));
4368
+ app.get("/health", async () => ({ ok: true, version, uptime_seconds: Math.floor((Date.now() - startedAt) / 1e3) }));
4369
+ mountMcp(app, db, fanout, channelWakeFanout);
4370
+ const cleanupIntervalMs = opts.cleanupIntervalMs ?? Number(process.env.CLEANUP_INTERVAL_MS ?? 60 * 60 * 1e3);
4371
+ const interval = setInterval(() => {
4372
+ try {
4373
+ runCleanup(db);
4374
+ } catch {
4375
+ }
4376
+ }, cleanupIntervalMs);
4377
+ if (typeof interval.unref === "function") interval.unref();
4378
+ app.addHook("onClose", async () => {
4379
+ clearInterval(interval);
4380
+ clearAllRetries();
4381
+ fanout.stopAll();
4382
+ db.close();
4383
+ });
4384
+ return app;
4385
+ }
4386
+ async function startServer(opts) {
4387
+ const app = await buildServer(opts);
4388
+ const host = opts.host ?? "127.0.0.1";
4389
+ await app.listen({ port: opts.port, host });
4390
+ const addr = app.server.address();
4391
+ const port = addr && typeof addr === "object" ? addr.port : opts.port;
4392
+ return { app, port, host };
4393
+ }
4394
+
4395
+ // src/daemon/pid.ts
4396
+ import { existsSync, mkdirSync as mkdirSync2, readFileSync, rmSync, writeFileSync } from "fs";
4397
+ import { dirname as dirname2 } from "path";
4398
+ function isAlive(pid) {
4399
+ try {
4400
+ process.kill(pid, 0);
4401
+ return true;
4402
+ } catch (e) {
4403
+ const err = e;
4404
+ if (err.code === "EPERM") return true;
4405
+ return false;
4406
+ }
4407
+ }
4408
+ function acquirePidFile(path, port) {
4409
+ mkdirSync2(dirname2(path), { recursive: true });
4410
+ if (existsSync(path)) {
4411
+ try {
4412
+ const prev = JSON.parse(readFileSync(path, "utf8"));
4413
+ if (isAlive(prev.pid) && prev.pid !== process.pid) {
4414
+ return { ok: false, reason: "already_running", pid: prev.pid, port: prev.port };
4415
+ }
4416
+ } catch {
4417
+ }
4418
+ }
4419
+ writeFileSync(path, JSON.stringify({ pid: process.pid, port }));
4420
+ return { ok: true };
4421
+ }
4422
+ function releasePidFile(path) {
4423
+ if (existsSync(path)) rmSync(path, { force: true });
4424
+ }
4425
+
4426
+ // src/daemon/shutdown.ts
4427
+ function wireShutdown(app, pidPath) {
4428
+ const handler = async (_signal) => {
4429
+ try {
4430
+ await app.close();
4431
+ } catch {
4432
+ }
4433
+ releasePidFile(pidPath);
4434
+ process.exit(0);
4435
+ };
4436
+ process.once("SIGTERM", handler);
4437
+ process.once("SIGINT", handler);
4438
+ }
4439
+
4440
+ // src/daemon/port.ts
4441
+ import { createServer } from "net";
4442
+ function tryBind(port, host) {
4443
+ return new Promise((resolve) => {
4444
+ const s = createServer();
4445
+ s.once("error", () => resolve(false));
4446
+ s.listen(port, host, () => s.close(() => resolve(true)));
4447
+ });
4448
+ }
4449
+ async function selectPort(candidates, host = "127.0.0.1") {
4450
+ for (const p of candidates) {
4451
+ if (await tryBind(p, host)) return p;
4452
+ }
4453
+ throw new Error(`ports ${candidates[0]}-${candidates[candidates.length - 1]} unavailable`);
4454
+ }
4455
+
4456
+ // src/cli.ts
4457
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4458
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4459
+ function parseArg(name, def) {
4460
+ const i = process.argv.indexOf(name);
4461
+ return i >= 0 && i + 1 < process.argv.length ? process.argv[i + 1] : def;
4462
+ }
4463
+ function defaultHome() {
4464
+ return process.env.CROSS_AGENT_TEAMS_MCP_HOME ?? join(homedir(), ".cross-agent-teams-mcp");
4465
+ }
4466
+ async function runDaemon() {
4467
+ const home = defaultHome();
4468
+ const pidPath = parseArg("--pid-file", join(home, "daemon.pid"));
4469
+ const dbPath = parseArg("--db", join(home, "data.db"));
4470
+ const token = parseArg("--token");
4471
+ const requested = Number(parseArg("--port", "9100"));
4472
+ const port = requested === 0 ? 0 : await selectPort([requested, requested + 1, requested + 2]);
4473
+ const r = acquirePidFile(pidPath, port || requested);
4474
+ if (!r.ok) {
4475
+ console.error("daemon already running pid=" + r.pid);
4476
+ process.exit(1);
4477
+ }
4478
+ const started = await startServer({ dbPath, token, port });
4479
+ wireShutdown(started.app, pidPath);
4480
+ console.log(`listening on ${started.host}:${started.port}`);
4481
+ }
4482
+ function resolveDaemonPort(explicit) {
4483
+ if (explicit !== void 0) {
4484
+ const n = Number(explicit);
4485
+ if (Number.isInteger(n) && n > 0) return n;
4486
+ return void 0;
4487
+ }
4488
+ const pidPath = parseArg("--pid-file", join(defaultHome(), "daemon.pid"));
4489
+ if (!existsSync2(pidPath)) return void 0;
4490
+ try {
4491
+ const parsed = JSON.parse(readFileSync2(pidPath, "utf8"));
4492
+ if (typeof parsed.port === "number" && parsed.port > 0) return parsed.port;
4493
+ } catch {
4494
+ }
4495
+ return void 0;
4496
+ }
4497
+ async function runPreRegisterCodexPane() {
4498
+ const pane = parseArg("--pane");
4499
+ const agentId = parseArg("--agent-id");
4500
+ const ttlRaw = parseArg("--ttl");
4501
+ const tokenExplicit = parseArg("--token");
4502
+ const portExplicit = parseArg("--port");
4503
+ if (!pane || !agentId) {
4504
+ console.error("usage: cross-agent-teams-mcp pre-register-codex-pane --pane <pane_id> --agent-id <uuid> [--ttl <seconds>] [--port <n>] [--token <t>]");
4505
+ process.exit(2);
4506
+ }
4507
+ const port = resolveDaemonPort(portExplicit);
4508
+ if (!port) {
4509
+ console.error('{"ok":false,"error":"daemon_port_unresolved","detail":"pass --port or start the daemon so the pid file is present"}');
4510
+ process.exit(1);
4511
+ }
4512
+ const token = tokenExplicit ?? process.env.CROSS_AGENT_TEAMS_MCP_TOKEN;
4513
+ const host = process.env.CROSS_AGENT_TEAMS_MCP_HOST ?? "127.0.0.1";
4514
+ const base = new URL(`http://${host}:${port}/mcp`);
4515
+ const requestInit = token ? { headers: { Authorization: `Bearer ${token}` } } : void 0;
4516
+ const transport = new StreamableHTTPClientTransport(base, {
4517
+ requestInit
4518
+ });
4519
+ const client = new Client({ name: "cross-agent-teams-mcp-cli", version: "0.1.0" });
4520
+ try {
4521
+ await client.connect(transport);
4522
+ const args = {
4523
+ pane_id: pane,
4524
+ xats_agent_id: agentId
4525
+ };
4526
+ if (ttlRaw !== void 0) {
4527
+ const ttl = Number(ttlRaw);
4528
+ if (!Number.isInteger(ttl) || ttl <= 0) {
4529
+ console.error('{"ok":false,"error":"invalid_ttl"}');
4530
+ process.exit(2);
4531
+ }
4532
+ args.ttl_seconds = ttl;
4533
+ }
4534
+ const resp = await client.callTool({
4535
+ name: "pre_register_codex_pane",
4536
+ arguments: args
4537
+ });
4538
+ const content = resp.content;
4539
+ const text = content?.[0]?.text ?? "";
4540
+ let parsed;
4541
+ try {
4542
+ parsed = JSON.parse(text);
4543
+ } catch {
4544
+ parsed = { raw: text };
4545
+ }
4546
+ const obj = parsed ?? {};
4547
+ if (obj.ok === true) {
4548
+ console.log(JSON.stringify(obj));
4549
+ process.exit(0);
4550
+ }
4551
+ console.error(JSON.stringify(obj));
4552
+ process.exit(1);
4553
+ } catch (error) {
4554
+ const msg = error instanceof Error ? error.message : String(error);
4555
+ console.error(JSON.stringify({ ok: false, error: "cli_failed", detail: msg }));
4556
+ process.exit(1);
4557
+ } finally {
4558
+ try {
4559
+ await transport.close();
4560
+ } catch {
4561
+ }
4562
+ try {
4563
+ await client.close();
4564
+ } catch {
4565
+ }
4566
+ }
4567
+ }
4568
+ async function main() {
4569
+ const cmd = process.argv[2];
4570
+ if (cmd === "daemon") {
4571
+ await runDaemon();
4572
+ return;
4573
+ }
4574
+ if (cmd === "pre-register-codex-pane") {
4575
+ await runPreRegisterCodexPane();
4576
+ return;
4577
+ }
4578
+ console.error("usage: cross-agent-teams-mcp <daemon|pre-register-codex-pane> [options]");
4579
+ process.exit(2);
4580
+ }
4581
+ main().catch((e) => {
4582
+ console.error(e?.message ?? e);
4583
+ process.exit(1);
4584
+ });
4585
+ //# sourceMappingURL=cli.js.map