agent-comm-hub 0.7.0 → 0.8.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 +1 -1
- package/agents/SKILL.md +5 -2
- package/assets/admin.html +526 -16
- package/lib/cli.js +207 -24
- package/lib/index.js +203 -22
- package/package.json +2 -2
package/lib/cli.js
CHANGED
|
@@ -424,6 +424,7 @@ import { randomUUID } from "node:crypto";
|
|
|
424
424
|
var KINDS = ["chat", "task", "notice", "ack"];
|
|
425
425
|
var PEER_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
426
426
|
var BROADCAST = "all";
|
|
427
|
+
var TERMINAL_TASK_STATUSES = ["rejected", "done", "failed"];
|
|
427
428
|
function decodeContent(kind, content) {
|
|
428
429
|
if (kind !== "task" && kind !== "ack") return content;
|
|
429
430
|
try {
|
|
@@ -436,6 +437,7 @@ function decodeContent(kind, content) {
|
|
|
436
437
|
|
|
437
438
|
// src/hub.ts
|
|
438
439
|
var MAX_PROFILES = 512;
|
|
440
|
+
var MAX_TASKS = 1024;
|
|
439
441
|
var AgentHub = class {
|
|
440
442
|
constructor(options) {
|
|
441
443
|
this.options = options;
|
|
@@ -454,6 +456,8 @@ var AgentHub = class {
|
|
|
454
456
|
profiles = /* @__PURE__ */ new Map();
|
|
455
457
|
/** Named channels (group channel), keyed by group id. */
|
|
456
458
|
groups = /* @__PURE__ */ new Map();
|
|
459
|
+
/** Task ledger: task message id → record (status + ack timeline). */
|
|
460
|
+
taskLedger = /* @__PURE__ */ new Map();
|
|
457
461
|
gcTimer;
|
|
458
462
|
/** Stop the idle GC (call when the hub shuts down). */
|
|
459
463
|
dispose() {
|
|
@@ -624,15 +628,92 @@ var AgentHub = class {
|
|
|
624
628
|
send(from, to, kind, content) {
|
|
625
629
|
return this.route(from, to, kind, content);
|
|
626
630
|
}
|
|
627
|
-
/** Send a structured task message to `to` (or {@link BROADCAST}).
|
|
631
|
+
/** Send a structured task message to `to` (or {@link BROADCAST}). Records
|
|
632
|
+
* a ledger entry so the sender can later query status / wait for acks. */
|
|
628
633
|
sendTask(from, to, task) {
|
|
629
|
-
|
|
634
|
+
const message = this.route(from, to, "task", JSON.stringify(task));
|
|
635
|
+
this.recordTask(message, task);
|
|
636
|
+
return message;
|
|
630
637
|
}
|
|
631
|
-
/** Send an acknowledgement back to the sender of `ref`.
|
|
638
|
+
/** Send an acknowledgement back to the sender of `ref`. Updates the task
|
|
639
|
+
* ledger when `ref` is a known task; chat/notice refs still route. */
|
|
632
640
|
sendAck(from, ref, ack) {
|
|
633
|
-
const original = this.historyRing.findLast((
|
|
641
|
+
const original = this.historyRing.findLast((message2) => message2.id === ref);
|
|
634
642
|
if (!original) throw new Error(`cannot ack unknown message: ${ref}`);
|
|
635
|
-
|
|
643
|
+
const message = this.route(from, original.from, "ack", JSON.stringify(ack), ref);
|
|
644
|
+
this.applyAckToLedger(message, from, ref, ack);
|
|
645
|
+
return message;
|
|
646
|
+
}
|
|
647
|
+
/** Create or refresh the ledger row for a newly sent task. */
|
|
648
|
+
recordTask(message, task) {
|
|
649
|
+
const now = message.ts;
|
|
650
|
+
this.taskLedger.set(message.id, {
|
|
651
|
+
id: message.id,
|
|
652
|
+
from: message.from,
|
|
653
|
+
to: message.to,
|
|
654
|
+
prompt: task.prompt,
|
|
655
|
+
...task.context !== void 0 ? { context: task.context } : {},
|
|
656
|
+
...task.deliverable !== void 0 ? { deliverable: task.deliverable } : {},
|
|
657
|
+
status: "pending",
|
|
658
|
+
acks: [],
|
|
659
|
+
createdAt: now,
|
|
660
|
+
updatedAt: now
|
|
661
|
+
});
|
|
662
|
+
this.evictOverflowTasks();
|
|
663
|
+
}
|
|
664
|
+
/** Fold an ack into the task's timeline and status. Unknown refs are a
|
|
665
|
+
* no-op (chat/notice can be acked for routing without a ledger row). */
|
|
666
|
+
applyAckToLedger(message, from, ref, ack) {
|
|
667
|
+
const record = this.taskLedger.get(ref);
|
|
668
|
+
if (record === void 0) return;
|
|
669
|
+
record.acks.push({
|
|
670
|
+
from,
|
|
671
|
+
status: ack.status,
|
|
672
|
+
...ack.note !== void 0 ? { note: ack.note } : {},
|
|
673
|
+
ts: message.ts,
|
|
674
|
+
messageId: message.id
|
|
675
|
+
});
|
|
676
|
+
record.status = ack.status;
|
|
677
|
+
record.updatedAt = message.ts;
|
|
678
|
+
}
|
|
679
|
+
/** Keep the ledger bounded: drop oldest terminal tasks first. */
|
|
680
|
+
evictOverflowTasks() {
|
|
681
|
+
if (this.taskLedger.size <= MAX_TASKS) return;
|
|
682
|
+
const evictable = [...this.taskLedger.values()].filter((t) => TERMINAL_TASK_STATUSES.includes(t.status)).sort((a, b) => a.updatedAt - b.updatedAt);
|
|
683
|
+
let overflow = this.taskLedger.size - MAX_TASKS;
|
|
684
|
+
for (const record of evictable) {
|
|
685
|
+
if (overflow <= 0) break;
|
|
686
|
+
this.taskLedger.delete(record.id);
|
|
687
|
+
overflow--;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/** One task by message id (the ack `ref`). */
|
|
691
|
+
taskOf(ref) {
|
|
692
|
+
const record = this.taskLedger.get(ref);
|
|
693
|
+
return record === void 0 ? void 0 : { ...record, acks: record.acks.map((a) => ({ ...a })) };
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Tasks involving `peer`. `role`:
|
|
697
|
+
* - `sent` — tasks this peer delegated (`from`)
|
|
698
|
+
* - `received` — tasks addressed to this peer (or broadcast)
|
|
699
|
+
* - `all` (default) — either side
|
|
700
|
+
* Optional `status` filter matches the ledger's current status.
|
|
701
|
+
*/
|
|
702
|
+
listTasks(peerId, options) {
|
|
703
|
+
const role = options?.role ?? "all";
|
|
704
|
+
const limit = Math.max(1, options?.limit ?? 50);
|
|
705
|
+
const out = [];
|
|
706
|
+
for (const record of this.taskLedger.values()) {
|
|
707
|
+
const isSent = record.from === peerId;
|
|
708
|
+
const isReceived = record.to === peerId || record.to === BROADCAST;
|
|
709
|
+
if (role === "sent" && !isSent) continue;
|
|
710
|
+
if (role === "received" && !isReceived) continue;
|
|
711
|
+
if (role === "all" && !isSent && !isReceived) continue;
|
|
712
|
+
if (options?.status !== void 0 && record.status !== options.status) continue;
|
|
713
|
+
out.push({ ...record, acks: record.acks.map((a) => ({ ...a })) });
|
|
714
|
+
}
|
|
715
|
+
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
716
|
+
return out.slice(0, limit);
|
|
636
717
|
}
|
|
637
718
|
/** Recent messages involving `peer` (inbound and outbound, plus group
|
|
638
719
|
* channels the peer belongs to), newest first. */
|
|
@@ -855,11 +936,29 @@ var AgentHub = class {
|
|
|
855
936
|
* Long-poll for the next message addressed to `peer`: resolves immediately
|
|
856
937
|
* when a matching one is queued, otherwise waits up to `timeoutMs` (capped
|
|
857
938
|
* by `waitTimeoutMs`) or until `signal` aborts. `from` narrows to one sender.
|
|
939
|
+
* `ref` narrows to the ack of a specific task message id.
|
|
940
|
+
*
|
|
941
|
+
* Takes ONLY the first matching queued message. Calling poll() here would
|
|
942
|
+
* drain the whole mailbox and discard every message after the first —
|
|
943
|
+
* a multi-message burst (or a reconnect with a full queue) would silently
|
|
944
|
+
* lose messages that never reach wait/poll again.
|
|
858
945
|
*/
|
|
859
|
-
wait(peerId, timeoutMs, from, signal) {
|
|
946
|
+
wait(peerId, timeoutMs, from, signal, ref) {
|
|
860
947
|
const startedAt = Date.now();
|
|
861
|
-
const
|
|
862
|
-
|
|
948
|
+
const matches = (message) => {
|
|
949
|
+
if (from !== void 0 && message.from !== from) return false;
|
|
950
|
+
if (ref !== void 0 && !(message.kind === "ack" && message.ref === ref)) return false;
|
|
951
|
+
return true;
|
|
952
|
+
};
|
|
953
|
+
const queue = this.queues.get(peerId);
|
|
954
|
+
if (queue !== void 0 && queue.length > 0) {
|
|
955
|
+
const index = queue.findIndex(matches);
|
|
956
|
+
if (index >= 0) {
|
|
957
|
+
const [queued] = queue.splice(index, 1);
|
|
958
|
+
this.options.onMailboxesChanged?.();
|
|
959
|
+
return Promise.resolve({ type: "message", message: queued });
|
|
960
|
+
}
|
|
961
|
+
}
|
|
863
962
|
const budget = Math.max(1, Math.min(Math.floor(timeoutMs), this.options.waitTimeoutMs));
|
|
864
963
|
return new Promise((resolve) => {
|
|
865
964
|
let settled = false;
|
|
@@ -888,7 +987,13 @@ var AgentHub = class {
|
|
|
888
987
|
}
|
|
889
988
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
890
989
|
const list = this.waiters.get(peerId) ?? [];
|
|
891
|
-
list.push({
|
|
990
|
+
list.push({
|
|
991
|
+
resolve: settle,
|
|
992
|
+
timer,
|
|
993
|
+
onAbort,
|
|
994
|
+
...from !== void 0 ? { from } : {},
|
|
995
|
+
...ref !== void 0 ? { ref } : {}
|
|
996
|
+
});
|
|
892
997
|
this.waiters.set(peerId, list);
|
|
893
998
|
});
|
|
894
999
|
}
|
|
@@ -918,7 +1023,11 @@ var AgentHub = class {
|
|
|
918
1023
|
/** Queue or hand off a message; wake the first matching waiter for its target. */
|
|
919
1024
|
deliver(target, message) {
|
|
920
1025
|
const list = this.waiters.get(target) ?? [];
|
|
921
|
-
const index = list.findIndex((waiter) =>
|
|
1026
|
+
const index = list.findIndex((waiter) => {
|
|
1027
|
+
if (waiter.from !== void 0 && waiter.from !== message.from) return false;
|
|
1028
|
+
if (waiter.ref !== void 0 && !(message.kind === "ack" && message.ref === waiter.ref)) return false;
|
|
1029
|
+
return true;
|
|
1030
|
+
});
|
|
922
1031
|
if (index >= 0) {
|
|
923
1032
|
const [waiter] = list.splice(index, 1);
|
|
924
1033
|
this.waiters.set(target, list);
|
|
@@ -1356,7 +1465,7 @@ function hubTools(hub, registry, options) {
|
|
|
1356
1465
|
},
|
|
1357
1466
|
{
|
|
1358
1467
|
name: "bridge_task",
|
|
1359
|
-
description: "Delegate a structured task to another agent. The receiving agent decides whether to accept; expect an ack (accepted/rejected/done/failed) via bridge_wait / bridge_poll.",
|
|
1468
|
+
description: "Delegate a structured task to another agent. The receiving agent decides whether to accept; expect an ack (accepted/rejected/done/failed) via bridge_wait / bridge_poll. Track progress with bridge_task_status(ref) or bridge_tasks; wait specifically for the ack with bridge_wait({ ref }).",
|
|
1360
1469
|
inputSchema: schema(
|
|
1361
1470
|
{
|
|
1362
1471
|
to: str('Target peerId, or "all" to broadcast.'),
|
|
@@ -1374,7 +1483,7 @@ function hubTools(hub, registry, options) {
|
|
|
1374
1483
|
},
|
|
1375
1484
|
{
|
|
1376
1485
|
name: "bridge_ack",
|
|
1377
|
-
description: "Acknowledge a message received from another agent (usually a delegated task): accepted | rejected | done | failed. The ack is routed back to the original sender of `ref
|
|
1486
|
+
description: "Acknowledge a message received from another agent (usually a delegated task): accepted | rejected | done | failed. The ack is routed back to the original sender of `ref` and updates the hub task ledger (see bridge_task_status / bridge_tasks).",
|
|
1378
1487
|
inputSchema: schema(
|
|
1379
1488
|
{
|
|
1380
1489
|
ref: str("The id of the message being acknowledged."),
|
|
@@ -1392,14 +1501,61 @@ function hubTools(hub, registry, options) {
|
|
|
1392
1501
|
return receipt(hub.sendAck(peer, String(args.ref), { status, ...args.note !== void 0 ? { note: String(args.note) } : {} }));
|
|
1393
1502
|
})
|
|
1394
1503
|
},
|
|
1504
|
+
{
|
|
1505
|
+
name: "bridge_tasks",
|
|
1506
|
+
description: "List delegated tasks involving you, newest first. role=sent = tasks you assigned; role=received = tasks assigned to you (or broadcast); status filters the ledger state (pending/accepted/rejected/done/failed). Each row carries the full ack timeline.",
|
|
1507
|
+
inputSchema: schema({
|
|
1508
|
+
role: { type: "string", enum: ["sent", "received", "all"], description: "Which side of the task to list (default all)." },
|
|
1509
|
+
status: { type: "string", enum: ["pending", "accepted", "rejected", "done", "failed"], description: "Only tasks whose current ledger status matches." },
|
|
1510
|
+
limit: int("How many tasks to return (default 50).")
|
|
1511
|
+
}),
|
|
1512
|
+
handler: wrap(true, async (args, peer) => {
|
|
1513
|
+
const role = args.role === void 0 ? void 0 : String(args.role);
|
|
1514
|
+
if (role !== void 0 && role !== "sent" && role !== "received" && role !== "all") {
|
|
1515
|
+
throw new Error(`invalid role: ${role} (expected sent | received | all)`);
|
|
1516
|
+
}
|
|
1517
|
+
const status = args.status === void 0 ? void 0 : String(args.status);
|
|
1518
|
+
const validStatus = ["pending", "accepted", "rejected", "done", "failed"];
|
|
1519
|
+
if (status !== void 0 && !validStatus.includes(status)) {
|
|
1520
|
+
throw new Error(`invalid status: ${status} (expected ${validStatus.join(" | ")})`);
|
|
1521
|
+
}
|
|
1522
|
+
const tasks = hub.listTasks(peer, {
|
|
1523
|
+
...role !== void 0 ? { role } : {},
|
|
1524
|
+
...status !== void 0 ? { status } : {},
|
|
1525
|
+
...args.limit !== void 0 ? { limit: Number(args.limit) } : {}
|
|
1526
|
+
});
|
|
1527
|
+
return { tasks };
|
|
1528
|
+
})
|
|
1529
|
+
},
|
|
1530
|
+
{
|
|
1531
|
+
name: "bridge_task_status",
|
|
1532
|
+
description: "Status of one delegated task: current ledger state (pending \u2192 accepted \u2192 done/failed, or rejected), the original prompt/context/deliverable, and every ack event (who, status, note, when). Use the task message id (the same id you pass as bridge_ack ref).",
|
|
1533
|
+
inputSchema: schema({ ref: str("Task message id (the id returned by bridge_task, also used as ack ref).") }, ["ref"]),
|
|
1534
|
+
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1535
|
+
const ref = String(args.ref);
|
|
1536
|
+
const task = hub.taskOf(ref);
|
|
1537
|
+
if (task === void 0) throw new Error(`unknown task: ${ref}`);
|
|
1538
|
+
if (task.from !== peer && task.to !== peer && task.to !== BROADCAST) {
|
|
1539
|
+
requireManager(peer, `reading task '${ref}'`, sessionId);
|
|
1540
|
+
}
|
|
1541
|
+
return { task };
|
|
1542
|
+
})
|
|
1543
|
+
},
|
|
1395
1544
|
{
|
|
1396
1545
|
name: "bridge_wait",
|
|
1397
|
-
description: "Wait (long-poll) for the next message addressed to you. Resolves immediately when one is queued; otherwise blocks until one arrives or the timeout fires. `from` narrows to one sender. Loop this tool to hold a real-time conversation.",
|
|
1546
|
+
description: "Wait (long-poll) for the next message addressed to you. Resolves immediately when one is queued; otherwise blocks until one arrives or the timeout fires. `from` narrows to one sender. `ref` waits specifically for the ack of that task id. Loop this tool to hold a real-time conversation.",
|
|
1398
1547
|
inputSchema: schema({
|
|
1399
1548
|
from: optStr("Only wait for messages from this peerId."),
|
|
1549
|
+
ref: optStr("Only wait for the ack of this task message id (bridge_task id)."),
|
|
1400
1550
|
timeoutMs: int(`Max wait in milliseconds (default ${DEFAULT_WAIT_MS}, ceiling is server waitTimeoutMs).`)
|
|
1401
1551
|
}),
|
|
1402
|
-
handler: wrap(true, async (args, peer) => presentWait(await hub.wait(
|
|
1552
|
+
handler: wrap(true, async (args, peer) => presentWait(await hub.wait(
|
|
1553
|
+
peer,
|
|
1554
|
+
args.timeoutMs === void 0 ? options.defaultWaitMs ?? DEFAULT_WAIT_MS : Number(args.timeoutMs),
|
|
1555
|
+
args.from === void 0 ? void 0 : String(args.from),
|
|
1556
|
+
void 0,
|
|
1557
|
+
args.ref === void 0 ? void 0 : String(args.ref)
|
|
1558
|
+
)))
|
|
1403
1559
|
},
|
|
1404
1560
|
{
|
|
1405
1561
|
name: "bridge_poll",
|
|
@@ -1422,7 +1578,11 @@ function hubTools(hub, registry, options) {
|
|
|
1422
1578
|
{
|
|
1423
1579
|
name: "bridge_history",
|
|
1424
1580
|
description: 'Recent messages involving you (newest first). Use to refresh context after a reconnect. Reading ANOTHER peer\'s conversation \u2014 or `peer: "all"` for the unfiltered tail \u2014 requires manager rights (hub managerPeers).',
|
|
1425
|
-
inputSchema: schema({
|
|
1581
|
+
inputSchema: schema({
|
|
1582
|
+
peer: optStr('PeerId whose conversation to inspect; "all" = every peer; default: yourself. Other peers / "all" require manager rights.'),
|
|
1583
|
+
channel: optStr("Read a group channel history instead of a peer conversation (you must be a member, or a manager)."),
|
|
1584
|
+
limit: int("How many messages to return (default 20).")
|
|
1585
|
+
}),
|
|
1426
1586
|
handler: wrap(true, async (args, peer, sessionId) => {
|
|
1427
1587
|
const limit = Math.min(args.limit === void 0 ? 20 : Number(args.limit), 1e3);
|
|
1428
1588
|
const target = args.peer === void 0 ? peer : String(args.peer);
|
|
@@ -1730,6 +1890,7 @@ function adminPage() {
|
|
|
1730
1890
|
var SUPPORTED_VERSIONS = ["2025-06-18", "2025-03-26", "2024-11-05"];
|
|
1731
1891
|
var LATEST_VERSION = SUPPORTED_VERSIONS[0];
|
|
1732
1892
|
var MAX_BODY_BYTES = 1048576;
|
|
1893
|
+
var HEARTBEAT_MS = 2e4;
|
|
1733
1894
|
var SessionRegistry = class {
|
|
1734
1895
|
sessions = /* @__PURE__ */ new Set();
|
|
1735
1896
|
/** sessionId → peerId claimed via `bridge_register`. */
|
|
@@ -1875,7 +2036,7 @@ var SessionRegistry = class {
|
|
|
1875
2036
|
var McpStreamableHttpServer = class {
|
|
1876
2037
|
constructor(tools, info, registry, log2 = () => {
|
|
1877
2038
|
}, onInitialize = () => {
|
|
1878
|
-
}, authenticate, adminApi) {
|
|
2039
|
+
}, authenticate, adminApi, heartbeatMs) {
|
|
1879
2040
|
this.tools = tools;
|
|
1880
2041
|
this.info = info;
|
|
1881
2042
|
this.registry = registry;
|
|
@@ -1883,10 +2044,21 @@ var McpStreamableHttpServer = class {
|
|
|
1883
2044
|
this.onInitialize = onInitialize;
|
|
1884
2045
|
this.authenticate = authenticate;
|
|
1885
2046
|
this.adminApi = adminApi;
|
|
2047
|
+
this.heartbeatMs = heartbeatMs;
|
|
1886
2048
|
}
|
|
1887
2049
|
sseStreams = /* @__PURE__ */ new Map();
|
|
2050
|
+
heartbeatTimer;
|
|
1888
2051
|
/** Attach request handling for `path` (e.g. `/mcp`) to an http server. */
|
|
1889
2052
|
attach(server, path2) {
|
|
2053
|
+
this.heartbeatTimer = setInterval(() => {
|
|
2054
|
+
if (this.sseStreams.size === 0) return;
|
|
2055
|
+
this.notifyAll("notifications/message", {
|
|
2056
|
+
level: "debug",
|
|
2057
|
+
logger: "bridge",
|
|
2058
|
+
data: { event: "heartbeat", ts: Date.now() }
|
|
2059
|
+
});
|
|
2060
|
+
}, this.heartbeatMs ?? HEARTBEAT_MS);
|
|
2061
|
+
this.heartbeatTimer.unref?.();
|
|
1890
2062
|
server.on("request", (req, res) => {
|
|
1891
2063
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1892
2064
|
if (url.pathname === ADMIN_PATH && req.method === "GET") {
|
|
@@ -1894,6 +2066,11 @@ var McpStreamableHttpServer = class {
|
|
|
1894
2066
|
res.end(adminPage());
|
|
1895
2067
|
return;
|
|
1896
2068
|
}
|
|
2069
|
+
if (url.pathname === "/healthz") {
|
|
2070
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8", ...corsHeaders() });
|
|
2071
|
+
res.end(JSON.stringify({ ok: true, ts: Date.now() }));
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
1897
2074
|
if (url.pathname.startsWith(ADMIN_API_PATH) && this.adminApi !== void 0) {
|
|
1898
2075
|
void this.handleAdminApi(req, res, url);
|
|
1899
2076
|
return;
|
|
@@ -1919,6 +2096,7 @@ var McpStreamableHttpServer = class {
|
|
|
1919
2096
|
}
|
|
1920
2097
|
/** Close all open SSE streams (called on server shutdown). */
|
|
1921
2098
|
close() {
|
|
2099
|
+
if (this.heartbeatTimer !== void 0) clearInterval(this.heartbeatTimer);
|
|
1922
2100
|
for (const stream of this.sseStreams.values()) stream.end();
|
|
1923
2101
|
this.sseStreams.clear();
|
|
1924
2102
|
}
|
|
@@ -2239,7 +2417,7 @@ var SQLiteStateStore = class {
|
|
|
2239
2417
|
id TEXT PRIMARY KEY, name TEXT, members TEXT NOT NULL, created_by TEXT NOT NULL, created_at INTEGER NOT NULL
|
|
2240
2418
|
);
|
|
2241
2419
|
`);
|
|
2242
|
-
log2.
|
|
2420
|
+
log2.info(`sqlite state open: ${file}`);
|
|
2243
2421
|
}
|
|
2244
2422
|
db;
|
|
2245
2423
|
buffer = [];
|
|
@@ -2334,7 +2512,7 @@ var SQLiteStateStore = class {
|
|
|
2334
2512
|
|
|
2335
2513
|
// src/index.ts
|
|
2336
2514
|
var SERVER_NAME = "agent-comm-hub";
|
|
2337
|
-
var SERVER_VERSION = "0.
|
|
2515
|
+
var SERVER_VERSION = "0.8.0";
|
|
2338
2516
|
var DEFAULT_HOST = "127.0.0.1";
|
|
2339
2517
|
var DEFAULT_PORT = 18764;
|
|
2340
2518
|
var DEFAULT_PATH = "/mcp";
|
|
@@ -2352,9 +2530,10 @@ var DEFAULT_CONFIG = {
|
|
|
2352
2530
|
connectedWindowMs: 3e4,
|
|
2353
2531
|
peerIdleTimeoutMs: 6e5,
|
|
2354
2532
|
// The companion desktop app (and `agent-comm-hub status` probe) registers
|
|
2355
|
-
// as `agent-hub-cli`;
|
|
2356
|
-
// are
|
|
2357
|
-
|
|
2533
|
+
// as `agent-hub-cli`; the web admin console registers as `hub-admin`.
|
|
2534
|
+
// Both are roster managers. Agents themselves are NOT managers — they
|
|
2535
|
+
// keep chatting, a GUI manages.
|
|
2536
|
+
managerPeers: ["agent-hub-cli", "hub-admin"]
|
|
2358
2537
|
};
|
|
2359
2538
|
function authTokensAdmin(file, reloadAuth) {
|
|
2360
2539
|
if (file === void 0) return void 0;
|
|
@@ -2604,7 +2783,8 @@ function startHub(config = {}, log2 = console) {
|
|
|
2604
2783
|
}
|
|
2605
2784
|
},
|
|
2606
2785
|
auth,
|
|
2607
|
-
authTokensAdmin(resolved.authTokens, authReload)
|
|
2786
|
+
authTokensAdmin(resolved.authTokens, authReload),
|
|
2787
|
+
resolved.heartbeatMs
|
|
2608
2788
|
);
|
|
2609
2789
|
const server = createServer();
|
|
2610
2790
|
mcp.attach(server, resolved.path);
|
|
@@ -3131,6 +3311,7 @@ function runUpdate() {
|
|
|
3131
3311
|
const out = execFileSync2(process.execPath, ["--input-type=module", "-e", script], { encoding: "utf8", windowsHide: true });
|
|
3132
3312
|
messages.push(out.trim());
|
|
3133
3313
|
messages.push("restart the hub (agent-comm-hub) to pick up the new version");
|
|
3314
|
+
messages.push("refresh installed agent skills: agent-comm-hub setup (copies the packaged SKILL.md into each agent; npm update alone does not rewrite skill dirs)");
|
|
3134
3315
|
return { ok: true, messages };
|
|
3135
3316
|
} catch (error) {
|
|
3136
3317
|
return { ok: false, messages: [`update failed: ${error.message}`] };
|
|
@@ -3306,7 +3487,7 @@ var DEFAULT_DB = join4(homedir4(), ".agent-comm-hub", "state.db");
|
|
|
3306
3487
|
var DEFAULT_AUTH_FILE = join4(homedir4(), ".agent-comm-hub", "tokens.json");
|
|
3307
3488
|
function parseArgs(argv) {
|
|
3308
3489
|
const args = {};
|
|
3309
|
-
const numeric = /* @__PURE__ */ new Set(["--port", "--max-queue", "--history-limit", "--wait-timeout-ms", "--default-wait-ms", "--connected-window-ms", "--peer-idle-timeout-ms", "--herdr-timeout-ms"]);
|
|
3490
|
+
const numeric = /* @__PURE__ */ new Set(["--port", "--max-queue", "--history-limit", "--wait-timeout-ms", "--default-wait-ms", "--connected-window-ms", "--peer-idle-timeout-ms", "--herdr-timeout-ms", "--heartbeat-ms"]);
|
|
3310
3491
|
const string = /* @__PURE__ */ new Set(["--host", "--path", "--url", "--server-name", "--agent", "--herdr-bin", "--manager-peers", "--state-file", "--auth-tokens", "--db", "--file", "--role", "--owner", "--token"]);
|
|
3311
3492
|
for (let i = 0; i < argv.length; i++) {
|
|
3312
3493
|
const flag = argv[i];
|
|
@@ -3364,6 +3545,7 @@ Hub options:
|
|
|
3364
3545
|
--path <p> MCP endpoint path (default /mcp)
|
|
3365
3546
|
--max-queue <n> Queued messages per peer before dropping oldest (default 200)
|
|
3366
3547
|
--history-limit <n> Retained history messages (default 1000)
|
|
3548
|
+
--heartbeat-ms <n> SSE heartbeat cadence in ms (default 20000)
|
|
3367
3549
|
--wait-timeout-ms <n> Long-poll ceiling for bridge_wait (default 60000)
|
|
3368
3550
|
--default-wait-ms <n> bridge_wait default budget (default 30000)
|
|
3369
3551
|
--connected-window-ms <n> Peer counts as active within this window (default 30000)
|
|
@@ -3593,7 +3775,8 @@ try {
|
|
|
3593
3775
|
stateFile: args["--state-file"] === void 0 ? DEFAULT_STATE_FILE : args["--state-file"] === "off" ? void 0 : String(args["--state-file"]),
|
|
3594
3776
|
authTokens: args["--auth-tokens"] === void 0 ? void 0 : String(args["--auth-tokens"]),
|
|
3595
3777
|
allowJoin: args["--allow-join"] === true,
|
|
3596
|
-
db: args["--db"] === void 0 ? DEFAULT_DB : args["--db"] === "off" ? void 0 : String(args["--db"])
|
|
3778
|
+
db: args["--db"] === void 0 ? DEFAULT_DB : args["--db"] === "off" ? void 0 : String(args["--db"]),
|
|
3779
|
+
heartbeatMs: args["--heartbeat-ms"]
|
|
3597
3780
|
}, log);
|
|
3598
3781
|
const shutdown = () => {
|
|
3599
3782
|
log.info("agent-comm-hub shutting down");
|