multi-agent-collaboration-mcp 0.16.0 → 0.17.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 +6 -0
- package/dist/build-info.json +1 -1
- package/dist/db.js +42 -14
- package/dist/index.js +30 -14
- package/dist/poller.js +32 -4
- package/docs/AI Team Playbook.md +152 -0
- package/docs/Installation.md +189 -0
- package/docs/Multi-Model AI Project Management Operating Guide.md +915 -0
- package/package.json +10 -3
- package/scripts/stamp-build.mjs +14 -8
- package/web/index.html +1 -0
- package/web/server.mjs +2 -1
package/README.md
CHANGED
|
@@ -26,6 +26,12 @@ work.
|
|
|
26
26
|
The [AI team playbook](<docs/AI Team Playbook.md>) gives copyable prompts and
|
|
27
27
|
handoffs for two-, three-, and four-agent teams.
|
|
28
28
|
|
|
29
|
+
For larger or higher-risk teams, paste the
|
|
30
|
+
[multi-model project-management operating guide](<docs/Multi-Model AI Project Management Operating Guide.md>)
|
|
31
|
+
into the PM LLM's standing context. It defines a model-neutral PM role and
|
|
32
|
+
concrete rules for evidence, authority, assignments, ownership, review,
|
|
33
|
+
verification, and handoff.
|
|
34
|
+
|
|
29
35
|
Tell the lead:
|
|
30
36
|
|
|
31
37
|
```text
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.
|
|
1
|
+
{"version":"0.17.0","commit":"e51920e","built_at":"2026-08-02T07:33:05.580Z","artifact_hash":"f1d5be7bb3ff1b80303165848d201ec8938187d9d97a68ab6257d751a1a437b9"}
|
package/dist/db.js
CHANGED
|
@@ -32,11 +32,19 @@ export const MAX_CROSSED_PREVIEW_CHARS = 2_000;
|
|
|
32
32
|
/** Public MCP/store caps used to keep direct callers from bypassing bounded
|
|
33
33
|
* reads with values the tool schemas would reject. */
|
|
34
34
|
const MAX_BULK_RESULT_CHARS = 400_000;
|
|
35
|
-
const
|
|
35
|
+
const MIN_NONADVANCING_RESULT_CHARS = 1_000;
|
|
36
|
+
const MAX_BULK_READ_ROWS = 500;
|
|
36
37
|
const MAX_GET_MESSAGE_CHARS = 400_000;
|
|
38
|
+
const MAX_RECIPIENT_IDS = 100;
|
|
37
39
|
/** Caller-supplied post idempotency keys are opaque, room/author scoped, and
|
|
38
40
|
* intentionally small enough to keep the sparse unique index cheap. */
|
|
39
41
|
export const MAX_CLIENT_MESSAGE_ID_CHARS = 200;
|
|
42
|
+
function requireIntegerInRange(value, name, min, max) {
|
|
43
|
+
if (!Number.isSafeInteger(value) || value < min || value > max) {
|
|
44
|
+
throw new Error(`${name} must be an integer from ${min} to ${max}`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
40
48
|
/** Persona/human id cap. Exported because the server SIZES the canonical id it
|
|
41
49
|
* generates against it before allocating, rather than discovering the overflow
|
|
42
50
|
* as a store assertion after the fact. */
|
|
@@ -1003,6 +1011,7 @@ export class ChatStore {
|
|
|
1003
1011
|
listRooms(limit = 200, afterId = 0) {
|
|
1004
1012
|
const PREVIEW = 300;
|
|
1005
1013
|
const lim = Math.max(1, Math.floor(limit));
|
|
1014
|
+
const cursor = requireIntegerInRange(afterId, "list_rooms after_id", 0, Number.MAX_SAFE_INTEGER);
|
|
1006
1015
|
// Rows and total in ONE deferred snapshot: read as separate statements, a
|
|
1007
1016
|
// concurrent create_room/delete_room BETWEEN them yields an internally
|
|
1008
1017
|
// contradictory page (e.g. total:4 with no next_id, so the keyset pager
|
|
@@ -1024,7 +1033,7 @@ export class ChatStore {
|
|
|
1024
1033
|
(SELECT created_at FROM messages g
|
|
1025
1034
|
WHERE g.room_id = r.id ORDER BY g.seq DESC LIMIT 1) AS last_activity
|
|
1026
1035
|
FROM rooms r WHERE r.id > ? ORDER BY r.id LIMIT ?`)
|
|
1027
|
-
.all(
|
|
1036
|
+
.all(cursor, lim + 1);
|
|
1028
1037
|
const { c: total } = this.db
|
|
1029
1038
|
.prepare("SELECT COUNT(*) AS c FROM rooms")
|
|
1030
1039
|
.get();
|
|
@@ -1399,6 +1408,9 @@ export class ChatStore {
|
|
|
1399
1408
|
retiredIds(ids) {
|
|
1400
1409
|
if (ids.length === 0)
|
|
1401
1410
|
return new Set();
|
|
1411
|
+
if (ids.length > MAX_RECIPIENT_IDS) {
|
|
1412
|
+
throw new Error(`retired_ids accepts at most ${MAX_RECIPIENT_IDS} ids`);
|
|
1413
|
+
}
|
|
1402
1414
|
const placeholders = ids.map(() => "?").join(",");
|
|
1403
1415
|
const rows = this.db
|
|
1404
1416
|
.prepare(`SELECT id FROM agents
|
|
@@ -1687,9 +1699,9 @@ export class ChatStore {
|
|
|
1687
1699
|
// rejoins (INSERT OR IGNORE keeps the row); rows come back in join order.
|
|
1688
1700
|
let keyset = "";
|
|
1689
1701
|
const rowParams = { ...base, lim: lim + 1 };
|
|
1690
|
-
if (after !== undefined
|
|
1702
|
+
if (after !== undefined) {
|
|
1691
1703
|
keyset = ` AND m.rowid > @after`;
|
|
1692
|
-
rowParams.after =
|
|
1704
|
+
rowParams.after = requireIntegerInRange(after, "list_agents after", 0, Number.MAX_SAFE_INTEGER);
|
|
1693
1705
|
}
|
|
1694
1706
|
// Rows and count in ONE deferred snapshot so total cannot disagree with the
|
|
1695
1707
|
// page (parity with listRooms). Fetch one MORE than asked to detect a page.
|
|
@@ -2338,6 +2350,9 @@ export class ChatStore {
|
|
|
2338
2350
|
recipientStatus(roomId, ids, activeWithinMinutes) {
|
|
2339
2351
|
if (ids.length === 0)
|
|
2340
2352
|
return [];
|
|
2353
|
+
if (ids.length > MAX_RECIPIENT_IDS) {
|
|
2354
|
+
throw new Error(`recipient_status accepts at most ${MAX_RECIPIENT_IDS} ids`);
|
|
2355
|
+
}
|
|
2341
2356
|
const placeholders = ids.map(() => "?").join(",");
|
|
2342
2357
|
// marker_behind baseline. Read alongside the rows without a transaction:
|
|
2343
2358
|
// this is a liveness heuristic, not an invariant, and the method already
|
|
@@ -2498,13 +2513,14 @@ export class ChatStore {
|
|
|
2498
2513
|
* delivered over budget. Oversized bodies arrive truncated with markers;
|
|
2499
2514
|
* page them via get_message.
|
|
2500
2515
|
*/
|
|
2501
|
-
getThread(roomId, seq, maxDepth = 3, previewChars) {
|
|
2516
|
+
getThread(roomId, seq, maxDepth = 3, previewChars, maxBytes = DEFAULT_MAX_BYTES) {
|
|
2517
|
+
maxBytes = requireIntegerInRange(maxBytes, "get_thread max_bytes", MIN_NONADVANCING_RESULT_CHARS, DEFAULT_MAX_BYTES);
|
|
2502
2518
|
const tx = this.db.transaction(() => {
|
|
2503
|
-
const focalRow = this.getRawMessage(roomId, seq);
|
|
2519
|
+
const focalRow = this.getRawMessage(roomId, seq, maxBytes);
|
|
2504
2520
|
if (!focalRow)
|
|
2505
2521
|
return undefined;
|
|
2506
2522
|
const mapPlain = (r, pc) => this.rowToMessage(r, pc);
|
|
2507
|
-
const budget =
|
|
2523
|
+
const budget = maxBytes - THREAD_ENVELOPE;
|
|
2508
2524
|
// Reserve the parent's slot before spending on the focal message: a stub
|
|
2509
2525
|
// allowance when a parent exists, 4 chars of literal null otherwise. The
|
|
2510
2526
|
// trailing 2 covers an empty replies array.
|
|
@@ -2514,7 +2530,7 @@ export class ChatStore {
|
|
|
2514
2530
|
let remaining = budget - JSON.stringify(message).length;
|
|
2515
2531
|
let parent = null;
|
|
2516
2532
|
if (parentSeq !== null) {
|
|
2517
|
-
const parentRow = this.getRawMessage(roomId, parentSeq);
|
|
2533
|
+
const parentRow = this.getRawMessage(roomId, parentSeq, maxBytes);
|
|
2518
2534
|
if (parentRow) {
|
|
2519
2535
|
// Leave a stub allowance (plus array brackets) for the replies when
|
|
2520
2536
|
// more than that remains; otherwise the parent gets a stub itself.
|
|
@@ -2544,7 +2560,7 @@ export class ChatStore {
|
|
|
2544
2560
|
ORDER BY path
|
|
2545
2561
|
LIMIT @lim
|
|
2546
2562
|
)
|
|
2547
|
-
SELECT ${messageCols(
|
|
2563
|
+
SELECT ${messageCols(maxBytes)}, d.depth AS depth
|
|
2548
2564
|
FROM descendants d
|
|
2549
2565
|
CROSS JOIN messages g
|
|
2550
2566
|
LEFT JOIN messages p ON p.room_id = g.room_id AND p.seq = g.reply_to_seq
|
|
@@ -2705,7 +2721,7 @@ export class ChatStore {
|
|
|
2705
2721
|
if (!Number.isFinite(limit)) {
|
|
2706
2722
|
throw new Error("catch_up limit must be finite");
|
|
2707
2723
|
}
|
|
2708
|
-
const pageLimit = Math.min(
|
|
2724
|
+
const pageLimit = Math.min(MAX_BULK_READ_ROWS, Math.max(1, Math.floor(limit)));
|
|
2709
2725
|
// Advancing path: read the cursor, fetch, and advance inside one IMMEDIATE
|
|
2710
2726
|
// transaction. A deferred WAL transaction could let another writer commit
|
|
2711
2727
|
// after this read snapshot, then fail the cursor update with
|
|
@@ -2856,6 +2872,9 @@ export class ChatStore {
|
|
|
2856
2872
|
* snapshot.
|
|
2857
2873
|
*/
|
|
2858
2874
|
myMentions(agentId, limit, previewChars, maxBytes = DEFAULT_MAX_BYTES, afterId = 0) {
|
|
2875
|
+
limit = requireIntegerInRange(limit, "my_mentions limit", 1, MAX_BULK_READ_ROWS);
|
|
2876
|
+
maxBytes = requireIntegerInRange(maxBytes, "my_mentions max_bytes", MIN_CATCH_UP_RESULT_BUDGET, MAX_BULK_RESULT_CHARS);
|
|
2877
|
+
afterId = requireIntegerInRange(afterId, "my_mentions after_id", 0, Number.MAX_SAFE_INTEGER);
|
|
2859
2878
|
const tx = this.db.transaction(() => {
|
|
2860
2879
|
// Fetch ONE more than `limit` so a page cut by the ROW limit (not the
|
|
2861
2880
|
// byte budget) is detectable. Without the probe, my_mentions was the only
|
|
@@ -2939,6 +2958,11 @@ export class ChatStore {
|
|
|
2939
2958
|
* `limit`; else older than beforeSeq.
|
|
2940
2959
|
*/
|
|
2941
2960
|
readHistory(roomId, limit, beforeSeq, previewChars, maxBytes = DEFAULT_MAX_BYTES) {
|
|
2961
|
+
limit = requireIntegerInRange(limit, "read_history limit", 1, MAX_BULK_READ_ROWS);
|
|
2962
|
+
maxBytes = requireIntegerInRange(maxBytes, "read_history max_bytes", MIN_CATCH_UP_RESULT_BUDGET, MAX_BULK_RESULT_CHARS);
|
|
2963
|
+
if (beforeSeq !== undefined) {
|
|
2964
|
+
requireIntegerInRange(beforeSeq, "read_history before_seq", 1, Number.MAX_SAFE_INTEGER);
|
|
2965
|
+
}
|
|
2942
2966
|
const conds = ["g.room_id = ?"];
|
|
2943
2967
|
const params = [roomId];
|
|
2944
2968
|
if (beforeSeq !== undefined) {
|
|
@@ -2978,6 +3002,9 @@ export class ChatStore {
|
|
|
2978
3002
|
* Returns the previous and new marker plus the room's latest seq.
|
|
2979
3003
|
*/
|
|
2980
3004
|
markRead(roomId, agentId, seq) {
|
|
3005
|
+
if (seq !== undefined) {
|
|
3006
|
+
requireIntegerInRange(seq, "mark_read seq", 0, Number.MAX_SAFE_INTEGER);
|
|
3007
|
+
}
|
|
2981
3008
|
const tx = this.db.transaction(() => {
|
|
2982
3009
|
this.requireLive(agentId);
|
|
2983
3010
|
// Advancing a marker requires presence and reports the prior cursor.
|
|
@@ -3006,21 +3033,22 @@ export class ChatStore {
|
|
|
3006
3033
|
* relevance sort can avoid; callers wanting exactly-once delivery should page
|
|
3007
3034
|
* in one burst.
|
|
3008
3035
|
*/
|
|
3009
|
-
searchMessages(roomId, query, limit, offset = 0) {
|
|
3036
|
+
searchMessages(roomId, query, limit, offset = 0, maxBytes = DEFAULT_MAX_BYTES) {
|
|
3037
|
+
maxBytes = requireIntegerInRange(maxBytes, "search_messages max_bytes", MIN_NONADVANCING_RESULT_CHARS, DEFAULT_MAX_BYTES);
|
|
3010
3038
|
const off = Math.max(0, Math.floor(offset));
|
|
3011
3039
|
// Fetch one MORE than asked: a full `limit` page does not by itself prove
|
|
3012
3040
|
// more exist, so the extra row is the definitive "there is a next page"
|
|
3013
3041
|
// probe (a page of exactly `limit` matches used to emit a false
|
|
3014
3042
|
// next_offset that returned nothing).
|
|
3015
|
-
const { rows, exhausted } = this.fetchBounded(this.db.prepare(`SELECT ${messageCols(
|
|
3043
|
+
const { rows, exhausted } = this.fetchBounded(this.db.prepare(`SELECT ${messageCols(maxBytes)}
|
|
3016
3044
|
FROM messages_fts f
|
|
3017
3045
|
JOIN messages g ON g.id = f.rowid
|
|
3018
3046
|
LEFT JOIN messages p ON p.room_id = g.room_id AND p.seq = g.reply_to_seq
|
|
3019
3047
|
WHERE f.body MATCH ? AND g.room_id = ?
|
|
3020
|
-
ORDER BY rank, g.id LIMIT ? OFFSET ?`), [query, roomId, limit + 1, off],
|
|
3048
|
+
ORDER BY rank, g.id LIMIT ? OFFSET ?`), [query, roomId, limit + 1, off], maxBytes);
|
|
3021
3049
|
const hasExtra = rows.length > limit;
|
|
3022
3050
|
const page = hasExtra ? rows.slice(0, limit) : rows;
|
|
3023
|
-
const { messages, byteLimited } = this.boundByBytes(page, undefined,
|
|
3051
|
+
const { messages, byteLimited } = this.boundByBytes(page, undefined, maxBytes - SEARCH_ENVELOPE, (r, pc) => this.rowToMessage(r, pc));
|
|
3024
3052
|
// More remain if the byte bound cut the page, a genuine extra match exists
|
|
3025
3053
|
// beyond `limit`, OR fetchBounded stopped on the raw byte budget with
|
|
3026
3054
|
// matches unfetched (!exhausted) -- the last case is invisible to
|
package/dist/index.js
CHANGED
|
@@ -120,7 +120,7 @@ SIZE AND PAGING
|
|
|
120
120
|
|
|
121
121
|
POSTING
|
|
122
122
|
- crossed counts ALL unread from others past your marker at post time (old backlog included, not only mid-composition arrivals); crossed_directed says how many are aimed at you; crossed_range gives the seq span. If crossed > 0, catch_up before acting on replies. crossed_preview_chars opts into bounded previews of the crossed messages in the same response.
|
|
123
|
-
-
|
|
123
|
+
- Use if_last_read_seq for a directive, assignment, or verdict whose validity depends on no peer message arriving after your last catch_up. It is a conditional post -- rejected (posted:false) if ANYTHING from others landed past your token, with bounded crossed previews returned; call catch_up for the complete delta before retrying. If pruning removed evidence after the token, it rejects conservatively with rejected:evidence_pruned and no invented previews. A token ahead of the target room's effective cursor is invalid and fails before posting. client_message_id makes an exact lost-response retry return the original seq instead of inserting twice; its guarantee lasts while that message is retained. Repeat the same explicit room or expected_room on retry so active-room drift cannot create a post in another room; a deduplicated response does not replay the original crossed/recipient snapshot, so catch_up for current state. room: posts to a named joined room without switching the active room; expected_room asserts which room is active. Never use the CAS on routine traffic: crossing is normal, the CAS is for posts whose validity depends on having read everything.
|
|
124
124
|
- recipients reports factual room-local state: status, idle_seconds, last_read_seq, marker_behind. A new unread tag normally adds one to marker_behind. delivery_warnings is definitive for never-joined/left/retired recipients; a long-idle warning is emitted only for pre-existing lag and states observed facts, never a responsiveness prediction.
|
|
125
125
|
- status/idle_seconds/last_seen measure LISTENER recency: an MCP call from the bound runtime, or the two-minute heartbeat of a watcher it armed. active therefore means a runtime is reachable, not that the model is reading, reasoning, or able to wake, and an armed seat stays active no matter how long its model has been silent. The absence of a long-idle warning is not evidence anyone is listening; watching:true (an open blocking call) is the stronger claim.
|
|
126
126
|
- supersedes_seq corrects YOUR OWN earlier message; readers see superseded_by on it. reply_to_seq threads; the log stays flat and globally ordered.
|
|
@@ -143,7 +143,7 @@ IDENTITY
|
|
|
143
143
|
- Human web participants use the reserved 'human-' prefix with a numeric ordinal (human-alex-1). The number prevents two independent browsers from colliding on one name; it is not ownership and not authentication.
|
|
144
144
|
|
|
145
145
|
BACKGROUND POLLER
|
|
146
|
-
-
|
|
146
|
+
- Launch the command returned by join_room or wait_for_messages as the tracked BACKGROUND task. The tracked task must remain attached to the poller's stdout and exit; if a wrapper is required, it must stay alive until the poller exits. Before arming, call catch_up until remaining is 0 or stops falling. The watcher probes before its first sleep, so unread traffic makes it exit immediately. One Node process holds one SQLite connection and runs one indexed LIMIT 1 probe after each sleep; it launches no children. Generated commands exit 0 for either a hit or quiet deadline: parse stdout has_updates true/false. Direct CLI calls without --ok-on-timeout retain exit 124 on timeout. Exit 2 is an error, a live-PID watcher lock, retired_identity, left_room, left_all_rooms, or no_room_memberships; inspect stderr before re-arming. Options: --interval <sec> (minimum/default 5), --timeout <sec> (default 1200, finite), --ok-on-timeout, --mentions-only, --room <id|name>, --owner-pid <pid>. Your own posts never wake it.
|
|
147
147
|
- The poller is an OS-level detector: its exit does NOT by itself schedule your next turn. This MCP does not replace an exited watcher; watching resumes only when a client runs a new command. Whether you are actually woken depends on your harness's background-task contract; do not report "watcher active" as evidence you will see a message.
|
|
148
148
|
|
|
149
149
|
RETENTION
|
|
@@ -1077,8 +1077,11 @@ server.registerTool("join_room", {
|
|
|
1077
1077
|
// persona (see the server instructions).
|
|
1078
1078
|
poller_cmd: pollerCmd(agentId),
|
|
1079
1079
|
next: "call catch_up until it returns `remaining: 0` or the value stops " +
|
|
1080
|
-
"falling; then
|
|
1081
|
-
"work. The
|
|
1080
|
+
"falling; then launch poller_cmd as the client's tracked background " +
|
|
1081
|
+
"task before starting work. The tracked task must remain attached to " +
|
|
1082
|
+
"the poller's stdout and exit; any wrapper must stay alive until the " +
|
|
1083
|
+
"poller exits. The watcher checks " +
|
|
1084
|
+
"immediately, so unread traffic makes it " +
|
|
1082
1085
|
"exit immediately. This MCP server returns the command but cannot " +
|
|
1083
1086
|
"launch it.",
|
|
1084
1087
|
presence_note: "active = recent MCP or poller contact, or a live blocking wait. " +
|
|
@@ -1292,8 +1295,9 @@ server.registerTool("post_message", {
|
|
|
1292
1295
|
"insert. `posted:true` means committed to SQLite only, not that a " +
|
|
1293
1296
|
"recipient was woken, acknowledged, or began processing it. A " +
|
|
1294
1297
|
"deduplicated retry returns the original seq/key only; catch " +
|
|
1295
|
-
"up for current state. For a " +
|
|
1296
|
-
"
|
|
1298
|
+
"up for current state. For a directive, assignment, or verdict that " +
|
|
1299
|
+
"would be invalid if peer traffic arrived after your last catch_up, use " +
|
|
1300
|
+
"`if_last_read_seq` + `expected_room`; use " +
|
|
1297
1301
|
"`client_message_id` to deduplicate an exact lost-response retry. " +
|
|
1298
1302
|
"`crossed_preview_chars` max is 2000. `priority:true` survives explicit " +
|
|
1299
1303
|
"priority-only backlog triage; `supersedes_seq` corrects your own post.",
|
|
@@ -1379,9 +1383,11 @@ server.registerTool("post_message", {
|
|
|
1379
1383
|
.int()
|
|
1380
1384
|
.nonnegative()
|
|
1381
1385
|
.optional()
|
|
1382
|
-
.describe("Conditional post (CAS) for
|
|
1383
|
-
"
|
|
1384
|
-
"catch_up
|
|
1386
|
+
.describe("Conditional post (CAS) for a directive, assignment, or verdict " +
|
|
1387
|
+
"that would be invalid if peer traffic arrived after your last " +
|
|
1388
|
+
"catch_up: reject if ANY message from others carries a seq above " +
|
|
1389
|
+
"this (use your last catch_up's new_last_read_seq). A token ahead " +
|
|
1390
|
+
"of this room's " +
|
|
1385
1391
|
"effective read cursor is invalid and rejected before posting. " +
|
|
1386
1392
|
"A stale rejection returns posted:false with bounded crossed " +
|
|
1387
1393
|
"previews; call catch_up for the full delta, then re-send the " +
|
|
@@ -1709,6 +1715,8 @@ server.registerTool("catch_up", {
|
|
|
1709
1715
|
first = advancingRead(waitSeconds === 0);
|
|
1710
1716
|
}
|
|
1711
1717
|
catch (e) {
|
|
1718
|
+
if (e instanceof PersonaLostError)
|
|
1719
|
+
throw e;
|
|
1712
1720
|
if (!store.getRoom(roomId))
|
|
1713
1721
|
return roomDeletedResult();
|
|
1714
1722
|
throw e;
|
|
@@ -1771,6 +1779,8 @@ server.registerTool("catch_up", {
|
|
|
1771
1779
|
hit = advancingRead(false);
|
|
1772
1780
|
}
|
|
1773
1781
|
catch (e) {
|
|
1782
|
+
if (e instanceof PersonaLostError)
|
|
1783
|
+
throw e;
|
|
1774
1784
|
if (!store.getRoom(roomId))
|
|
1775
1785
|
return roomDeletedResult(true);
|
|
1776
1786
|
throw e;
|
|
@@ -1790,6 +1800,8 @@ server.registerTool("catch_up", {
|
|
|
1790
1800
|
last = advancingRead(true);
|
|
1791
1801
|
}
|
|
1792
1802
|
catch (e) {
|
|
1803
|
+
if (e instanceof PersonaLostError)
|
|
1804
|
+
throw e;
|
|
1793
1805
|
if (!store.getRoom(roomId))
|
|
1794
1806
|
return roomDeletedResult(true);
|
|
1795
1807
|
throw e;
|
|
@@ -2026,7 +2038,9 @@ server.registerTool("wait_for_messages", {
|
|
|
2026
2038
|
return ok({
|
|
2027
2039
|
command,
|
|
2028
2040
|
run_as: "background process (do not wait for it inline)",
|
|
2029
|
-
how_to: "
|
|
2041
|
+
how_to: "Launch `command` as your client's tracked background task. The " +
|
|
2042
|
+
"tracked task must remain attached to the poller's stdout and exit; " +
|
|
2043
|
+
"any wrapper must stay alive until the poller exits. On exit 0, parse stdout: " +
|
|
2030
2044
|
"has_updates:true names the room to catch_up; has_updates:false is a " +
|
|
2031
2045
|
"normal quiet deadline. Exit 2 is an error, duplicate watcher, or a " +
|
|
2032
2046
|
"terminal room/binding state; inspect stderr before re-arming. The " +
|
|
@@ -2209,11 +2223,12 @@ server.registerTool("get_thread", {
|
|
|
2209
2223
|
touchSession();
|
|
2210
2224
|
const { agentId, roomId, roomName } = resolveJoinedRoom(room);
|
|
2211
2225
|
touchCapturedRoom(roomId, agentId);
|
|
2212
|
-
const
|
|
2226
|
+
const lost = lossDisclosure(agentId);
|
|
2227
|
+
const thread = store.getThread(roomId, seq, max_depth ?? 3, preview_chars, DEFAULT_MAX_BYTES - disclosureReserve(lost));
|
|
2213
2228
|
if (!thread) {
|
|
2214
2229
|
return fail(`no message ${seq} in room "${roomName}"`);
|
|
2215
2230
|
}
|
|
2216
|
-
return ok({ ...
|
|
2231
|
+
return ok({ ...lost, ...thread });
|
|
2217
2232
|
}
|
|
2218
2233
|
catch (e) {
|
|
2219
2234
|
return failFrom(e);
|
|
@@ -2269,9 +2284,10 @@ server.registerTool("search_messages", {
|
|
|
2269
2284
|
try {
|
|
2270
2285
|
touchSession();
|
|
2271
2286
|
const { agentId, roomId } = requireActive();
|
|
2287
|
+
const lost = lossDisclosure(agentId);
|
|
2272
2288
|
return ok({
|
|
2273
|
-
...
|
|
2274
|
-
...store.searchMessages(roomId, query, limit ?? 20, offset ?? 0),
|
|
2289
|
+
...lost,
|
|
2290
|
+
...store.searchMessages(roomId, query, limit ?? 20, offset ?? 0, DEFAULT_MAX_BYTES - disclosureReserve(lost)),
|
|
2275
2291
|
});
|
|
2276
2292
|
}
|
|
2277
2293
|
catch (e) {
|
package/dist/poller.js
CHANGED
|
@@ -134,6 +134,9 @@ function dbPath(override) {
|
|
|
134
134
|
return join(homedir(), ".agent-chat-mcp", "chat.db");
|
|
135
135
|
}
|
|
136
136
|
function processIsAlive(pid) {
|
|
137
|
+
// Existence only, not process identity. A recycled PID is indistinguishable
|
|
138
|
+
// here from the process that originally owned it; closing that portability
|
|
139
|
+
// gap requires a process-instance protocol rather than another PID check.
|
|
137
140
|
if (!Number.isSafeInteger(pid) || pid < 1)
|
|
138
141
|
return false;
|
|
139
142
|
try {
|
|
@@ -144,6 +147,8 @@ function processIsAlive(pid) {
|
|
|
144
147
|
return error.code !== "ESRCH";
|
|
145
148
|
}
|
|
146
149
|
}
|
|
150
|
+
const LOCK_METADATA_READ_ATTEMPTS = 5;
|
|
151
|
+
const LOCK_METADATA_RETRY_DELAY_MS = 25;
|
|
147
152
|
function acquireLock(path, token) {
|
|
148
153
|
try {
|
|
149
154
|
const fd = openSync(path, "wx", 0o600);
|
|
@@ -164,11 +169,34 @@ function acquireLock(path, token) {
|
|
|
164
169
|
catch (error) {
|
|
165
170
|
if (error.code !== "EEXIST")
|
|
166
171
|
throw error;
|
|
167
|
-
let owner
|
|
168
|
-
|
|
169
|
-
|
|
172
|
+
let owner;
|
|
173
|
+
const retrySignal = new Int32Array(new SharedArrayBuffer(4));
|
|
174
|
+
for (let attempt = 0; attempt < LOCK_METADATA_READ_ATTEMPTS; attempt++) {
|
|
175
|
+
try {
|
|
176
|
+
const metadata = JSON.parse(readFileSync(path, "utf8"));
|
|
177
|
+
if (typeof metadata.pid === "number" &&
|
|
178
|
+
Number.isSafeInteger(metadata.pid) &&
|
|
179
|
+
metadata.pid > 0 &&
|
|
180
|
+
typeof metadata.token === "string" &&
|
|
181
|
+
metadata.token.length > 0) {
|
|
182
|
+
owner = metadata.pid;
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { }
|
|
187
|
+
if (attempt + 1 < LOCK_METADATA_READ_ATTEMPTS) {
|
|
188
|
+
Atomics.wait(retrySignal, 0, 0, LOCK_METADATA_RETRY_DELAY_MS);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (owner === undefined) {
|
|
192
|
+
// The exclusive create publishes the pathname before the tiny metadata
|
|
193
|
+
// write completes. Give that bounded publication window time to settle,
|
|
194
|
+
// then preserve exclusion without falsely calling an invalid lock stale.
|
|
195
|
+
argumentError(`watcher lock metadata could not be read or validated; the lock may be ` +
|
|
196
|
+
`initializing or stale, so acquisition is refused (lock: ${path}). ` +
|
|
197
|
+
`Retry without removing it; inspect this exact lock before any manual ` +
|
|
198
|
+
`recovery`);
|
|
170
199
|
}
|
|
171
|
-
catch { }
|
|
172
200
|
if (processIsAlive(owner)) {
|
|
173
201
|
argumentError(`watcher lock references live pid ${owner}; refusing a duplicate (lock: ${path})`);
|
|
174
202
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# AI Team Playbook
|
|
2
|
+
|
|
3
|
+
Use this guide after installing Agent Chat. It is for people who want several
|
|
4
|
+
coding agents to produce one project without becoming the message carrier.
|
|
5
|
+
Start with two seats. Add a seat only when you can name its job in one sentence.
|
|
6
|
+
Agent identity is self-reported, so use only clients you trust.
|
|
7
|
+
|
|
8
|
+
## Choose the smallest team
|
|
9
|
+
|
|
10
|
+
| Seats | Roles | Use when |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| Two | Project lead and builder | Recommended default: one owns the result and one implements it. |
|
|
13
|
+
| Three | Add an architect or reviewer | A named design question, subsystem, or independent review needs its own attention. |
|
|
14
|
+
| Four | Add an oversight reviewer | A long, high-risk, or policy-bound project needs an independent scope and progress check. |
|
|
15
|
+
|
|
16
|
+
Roles are jobs, not headcount; one agent can hold several. Every added seat must
|
|
17
|
+
catch up, keep its watcher armed, avoid conflicting edits, and report back. Add
|
|
18
|
+
a security, database, performance, or test specialist only for a named problem,
|
|
19
|
+
then remove that seat when the problem is closed.
|
|
20
|
+
|
|
21
|
+
The project lead is the PM and final tie-breaker. Use your strongest overall
|
|
22
|
+
reasoning model in that seat: it must understand both architecture and
|
|
23
|
+
implementation reports, reject drift, and decide. That is not automatically the
|
|
24
|
+
best code writer. A narrow technical problem may still need your strongest
|
|
25
|
+
specialist as architect.
|
|
26
|
+
|
|
27
|
+
## Give each role a boundary
|
|
28
|
+
|
|
29
|
+
| Role | Owns | Does not own |
|
|
30
|
+
| --- | --- | --- |
|
|
31
|
+
| Project lead | Goal, scope, assignments, decisions, review, and completion | Unapproved changes to the human's goal or policy |
|
|
32
|
+
| Builder | Code, tests, file or task claims, and concrete progress reports | Project-wide scope changes |
|
|
33
|
+
| Architect or reviewer | Minimal design, alternatives, risks, and independent review | Editing the work while claiming to review it independently |
|
|
34
|
+
| Oversight reviewer | Plan compliance, policy, elapsed time, stalled work, and unsupported reports | Code correctness or technical review |
|
|
35
|
+
|
|
36
|
+
The oversight reviewer is the optional global manager. It reports concerns to
|
|
37
|
+
the PM and human, not competing orders to the builder. It can verify observable
|
|
38
|
+
state, but a silent agent may be working, throttled, disconnected, or gone.
|
|
39
|
+
Silence alone proves none of those.
|
|
40
|
+
|
|
41
|
+
## Start the room
|
|
42
|
+
|
|
43
|
+
Replace `<room>` and the bracketed project details, then give each prompt to the
|
|
44
|
+
matching agent.
|
|
45
|
+
|
|
46
|
+
Open the local browser page beside the agent terminals. You can watch without
|
|
47
|
+
joining; enter a name only when you want to post, answer, or break a tie.
|
|
48
|
+
|
|
49
|
+
Project lead:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
Use agent-chat. Create or join room "<room>" as project lead. Catch up, then post
|
|
53
|
+
the goal, required constraints, non-goals, and definition of done. Assign
|
|
54
|
+
bounded work, check claims and progress, resolve disagreements, reject
|
|
55
|
+
unapproved scope changes, and keep rearming the one-shot watcher. Do not declare
|
|
56
|
+
completion until reports and validation evidence agree.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Builder:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
Use agent-chat. Join room "<room>" as builder and catch up before acting. Claim
|
|
63
|
+
each file or task before editing. Implement only the assigned scope, test it,
|
|
64
|
+
report blockers, changed files, validation results, and remaining risks,
|
|
65
|
+
release claims when done, and keep rearming the one-shot watcher.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Architect or reviewer:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
Use agent-chat. Join room "<room>" as architect and reviewer. Investigate the
|
|
72
|
+
assigned design or review question. Compare the smallest viable alternatives,
|
|
73
|
+
their costs, failure cases, and validation. Do not edit while acting as an
|
|
74
|
+
independent reviewer. Post your recommendation and evidence, then keep
|
|
75
|
+
rearming the one-shot watcher.
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Oversight reviewer:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
Use agent-chat. Join room "<room>" as oversight reviewer. Do not read or write code.
|
|
82
|
+
Compare current work with the original goal, constraints, and policy. Check
|
|
83
|
+
list_agents and list_claims, inspect the background watcher process directly,
|
|
84
|
+
and check git status, git log, timestamps, and validation evidence.
|
|
85
|
+
watching:false does not mean a background watcher is off. Flag stalled work,
|
|
86
|
+
stale claims, unsupported status reports, and scope drift. State when a
|
|
87
|
+
conclusion would require reading code. Escalate to the PM and human, not
|
|
88
|
+
directly to the builder, and keep rearming the one-shot watcher.
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Run one visible work loop
|
|
92
|
+
|
|
93
|
+
1. **Assign.** The PM posts one bounded task with its owner, expected result,
|
|
94
|
+
relevant files, constraints, and required validation.
|
|
95
|
+
2. **Claim.** Before changing shared files or tasks, the assigned agent uses an
|
|
96
|
+
Agent Chat claim. Claims are advisory and expire, so renew long work and
|
|
97
|
+
release finished work.
|
|
98
|
+
3. **Build.** The agent works inside the assignment. It reports a blocker, a
|
|
99
|
+
required scope change, or a completed checkpoint instead of posting empty
|
|
100
|
+
status updates.
|
|
101
|
+
4. **Report.** A useful completion report names changed files, commands run,
|
|
102
|
+
results, remaining risks, and anything not checked. "Done" alone is not
|
|
103
|
+
evidence.
|
|
104
|
+
5. **Review.** The reviewer catches up, reads current evidence and code when
|
|
105
|
+
required, and posts findings. The author does not supply the independent
|
|
106
|
+
review of its own work.
|
|
107
|
+
6. **Decide.** The PM records accepted findings, rejected findings with reasons,
|
|
108
|
+
tie-breaks, and any human-approved scope change. Keep disputes in the room;
|
|
109
|
+
correct your own earlier post with `supersedes_seq` instead of rewriting
|
|
110
|
+
history.
|
|
111
|
+
7. **Close.** The PM checks that required validation passed, claims were
|
|
112
|
+
released, unresolved issues are named, and the result still matches the
|
|
113
|
+
original goal.
|
|
114
|
+
|
|
115
|
+
Before a consequential decision, catch up until no unread peer message remains.
|
|
116
|
+
If new traffic crossed a post, read it before treating the post as settled.
|
|
117
|
+
|
|
118
|
+
## Keep agents reachable
|
|
119
|
+
|
|
120
|
+
One watcher covers one wait. It can exit on traffic, a quiet deadline, an error,
|
|
121
|
+
or a client restart; the agent must start a current command again. Watcher exit
|
|
122
|
+
does not make every client start a new model turn.
|
|
123
|
+
|
|
124
|
+
The watcher cannot interrupt a model already reasoning. Break long work at
|
|
125
|
+
natural checkpoints, catch up, then continue.
|
|
126
|
+
|
|
127
|
+
`list_agents` reports self-declared identity and recent tool, wait, or watcher
|
|
128
|
+
contact, not proof of identity, model attention, or wake. Its `watching:false`
|
|
129
|
+
does not mean a background watcher is off; background watchers never set it.
|
|
130
|
+
Use the client's background-process view or the OS process table to check the
|
|
131
|
+
watcher itself.
|
|
132
|
+
`list_claims` reports advisory ownership, not proof that work is progressing.
|
|
133
|
+
Compare both with Git state, room reports, and objective timestamps.
|
|
134
|
+
|
|
135
|
+
If the watcher reports `owner MCP process ... has ended`, reconnect and generate
|
|
136
|
+
a current watcher command. If it reports `retired_identity`, that identity is
|
|
137
|
+
gone permanently; a fresh identity must join and receive a new assignment.
|
|
138
|
+
|
|
139
|
+
## Know when to intervene or shrink
|
|
140
|
+
|
|
141
|
+
The PM or human should intervene when:
|
|
142
|
+
|
|
143
|
+
- silence lasts beyond the agreed checkpoint or watcher deadline;
|
|
144
|
+
- two agents disagree and no named tie-breaker owns the decision;
|
|
145
|
+
- an agent changes scope without approval;
|
|
146
|
+
- claims stay held without matching progress;
|
|
147
|
+
- a completion report lacks commands, results, or remaining risks; or
|
|
148
|
+
- the PM spends more effort reconciling agents than deciding the project.
|
|
149
|
+
|
|
150
|
+
When coordination traffic outweighs useful work, remove a seat or combine
|
|
151
|
+
roles. More agents are useful only while their independent work or criticism
|
|
152
|
+
costs less than managing them.
|