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/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
- return this.route(from, to, "task", JSON.stringify(task));
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((message) => message.id === ref);
641
+ const original = this.historyRing.findLast((message2) => message2.id === ref);
634
642
  if (!original) throw new Error(`cannot ack unknown message: ${ref}`);
635
- return this.route(from, original.from, "ack", JSON.stringify(ack), ref);
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 queued = this.poll(peerId, from)[0];
862
- if (queued) return Promise.resolve({ type: "message", message: queued });
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({ resolve: settle, timer, onAbort, ...from !== void 0 ? { from } : {} });
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) => waiter.from === void 0 || waiter.from === message.from);
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(peer, args.timeoutMs === void 0 ? options.defaultWaitMs ?? DEFAULT_WAIT_MS : Number(args.timeoutMs), args.from === void 0 ? void 0 : String(args.from))))
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({ peer: optStr('PeerId whose conversation to inspect; "all" = every peer; default: yourself. Other peers / "all" require manager rights.'), limit: int("How many messages to return (default 20).") }),
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.warn(`sqlite state open: ${file}`);
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.7.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`; it is the natural roster manager. Agents themselves
2356
- // are NOT managers — they keep chatting, a GUI manages.
2357
- managerPeers: ["agent-hub-cli"]
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");