@unagents/messenger-broker 0.10.0 → 0.11.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/dist/index.d.mts CHANGED
@@ -22,6 +22,11 @@ interface BrokerOptions {
22
22
  discoveryPort?: number;
23
23
  /** UDP bind address when pinned (`AGENT_MESSENGER_DISCOVERY_ADDR`, default `""` = all interfaces). */
24
24
  discoveryAddr?: string;
25
+ /**
26
+ * Explicit discovery send targets, `host:port` each (`AGENT_MESSENGER_DISCOVERY_TARGETS`, comma separated).
27
+ * When non-empty, probes and announces go to these instead of broadcast (tests; LAN without broadcast).
28
+ */
29
+ discoveryTargets?: string[];
25
30
  /**
26
31
  * Run as a local fallback broker (`AGENT_MESSENGER_FALLBACK=1`, set by the client when it spawns one).
27
32
  * A fallback probes for other brokers every `fallbackIntervalMs`; when it finds one it relays its
@@ -38,16 +43,16 @@ interface BrokerOptions {
38
43
  */
39
44
  mesh?: boolean;
40
45
  /**
41
- * Mesh members to sync with. Empty (default) = the `brokers` list of the client
42
- * config file (`AGENT_MESSENGER_CONFIG`, default `~/.agent-messenger/config.json`), re-read every round.
46
+ * Test-only: a fixed list of mesh members that replaces discovery. Empty (default) = members are
47
+ * found by UDP discovery (announce / probe reply, token-checked). Not documented for users.
43
48
  */
44
49
  meshBrokers?: string[];
50
+ /** Mesh announce interval in ms (`AGENT_MESSENGER_MESH_ANNOUNCE_MS`, default 10000). */
51
+ meshAnnounceMs?: number;
45
52
  /** Mesh sync interval in ms (`AGENT_MESSENGER_MESH_INTERVAL_MS`, default 5000). */
46
53
  meshIntervalMs?: number;
47
54
  /** Replicated peers disappear this long after their origin stopped syncing (`AGENT_MESSENGER_MESH_PEER_TTL_MS`, default 300000). */
48
55
  meshPeerTtlMs?: number;
49
- /** Client config file read for the mesh member list (`AGENT_MESSENGER_CONFIG`). */
50
- configFile?: string;
51
56
  }
52
57
  type BrokerConfig = Required<Omit<BrokerOptions, "onSupersede">> & Pick<BrokerOptions, "onSupersede">;
53
58
  /** Marks a broker as a local fallback. Broker-private (not in the protocol ENV table, which mirrors legacy). */
package/dist/index.d.ts CHANGED
@@ -22,6 +22,11 @@ interface BrokerOptions {
22
22
  discoveryPort?: number;
23
23
  /** UDP bind address when pinned (`AGENT_MESSENGER_DISCOVERY_ADDR`, default `""` = all interfaces). */
24
24
  discoveryAddr?: string;
25
+ /**
26
+ * Explicit discovery send targets, `host:port` each (`AGENT_MESSENGER_DISCOVERY_TARGETS`, comma separated).
27
+ * When non-empty, probes and announces go to these instead of broadcast (tests; LAN without broadcast).
28
+ */
29
+ discoveryTargets?: string[];
25
30
  /**
26
31
  * Run as a local fallback broker (`AGENT_MESSENGER_FALLBACK=1`, set by the client when it spawns one).
27
32
  * A fallback probes for other brokers every `fallbackIntervalMs`; when it finds one it relays its
@@ -38,16 +43,16 @@ interface BrokerOptions {
38
43
  */
39
44
  mesh?: boolean;
40
45
  /**
41
- * Mesh members to sync with. Empty (default) = the `brokers` list of the client
42
- * config file (`AGENT_MESSENGER_CONFIG`, default `~/.agent-messenger/config.json`), re-read every round.
46
+ * Test-only: a fixed list of mesh members that replaces discovery. Empty (default) = members are
47
+ * found by UDP discovery (announce / probe reply, token-checked). Not documented for users.
43
48
  */
44
49
  meshBrokers?: string[];
50
+ /** Mesh announce interval in ms (`AGENT_MESSENGER_MESH_ANNOUNCE_MS`, default 10000). */
51
+ meshAnnounceMs?: number;
45
52
  /** Mesh sync interval in ms (`AGENT_MESSENGER_MESH_INTERVAL_MS`, default 5000). */
46
53
  meshIntervalMs?: number;
47
54
  /** Replicated peers disappear this long after their origin stopped syncing (`AGENT_MESSENGER_MESH_PEER_TTL_MS`, default 300000). */
48
55
  meshPeerTtlMs?: number;
49
- /** Client config file read for the mesh member list (`AGENT_MESSENGER_CONFIG`). */
50
- configFile?: string;
51
56
  }
52
57
  type BrokerConfig = Required<Omit<BrokerOptions, "onSupersede">> & Pick<BrokerOptions, "onSupersede">;
53
58
  /** Marks a broker as a local fallback. Broker-private (not in the protocol ENV table, which mirrors legacy). */
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import 'listhen';
2
2
  import 'h3';
3
3
  import '@unagents/messenger-protocol';
4
- export { D as DEFAULT_STATE_FILE, F as FALLBACK_ENV, a as FALLBACK_INTERVAL_ENV, M as MESH_ENV, S as STATE_DIR, V as VERSION, c as configFromEnv, r as resolveConfig, s as startBroker } from './shared/messenger-broker.B8W77sqB.mjs';
4
+ export { D as DEFAULT_STATE_FILE, F as FALLBACK_ENV, a as FALLBACK_INTERVAL_ENV, M as MESH_ENV, S as STATE_DIR, V as VERSION, c as configFromEnv, r as resolveConfig, s as startBroker } from './shared/messenger-broker.Co46RRtX.mjs';
5
5
  import 'node:os';
6
6
  import 'node:path';
7
7
  import 'defu';
package/dist/main.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { s as startBroker, l as log } from './shared/messenger-broker.B8W77sqB.mjs';
1
+ import { s as startBroker, l as log } from './shared/messenger-broker.Co46RRtX.mjs';
2
2
  import 'listhen';
3
3
  import 'h3';
4
4
  import '@unagents/messenger-protocol';
@@ -1,12 +1,12 @@
1
1
  import { listen } from 'listhen';
2
2
  import { createRouter, defineEventHandler, createApp, send, setResponseStatus, getRequestHeader, readRawBody, toNodeListener } from 'h3';
3
- import { DEFAULT_BIND, ENV, DEFAULT_DISCOVERY_PORT, DEFAULT_HTTP_PORT, PROTOCOL_NAME_PATTERN, PROTOCOL_META_MAX_LENGTH, PROTOCOL_MANIFEST_MAX_BYTES, PROTOCOL_DOC_MAX_BYTES, PROTOCOL_TOTAL_MAX_BYTES, canonicalJSON, HEARTBEAT_TIMEOUT_MS, PROTOCOL_MAX_STORED, MESH_SYNC_ROUTE, HEALTH_ROUTE, BROKER_POST_ROUTES, PROTOCOL_ROUTES, RELAY_ROUTE, DISCOVERY_KEY, DISCOVERY_PROBE_VALUE, DISCOVERY_REPLY_VALUE } from '@unagents/messenger-protocol';
3
+ import { DEFAULT_BIND, MESH_ANNOUNCE_ENV, DISCOVERY_TARGETS_ENV, ENV, DEFAULT_DISCOVERY_PORT, DEFAULT_HTTP_PORT, PROTOCOL_NAME_PATTERN, PROTOCOL_META_MAX_LENGTH, PROTOCOL_MANIFEST_MAX_BYTES, PROTOCOL_DOC_MAX_BYTES, PROTOCOL_TOTAL_MAX_BYTES, canonicalJSON, HEARTBEAT_TIMEOUT_MS, PROTOCOL_MAX_STORED, MESH_SYNC_ROUTE, DISCOVERY_KEY, DISCOVERY_ANNOUNCE_VALUE, MESH_ANNOUNCE_MAX_SKEW_MS, DISCOVERY_REPLY_VALUE, meshMacEqual, meshMac, HEALTH_ROUTE, BROKER_POST_ROUTES, PROTOCOL_ROUTES, RELAY_ROUTE, DISCOVERY_PROBE_VALUE } from '@unagents/messenger-protocol';
4
4
  import { homedir, hostname, networkInterfaces } from 'node:os';
5
5
  import { join, dirname } from 'node:path';
6
6
  import { defu } from 'defu';
7
7
  import { readFileSync, mkdirSync, writeFileSync, renameSync } from 'node:fs';
8
8
  import { createConsola } from 'consola';
9
- import { createHash } from 'node:crypto';
9
+ import { createHash, randomBytes } from 'node:crypto';
10
10
  import { request } from 'node:http';
11
11
  import { createSocket } from 'node:dgram';
12
12
 
@@ -30,13 +30,14 @@ function configFromEnv(env = process.env) {
30
30
  discovery: (env[ENV.DISCOVERY] ?? "on").toLowerCase() !== "off",
31
31
  discoveryPort: parseInt(env[ENV.DISCOVERY_PORT] ?? String(DEFAULT_DISCOVERY_PORT), 10),
32
32
  discoveryAddr: env[ENV.DISCOVERY_ADDR] ?? "",
33
+ discoveryTargets: (env[DISCOVERY_TARGETS_ENV] ?? "").split(",").map((s) => s.trim()).filter(Boolean),
33
34
  fallback: ["1", "true", "on", "yes"].includes((env[FALLBACK_ENV] ?? "").toLowerCase()),
34
35
  fallbackIntervalMs: parseInt(env[FALLBACK_INTERVAL_ENV] ?? String(DEFAULT_FALLBACK_INTERVAL_MS), 10),
35
36
  mesh: (env[MESH_ENV.MESH] ?? "off").toLowerCase() === "on",
36
37
  meshBrokers: [],
38
+ meshAnnounceMs: parseInt(env[MESH_ANNOUNCE_ENV] ?? "10000", 10),
37
39
  meshIntervalMs: parseInt(env[MESH_ENV.INTERVAL_MS] ?? "5000", 10),
38
- meshPeerTtlMs: parseInt(env[MESH_ENV.PEER_TTL_MS] ?? "300000", 10),
39
- configFile: env[ENV.CONFIG] ?? join(STATE_DIR, "config.json")
40
+ meshPeerTtlMs: parseInt(env[MESH_ENV.PEER_TTL_MS] ?? "300000", 10)
40
41
  };
41
42
  }
42
43
  function resolveConfig(options = {}) {
@@ -780,6 +781,15 @@ class Mesh {
780
781
  if (this.timer) clearInterval(this.timer);
781
782
  this.timer = null;
782
783
  }
784
+ /** True while the last sync with `url` succeeded. */
785
+ linkUp(url) {
786
+ return this.links.get(url) === "up";
787
+ }
788
+ /** Forget everything learned about `url` (its broker left or moved). */
789
+ forget(url) {
790
+ this.links.delete(url);
791
+ this.origins.delete(url);
792
+ }
783
793
  /** Sync soon (coalesced). */
784
794
  kick() {
785
795
  if (this.running) this.again = true;
@@ -843,24 +853,6 @@ class Mesh {
843
853
  this.setLink(url, "up");
844
854
  }
845
855
  }
846
- function readMeshBrokers(configFile) {
847
- try {
848
- const parsed = JSON.parse(readFileSync(configFile, "utf8"));
849
- const list = Array.isArray(parsed.brokers) ? parsed.brokers : parsed.broker ? [parsed.broker] : [];
850
- const out = [];
851
- for (const s of list) {
852
- if (typeof s !== "string" || !s.trim()) continue;
853
- try {
854
- out.push(normalizeBrokerUrl(s));
855
- } catch {
856
- log.warn(`mesh: ignoring unparsable broker entry "${s}"`);
857
- }
858
- }
859
- return out;
860
- } catch {
861
- return [];
862
- }
863
- }
864
856
  function normalizeBrokerUrl(s) {
865
857
  s = s.trim();
866
858
  if (!/^https?:\/\//.test(s)) s = `http://${s}`;
@@ -882,6 +874,81 @@ function postJson(url, headers, body, timeoutMs) {
882
874
  });
883
875
  }
884
876
 
877
+ function meshExpireMs(announceMs) {
878
+ return announceMs * 3 + 5e3;
879
+ }
880
+ class MeshPeerTable {
881
+ constructor(opts) {
882
+ this.opts = opts;
883
+ this.now = opts.now ?? Date.now;
884
+ }
885
+ byId = /* @__PURE__ */ new Map();
886
+ mismatchLogged = /* @__PURE__ */ new Set();
887
+ now;
888
+ /** Current sync targets. */
889
+ urls() {
890
+ return [...this.byId.values()].map((e) => e.url);
891
+ }
892
+ entries() {
893
+ return [...this.byId.values()];
894
+ }
895
+ /** `announce` packet from `address` (UDP source). Returns true when accepted. */
896
+ handleAnnounce(m, address) {
897
+ if (m[DISCOVERY_KEY] !== DISCOVERY_ANNOUNCE_VALUE || m.mesh !== true) return false;
898
+ const ts = m.ts;
899
+ if (typeof ts !== "number" || !Number.isFinite(ts) || Math.abs(this.now() - ts) > MESH_ANNOUNCE_MAX_SKEW_MS) return false;
900
+ return this.accept(m, address, String(ts));
901
+ }
902
+ /** `broker` reply to our probe that carried `nonce`. Returns true when accepted. */
903
+ handleReply(m, address, nonce) {
904
+ if (m[DISCOVERY_KEY] !== DISCOVERY_REPLY_VALUE || m.mesh !== true) return false;
905
+ return this.accept(m, address, nonce);
906
+ }
907
+ accept(m, address, nonce) {
908
+ const { id, port, version, mac } = m;
909
+ if (typeof id !== "string" || !id || typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535) return false;
910
+ if (typeof mac !== "string") return false;
911
+ if (!meshMacEqual(meshMac(this.opts.token, nonce, id, port), mac)) {
912
+ const key = `${id}@${address}`;
913
+ if (!this.mismatchLogged.has(key)) {
914
+ this.mismatchLogged.add(key);
915
+ log.warn(`mesh: ignoring ${id}@${address} (token mismatch)`);
916
+ }
917
+ return false;
918
+ }
919
+ if (id === this.opts.selfId) return false;
920
+ const url = `http://${address}:${port}`;
921
+ const prev = this.byId.get(id);
922
+ const entry = { id, url, version: typeof version === "string" ? version : "", lastSeen: this.now() };
923
+ this.byId.set(id, entry);
924
+ if (!prev) {
925
+ log.info(`mesh: found ${id} at ${url}`);
926
+ this.opts.onJoin?.(entry);
927
+ } else if (prev.url !== url) {
928
+ log.info(`mesh: ${id} moved ${prev.url} -> ${url}`);
929
+ this.opts.onDrop?.(prev.url);
930
+ this.opts.onJoin?.(entry);
931
+ }
932
+ return true;
933
+ }
934
+ /**
935
+ * Drop entries not vouched for within `expireMs` whose last sync did not succeed (`linkUp`): a link that
936
+ * still syncs stays even when UDP is lost. Returns the removed entries.
937
+ */
938
+ sweep(linkUp) {
939
+ const gone = [];
940
+ const t = this.now();
941
+ for (const e of this.byId.values()) {
942
+ if (t - e.lastSeen <= this.opts.expireMs || linkUp(e.url)) continue;
943
+ this.byId.delete(e.id);
944
+ gone.push(e);
945
+ log.info(`mesh: ${e.url} left`);
946
+ this.opts.onDrop?.(e.url);
947
+ }
948
+ return gone;
949
+ }
950
+ }
951
+
885
952
  function createBrokerApp(opts) {
886
953
  const routes = opts.handlers.routes();
887
954
  const protocolRoutes = opts.handlers.protocolRoutes();
@@ -892,7 +959,8 @@ function createBrokerApp(opts) {
892
959
  id: opts.id,
893
960
  port: opts.port(),
894
961
  peers: opts.handlers.alivePeerCount(),
895
- ...opts.fallback ? { fallback: true } : {}
962
+ ...opts.fallback ? { fallback: true } : {},
963
+ ...opts.mesh ? { mesh: true } : {}
896
964
  })));
897
965
  const post = (route, run) => router.post(route, defineEventHandler(async (event) => {
898
966
  if (!authorized(event, opts.token)) return unauthorized(event);
@@ -936,12 +1004,16 @@ function startDiscoveryResponder(opts) {
936
1004
  const disco = createSocket({ type: "udp4", reuseAddr: true });
937
1005
  disco.on("error", (e) => log.warn(`discovery responder error: ${e.message}`));
938
1006
  disco.on("message", (msg, rinfo) => {
1007
+ let m;
939
1008
  try {
940
- if (JSON.parse(msg.toString())[DISCOVERY_KEY] !== DISCOVERY_PROBE_VALUE) return;
1009
+ m = JSON.parse(msg.toString());
941
1010
  } catch {
942
1011
  return;
943
1012
  }
944
- const reply = Buffer.from(JSON.stringify(opts.reply()));
1013
+ if (m[DISCOVERY_KEY] === DISCOVERY_ANNOUNCE_VALUE) return opts.onAnnounce?.(m, rinfo);
1014
+ if (m[DISCOVERY_KEY] !== DISCOVERY_PROBE_VALUE) return;
1015
+ const nonce = typeof m.n === "string" && m.n.length > 0 && m.n.length <= 128 ? m.n : void 0;
1016
+ const reply = Buffer.from(JSON.stringify(opts.reply(nonce)));
945
1017
  disco.send(reply, rinfo.port, rinfo.address, () => {
946
1018
  });
947
1019
  });
@@ -958,6 +1030,48 @@ function startDiscoveryResponder(opts) {
958
1030
  });
959
1031
  });
960
1032
  }
1033
+ function parseDiscoveryTargets(list) {
1034
+ const out = [];
1035
+ for (const s of list) {
1036
+ const i = s.lastIndexOf(":");
1037
+ const port = Number(s.slice(i + 1));
1038
+ if (i <= 0 || !Number.isInteger(port) || port < 1 || port > 65535) {
1039
+ log.warn(`discovery: ignoring invalid target "${s}" (want host:port)`);
1040
+ continue;
1041
+ }
1042
+ out.push({ host: s.slice(0, i), port });
1043
+ }
1044
+ return out;
1045
+ }
1046
+ function sendTargets(opts) {
1047
+ const explicit = parseDiscoveryTargets(opts.targets ?? []);
1048
+ if (explicit.length) return explicit;
1049
+ return probeTargets(opts.addr).map((host) => ({ host, port: opts.port }));
1050
+ }
1051
+ function buildAnnounce(a) {
1052
+ return {
1053
+ [DISCOVERY_KEY]: DISCOVERY_ANNOUNCE_VALUE,
1054
+ id: a.id,
1055
+ port: a.port,
1056
+ version: a.version,
1057
+ mesh: true,
1058
+ ts: a.ts,
1059
+ mac: meshMac(a.token, String(a.ts), a.id, a.port)
1060
+ };
1061
+ }
1062
+ function sendAnnounce(sock, packet, targets) {
1063
+ const buf = Buffer.from(JSON.stringify(packet));
1064
+ for (const t of targets) {
1065
+ try {
1066
+ sock.send(buf, t.port, t.host, () => {
1067
+ });
1068
+ } catch {
1069
+ }
1070
+ }
1071
+ }
1072
+ function newNonce() {
1073
+ return randomBytes(16).toString("hex");
1074
+ }
961
1075
  function probeTargets(addr) {
962
1076
  if (addr) return [addr];
963
1077
  const targets = /* @__PURE__ */ new Set(["255.255.255.255"]);
@@ -1001,7 +1115,16 @@ function probeBrokers(opts) {
1001
1115
  if (m[DISCOVERY_KEY] !== DISCOVERY_REPLY_VALUE || !m.port) return;
1002
1116
  const url = `http://${rinfo.address}:${m.port}`;
1003
1117
  if (!found.has(url)) {
1004
- found.set(url, { url, host: rinfo.address, port: m.port, id: m.id ?? rinfo.address, version: m.version ?? "", fallback: m.fallback === true });
1118
+ found.set(url, {
1119
+ url,
1120
+ host: rinfo.address,
1121
+ port: m.port,
1122
+ id: m.id ?? rinfo.address,
1123
+ version: m.version ?? "",
1124
+ fallback: m.fallback === true,
1125
+ mesh: m.mesh === true,
1126
+ ...typeof m.mac === "string" ? { mac: m.mac } : {}
1127
+ });
1005
1128
  }
1006
1129
  } catch {
1007
1130
  }
@@ -1011,8 +1134,8 @@ function probeBrokers(opts) {
1011
1134
  sock.setBroadcast(true);
1012
1135
  } catch {
1013
1136
  }
1014
- const probe = Buffer.from(JSON.stringify({ [DISCOVERY_KEY]: DISCOVERY_PROBE_VALUE }));
1015
- for (const t of probeTargets(opts.addr)) sock.send(probe, opts.port, t, () => {
1137
+ const probe = Buffer.from(JSON.stringify({ [DISCOVERY_KEY]: DISCOVERY_PROBE_VALUE, ...opts.nonce ? { n: opts.nonce } : {} }));
1138
+ for (const t of sendTargets(opts)) sock.send(probe, t.port, t.host, () => {
1016
1139
  });
1017
1140
  });
1018
1141
  });
@@ -1068,7 +1191,7 @@ async function relayUndelivered(state, url, token) {
1068
1191
  return { sent, dropped };
1069
1192
  }
1070
1193
 
1071
- const version = "0.10.0";
1194
+ const version = "0.11.0";
1072
1195
  const pkg = {
1073
1196
  version: version};
1074
1197
 
@@ -1114,21 +1237,61 @@ async function startBroker(options = {}) {
1114
1237
  throw e;
1115
1238
  }
1116
1239
  port = listener.address.port;
1117
- const reply = () => ({ [DISCOVERY_KEY]: DISCOVERY_REPLY_VALUE, id: cfg.id, port, version: VERSION, ...cfg.fallback ? { fallback: true } : {} });
1240
+ const reply = (nonce) => ({
1241
+ [DISCOVERY_KEY]: DISCOVERY_REPLY_VALUE,
1242
+ id: cfg.id,
1243
+ port,
1244
+ version: VERSION,
1245
+ ...cfg.fallback ? { fallback: true } : {},
1246
+ ...cfg.mesh ? { mesh: true, ...nonce ? { mac: meshMac(cfg.token, nonce, cfg.id, port) } : {} } : {}
1247
+ });
1248
+ const fixed = cfg.meshBrokers.map(normalizeBrokerUrl);
1249
+ const peerTable = cfg.mesh ? new MeshPeerTable({
1250
+ selfId: cfg.id,
1251
+ token: cfg.token,
1252
+ expireMs: meshExpireMs(cfg.meshAnnounceMs),
1253
+ onDrop: (url) => mesh?.forget(url),
1254
+ onJoin: () => mesh?.kick()
1255
+ }) : null;
1118
1256
  let disco = null;
1119
1257
  if (cfg.discovery) {
1120
- disco = await startDiscoveryResponder({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, reply });
1258
+ disco = await startDiscoveryResponder({
1259
+ port: cfg.discoveryPort,
1260
+ addr: cfg.discoveryAddr,
1261
+ reply,
1262
+ ...peerTable ? { onAnnounce: (m, rinfo) => void peerTable.handleAnnounce(m, rinfo.address) } : {}
1263
+ });
1121
1264
  }
1265
+ let announceTimer = null;
1122
1266
  if (cfg.mesh) {
1123
- const fixed = cfg.meshBrokers.map(normalizeBrokerUrl);
1124
1267
  mesh = new Mesh({
1125
1268
  id: cfg.id,
1126
1269
  token: cfg.token,
1127
1270
  handlers,
1128
- urls: () => fixed.length ? fixed : readMeshBrokers(cfg.configFile),
1271
+ urls: () => fixed.length ? fixed : peerTable.urls(),
1129
1272
  intervalMs: cfg.meshIntervalMs
1130
1273
  });
1131
1274
  mesh.start();
1275
+ if (!cfg.discovery && !fixed.length) {
1276
+ log.warn("mesh: AGENT_MESSENGER_MESH=on but AGENT_MESSENGER_DISCOVERY=off; no other mesh broker can be found");
1277
+ }
1278
+ if (cfg.discovery && disco && !fixed.length) {
1279
+ const sock = disco;
1280
+ const targets = sendTargets({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets });
1281
+ const announceAndSweep = () => {
1282
+ sendAnnounce(sock, buildAnnounce({ token: cfg.token, id: cfg.id, port, version: VERSION, ts: Date.now() }), targets);
1283
+ peerTable.sweep((url) => mesh?.linkUp(url) ?? false);
1284
+ };
1285
+ announceAndSweep();
1286
+ announceTimer = setInterval(announceAndSweep, cfg.meshAnnounceMs);
1287
+ announceTimer.unref();
1288
+ const nonce = newNonce();
1289
+ void probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets, nonce, timeoutMs: 1e3 }).then((found) => {
1290
+ for (const b of found) {
1291
+ peerTable.handleReply({ [DISCOVERY_KEY]: DISCOVERY_REPLY_VALUE, id: b.id, port: b.port, version: b.version, mesh: b.mesh, mac: b.mac }, b.host, nonce);
1292
+ }
1293
+ });
1294
+ }
1132
1295
  }
1133
1296
  log.info(`v${VERSION} id=${cfg.id} listening on ${cfg.bind}:${port} (state: ${cfg.stateFile})`);
1134
1297
  if (cfg.fallback) log.info(`FALLBACK mode: looking for another broker every ${cfg.fallbackIntervalMs} ms (discovery ${cfg.discovery ? "on" : "off"})`);
@@ -1140,6 +1303,7 @@ async function startBroker(options = {}) {
1140
1303
  closed = true;
1141
1304
  clearInterval(cleaner);
1142
1305
  if (fallbackTimer) clearInterval(fallbackTimer);
1306
+ if (announceTimer) clearInterval(announceTimer);
1143
1307
  mesh?.stop();
1144
1308
  if (disco) {
1145
1309
  await new Promise((resolve) => disco.close(() => resolve()));
@@ -1154,7 +1318,7 @@ async function startBroker(options = {}) {
1154
1318
  if (busy || closed) return;
1155
1319
  busy = true;
1156
1320
  try {
1157
- const found = await probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, timeoutMs: 1e3 });
1321
+ const found = await probeBrokers({ port: cfg.discoveryPort, addr: cfg.discoveryAddr, targets: cfg.discoveryTargets, timeoutMs: 1e3 });
1158
1322
  const other = found.find((b) => !b.fallback && !(b.port === port && mine.has(b.host)));
1159
1323
  if (!other || closed) return;
1160
1324
  log.info(`fallback: found broker ${other.id} at ${other.url} \u2014 relaying messages and exiting`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unagents/messenger-broker",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "agent-messenger broker: HTTP daemon that tracks peers and routes messages between Claude Code sessions.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "test": "vitest run"
29
29
  },
30
30
  "dependencies": {
31
- "@unagents/messenger-protocol": "0.10.0",
31
+ "@unagents/messenger-protocol": "0.11.0",
32
32
  "consola": "^3.4.2",
33
33
  "defu": "^6.1.7",
34
34
  "h3": "^1.15.11",