multi-agent-collaboration-mcp 0.12.0 → 0.13.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.
- package/README.md +227 -138
- package/dist/build-info.json +1 -1
- package/dist/check.js +25 -140
- package/dist/db.js +670 -824
- package/dist/index.js +841 -422
- package/dist/poller.js +188 -81
- package/package.json +2 -5
- package/scripts/stamp-build.mjs +4 -2
- package/web/index.html +19 -39
- package/web/server.mjs +82 -165
- package/scripts/wait-for-updates.sh +0 -39
package/dist/check.js
CHANGED
|
@@ -8,12 +8,11 @@
|
|
|
8
8
|
// Diagnostic one-shot. The background watcher is src/poller.ts and keeps one
|
|
9
9
|
// connection instead of launching this process on every interval.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// exists.
|
|
11
|
+
// Baselines are the persona's read markers (memberships.last_read_seq). This
|
|
12
|
+
// probe is deliberately NOT epoch-bound: it is a one-shot diagnostic answering
|
|
13
|
+
// "does this persona have unread work", a question that stays meaningful
|
|
14
|
+
// regardless of which runtime currently holds the persona. The watcher that
|
|
15
|
+
// speaks FOR a runtime is poller.ts, and that one is bound.
|
|
17
16
|
import Database from "better-sqlite3";
|
|
18
17
|
import { existsSync, writeFileSync } from "node:fs";
|
|
19
18
|
import { homedir } from "node:os";
|
|
@@ -27,14 +26,12 @@ function fail(msg) {
|
|
|
27
26
|
}
|
|
28
27
|
const USAGE = `agent-chat-check: one-shot, read-only unread probe.
|
|
29
28
|
Usage:
|
|
30
|
-
check.js --agent <agent_id> [--
|
|
31
|
-
check.js --room <id|name> --agent <agent_id> [--
|
|
29
|
+
check.js --agent <agent_id> [--mentions-only] # all rooms
|
|
30
|
+
check.js --room <id|name> --agent <agent_id> [--since <seq>]
|
|
32
31
|
Flags:
|
|
33
|
-
--agent <id>
|
|
32
|
+
--agent <id> persona to check; baselines are its read markers
|
|
34
33
|
--room <id|name> scope to one room (default: every room the agent is in)
|
|
35
34
|
--since <seq> explicit baseline instead of the read marker (needs --room)
|
|
36
|
-
--session <nonce> baseline off this session's private cursor where it exists;
|
|
37
|
-
all-rooms mode also mutes rooms that session left
|
|
38
35
|
--mentions-only count only messages that mention --agent or reply to it
|
|
39
36
|
--help, -h print this and exit 0
|
|
40
37
|
Exit codes: 0 = updates exist (JSON status on stdout; rooms_with_updates names
|
|
@@ -42,10 +39,9 @@ up to 20 firing rooms on the all-rooms path and
|
|
|
42
39
|
rooms_with_updates_truncated:true means more fired), 1 = nothing new, 2 = error.
|
|
43
40
|
`;
|
|
44
41
|
function parseArgs(argv) {
|
|
45
|
-
const out = { mentionsOnly: false
|
|
42
|
+
const out = { mentionsOnly: false };
|
|
46
43
|
for (let i = 0; i < argv.length; i++) {
|
|
47
|
-
// Accept both `--flag value` and `--flag=value
|
|
48
|
-
// accepts the = form for its own flags, so the probe must not reject it).
|
|
44
|
+
// Accept both `--flag value` and `--flag=value`.
|
|
49
45
|
let a = argv[i];
|
|
50
46
|
let inline;
|
|
51
47
|
if (a.startsWith("--")) {
|
|
@@ -72,14 +68,6 @@ function parseArgs(argv) {
|
|
|
72
68
|
fail("--mentions-only takes no value");
|
|
73
69
|
out.mentionsOnly = true;
|
|
74
70
|
}
|
|
75
|
-
else if (a === "--wake-only") {
|
|
76
|
-
// Internal poller mode: on a quiet cycle, answer via indexed EXISTS and
|
|
77
|
-
// skip exact counts that no caller will see. Kept out of --help because
|
|
78
|
-
// direct one-shot users should retain the full status object.
|
|
79
|
-
if (inline !== undefined)
|
|
80
|
-
fail("--wake-only takes no value");
|
|
81
|
-
out.wakeOnly = true;
|
|
82
|
-
}
|
|
83
71
|
else if (a === "--room") {
|
|
84
72
|
out.room = take(a);
|
|
85
73
|
}
|
|
@@ -102,19 +90,6 @@ function parseArgs(argv) {
|
|
|
102
90
|
else if (a === "--db") {
|
|
103
91
|
out.db = take(a);
|
|
104
92
|
}
|
|
105
|
-
else if (a === "--session") {
|
|
106
|
-
// A process nonce that makes the watch session-aware. In all-rooms mode,
|
|
107
|
-
// rooms the owning session soft-left (a session_presence row with
|
|
108
|
-
// left_at set) are excluded, matching my_mentions, and each room
|
|
109
|
-
// baselines off that session's OWN private cursor where one exists
|
|
110
|
-
// (session_markers is keyed by the same nonce). Without the cursor
|
|
111
|
-
// baseline a private session whose twin read ahead was never woken: the
|
|
112
|
-
// identity marker is the MAX across sessions, so its own unread was
|
|
113
|
-
// invisible here while its catch_up still had messages. Scoped mode uses
|
|
114
|
-
// the same cursor baseline but deliberately remains readable after a
|
|
115
|
-
// soft leave, matching catch_up({room}) and poller.ts.
|
|
116
|
-
out.session = take(a);
|
|
117
|
-
}
|
|
118
93
|
else if (a === "--help" || a === "-h") {
|
|
119
94
|
writeFileSync(1, USAGE);
|
|
120
95
|
process.exit(0);
|
|
@@ -174,77 +149,32 @@ try {
|
|
|
174
149
|
fail("--agent is required when watching all rooms");
|
|
175
150
|
const counts = db
|
|
176
151
|
.transaction(() => {
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
// off this session's OWN private cursor where one exists (COALESCE to
|
|
180
|
-
// the identity marker; parity with the session's catch_up).
|
|
181
|
-
const sess = args.session;
|
|
182
|
-
const smJoin = sess
|
|
183
|
-
? ` LEFT JOIN session_markers sm ON sm.room_id = mb.room_id
|
|
184
|
-
AND sm.agent_id = mb.agent_id AND sm.session_id = ?`
|
|
185
|
-
: "";
|
|
186
|
-
const baseline = sess
|
|
187
|
-
? "COALESCE(sm.last_read_seq, mb.last_read_seq)"
|
|
188
|
-
: "mb.last_read_seq";
|
|
189
|
-
const sessClause = sess
|
|
190
|
-
? ` AND NOT EXISTS (SELECT 1 FROM session_presence sp
|
|
191
|
-
WHERE sp.room_id = mb.room_id AND sp.agent_id = mb.agent_id
|
|
192
|
-
AND sp.session_id = ? AND sp.left_at IS NOT NULL)`
|
|
193
|
-
: "";
|
|
194
|
-
// Rooms count stays IDENTITY-level: it only distinguishes a doomed
|
|
195
|
-
// watch (identity in no room -> fail) from a live one. A session that
|
|
196
|
-
// left all its rooms while a twin keeps the identity present is NOT
|
|
197
|
-
// doomed -- its session-filtered unread below is simply 0 (exit 1, no
|
|
198
|
-
// updates), never an error.
|
|
152
|
+
// Rooms count distinguishes a doomed watch (persona in no room -> fail)
|
|
153
|
+
// from a live one.
|
|
199
154
|
const { n: rooms } = db
|
|
200
155
|
.prepare("SELECT COUNT(*) AS n FROM memberships WHERE agent_id = ? AND left_at IS NULL")
|
|
201
156
|
.get(agent);
|
|
202
157
|
if (rooms === 0)
|
|
203
158
|
return null;
|
|
204
|
-
if (args.wakeOnly) {
|
|
205
|
-
const candidateClause = args.mentionsOnly
|
|
206
|
-
? ` AND (g.mentions IS NOT NULL OR g.reply_to_agent IS NOT NULL)
|
|
207
|
-
AND ${directedAt("g")}`
|
|
208
|
-
: "";
|
|
209
|
-
const hit = db
|
|
210
|
-
.prepare(`SELECT 1 FROM messages g
|
|
211
|
-
JOIN memberships mb ON mb.room_id = g.room_id
|
|
212
|
-
AND mb.agent_id = ? AND mb.left_at IS NULL${smJoin}
|
|
213
|
-
WHERE g.seq > ${baseline} AND g.agent_id != ?
|
|
214
|
-
${candidateClause}${sessClause}
|
|
215
|
-
LIMIT 1`)
|
|
216
|
-
.get(...(sess
|
|
217
|
-
? args.mentionsOnly
|
|
218
|
-
? [agent, sess, agent, agent, agent, sess]
|
|
219
|
-
: [agent, sess, agent, sess]
|
|
220
|
-
: args.mentionsOnly
|
|
221
|
-
? [agent, agent, agent, agent]
|
|
222
|
-
: [agent, agent]));
|
|
223
|
-
if (!hit)
|
|
224
|
-
return { rooms, wakeOnlyQuiet: true };
|
|
225
|
-
}
|
|
226
159
|
const { c: unread } = db
|
|
227
160
|
.prepare(`SELECT COUNT(*) AS c FROM messages g
|
|
228
161
|
JOIN memberships mb ON mb.room_id = g.room_id
|
|
229
|
-
AND mb.agent_id = ? AND mb.left_at IS NULL
|
|
230
|
-
WHERE g.seq >
|
|
231
|
-
.get(
|
|
162
|
+
AND mb.agent_id = ? AND mb.left_at IS NULL
|
|
163
|
+
WHERE g.seq > mb.last_read_seq AND g.agent_id != ?`)
|
|
164
|
+
.get(agent, agent);
|
|
232
165
|
const { c: unreadMentions } = db
|
|
233
166
|
.prepare(`SELECT COUNT(*) AS c FROM messages g
|
|
234
167
|
JOIN memberships mb ON mb.room_id = g.room_id
|
|
235
|
-
AND mb.agent_id = ? AND mb.left_at IS NULL
|
|
236
|
-
WHERE g.seq >
|
|
168
|
+
AND mb.agent_id = ? AND mb.left_at IS NULL
|
|
169
|
+
WHERE g.seq > mb.last_read_seq AND g.agent_id != ?
|
|
237
170
|
AND (g.mentions IS NOT NULL OR g.reply_to_agent IS NOT NULL)
|
|
238
|
-
AND ${directedAt("g")}
|
|
239
|
-
.get(
|
|
240
|
-
? [agent, sess, agent, agent, agent, sess]
|
|
241
|
-
: [agent, agent, agent, agent]));
|
|
171
|
+
AND ${directedAt("g")}`)
|
|
172
|
+
.get(agent, agent, agent, agent);
|
|
242
173
|
// Which rooms fired: same baseline/muting as the counts, read in the
|
|
243
174
|
// same snapshot. Gated behind an actual wake (quiet interval polls,
|
|
244
175
|
// the overwhelmingly common case, must not pay for the GROUP BY).
|
|
245
176
|
// Placeholders in SQL text order: the directedAt pair (SELECT), the
|
|
246
|
-
// membership join,
|
|
247
|
-
// the presence key (if any).
|
|
177
|
+
// membership join, then the author exclusion.
|
|
248
178
|
let roomsWithUpdates = null;
|
|
249
179
|
let roomsWithUpdatesTruncated = false;
|
|
250
180
|
if (args.mentionsOnly ? unreadMentions > 0 : unread > 0) {
|
|
@@ -259,16 +189,14 @@ try {
|
|
|
259
189
|
SUM(CASE WHEN ${directedAt("g")} THEN 1 ELSE 0 END) AS directed
|
|
260
190
|
FROM messages g
|
|
261
191
|
JOIN memberships mb ON mb.room_id = g.room_id
|
|
262
|
-
AND mb.agent_id = ? AND mb.left_at IS NULL
|
|
192
|
+
AND mb.agent_id = ? AND mb.left_at IS NULL
|
|
263
193
|
JOIN rooms r ON r.id = g.room_id
|
|
264
|
-
WHERE g.seq >
|
|
194
|
+
WHERE g.seq > mb.last_read_seq AND g.agent_id != ?${candidateClause}
|
|
265
195
|
GROUP BY g.room_id, r.name
|
|
266
196
|
${mentionsHaving}
|
|
267
197
|
ORDER BY directed DESC, unread DESC, g.room_id ASC
|
|
268
198
|
LIMIT 21`)
|
|
269
|
-
.all(
|
|
270
|
-
? [agent, agent, agent, sess, agent, sess]
|
|
271
|
-
: [agent, agent, agent, agent]));
|
|
199
|
+
.all(agent, agent, agent, agent);
|
|
272
200
|
// The summary drives which rooms callers read next. Silently
|
|
273
201
|
// dropping its tail made firing rooms look quiet; in mentions-only
|
|
274
202
|
// mode, broadcast-only groups did not fire and must not appear.
|
|
@@ -288,18 +216,6 @@ try {
|
|
|
288
216
|
.deferred();
|
|
289
217
|
if (counts === null)
|
|
290
218
|
fail(`agent "${agent}" is not a member of any room`);
|
|
291
|
-
if ("wakeOnlyQuiet" in counts && counts.wakeOnlyQuiet) {
|
|
292
|
-
db.close();
|
|
293
|
-
writeFileSync(1, JSON.stringify({
|
|
294
|
-
agent,
|
|
295
|
-
rooms: counts.rooms,
|
|
296
|
-
...(args.mentionsOnly ? { unread_count_skipped: true } : { unread: 0 }),
|
|
297
|
-
unread_mentions: 0,
|
|
298
|
-
mentions_only: args.mentionsOnly,
|
|
299
|
-
has_updates: false,
|
|
300
|
-
}) + "\n");
|
|
301
|
-
process.exit(1);
|
|
302
|
-
}
|
|
303
219
|
const { rooms, unread, unreadMentions, roomsWithUpdates, roomsWithUpdatesTruncated, } = counts;
|
|
304
220
|
db.close();
|
|
305
221
|
const hasUpdates = args.mentionsOnly ? unreadMentions > 0 : unread > 0;
|
|
@@ -343,32 +259,14 @@ try {
|
|
|
343
259
|
}
|
|
344
260
|
else {
|
|
345
261
|
const m = db
|
|
346
|
-
.prepare(`SELECT
|
|
262
|
+
.prepare(`SELECT mb.last_read_seq AS last_read_seq
|
|
347
263
|
FROM memberships mb
|
|
348
|
-
LEFT JOIN session_markers sm ON sm.room_id = mb.room_id
|
|
349
|
-
AND sm.agent_id = mb.agent_id AND sm.session_id = ?
|
|
350
264
|
WHERE mb.room_id = ? AND mb.agent_id = ?`)
|
|
351
|
-
.get(
|
|
265
|
+
.get(roomId, args.agent);
|
|
352
266
|
if (!m)
|
|
353
267
|
return null;
|
|
354
268
|
baseline = m.last_read_seq;
|
|
355
269
|
}
|
|
356
|
-
if (args.wakeOnly) {
|
|
357
|
-
const authorClause = args.agent ? " AND agent_id != ?" : "";
|
|
358
|
-
const directedClause = args.mentionsOnly
|
|
359
|
-
? ` AND (mentions IS NOT NULL OR reply_to_agent IS NOT NULL)
|
|
360
|
-
AND ${directedAt("messages")}`
|
|
361
|
-
: "";
|
|
362
|
-
const hit = db
|
|
363
|
-
.prepare(`SELECT 1 FROM messages
|
|
364
|
-
WHERE room_id = ? AND seq > ?${authorClause}${directedClause}
|
|
365
|
-
LIMIT 1`)
|
|
366
|
-
.get(roomId, baseline, ...(args.agent ? [args.agent] : []), ...(args.mentionsOnly && args.agent
|
|
367
|
-
? [args.agent, args.agent]
|
|
368
|
-
: []));
|
|
369
|
-
if (!hit)
|
|
370
|
-
return { baseline, wakeOnlyQuiet: true };
|
|
371
|
-
}
|
|
372
270
|
// Exclude the agent's own messages: posting should not make you "have updates".
|
|
373
271
|
const unread = (args.agent
|
|
374
272
|
? db
|
|
@@ -395,19 +293,6 @@ try {
|
|
|
395
293
|
if (snap === null) {
|
|
396
294
|
fail(`agent "${args.agent}" is not a member of room ${roomId}; join first or pass --since`);
|
|
397
295
|
}
|
|
398
|
-
if ("wakeOnlyQuiet" in snap && snap.wakeOnlyQuiet) {
|
|
399
|
-
db.close();
|
|
400
|
-
writeFileSync(1, JSON.stringify({
|
|
401
|
-
room_id: roomId,
|
|
402
|
-
agent: args.agent ?? null,
|
|
403
|
-
baseline_seq: snap.baseline,
|
|
404
|
-
...(args.mentionsOnly ? { unread_count_skipped: true } : { unread: 0 }),
|
|
405
|
-
unread_mentions: 0,
|
|
406
|
-
mentions_only: args.mentionsOnly,
|
|
407
|
-
has_updates: false,
|
|
408
|
-
}) + "\n");
|
|
409
|
-
process.exit(1);
|
|
410
|
-
}
|
|
411
296
|
const { baseline, unread, unreadMentions, latest } = snap;
|
|
412
297
|
db.close();
|
|
413
298
|
const hasUpdates = args.mentionsOnly ? unreadMentions > 0 : unread > 0;
|