@openclaw/buzz 2026.8.1-beta.3 → 2026.9.1-beta.1

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.
Files changed (134) hide show
  1. package/dist/{adapter.runtime-B0130te-.js → adapter.runtime-DA6guOQ_.js} +4 -4
  2. package/dist/api.js +1 -1
  3. package/dist/{channel-Bg5qZukN.js → channel-BulZqShF.js} +215 -252
  4. package/dist/channel-plugin-api.js +1 -1
  5. package/dist/qa-runner-api.js +2 -2
  6. package/dist/{room-membership-CRuRK8PE.js → room-membership-DSAkyE0a.js} +54 -4
  7. package/dist/setup-plugin-api.js +1 -1
  8. package/dist/{setup-surface-sJVYSY73.js → setup-surface-BvQgEJ7O.js} +19 -41
  9. package/node_modules/nostr-tools/lib/cjs/abstract-pool.js +7 -5
  10. package/node_modules/nostr-tools/lib/cjs/abstract-pool.js.map +2 -2
  11. package/node_modules/nostr-tools/lib/cjs/abstract-relay.js +6 -4
  12. package/node_modules/nostr-tools/lib/cjs/abstract-relay.js.map +2 -2
  13. package/node_modules/nostr-tools/lib/cjs/core.js +10 -6
  14. package/node_modules/nostr-tools/lib/cjs/core.js.map +2 -2
  15. package/node_modules/nostr-tools/lib/cjs/index.js +17 -11
  16. package/node_modules/nostr-tools/lib/cjs/index.js.map +2 -2
  17. package/node_modules/nostr-tools/lib/cjs/kinds.js +2 -0
  18. package/node_modules/nostr-tools/lib/cjs/kinds.js.map +2 -2
  19. package/node_modules/nostr-tools/lib/cjs/nip04.js.map +1 -1
  20. package/node_modules/nostr-tools/lib/cjs/nip10.js +2 -0
  21. package/node_modules/nostr-tools/lib/cjs/nip10.js.map +2 -2
  22. package/node_modules/nostr-tools/lib/cjs/nip13.js.map +1 -1
  23. package/node_modules/nostr-tools/lib/cjs/nip17.js +2 -0
  24. package/node_modules/nostr-tools/lib/cjs/nip17.js.map +2 -2
  25. package/node_modules/nostr-tools/lib/cjs/nip18.js +2 -0
  26. package/node_modules/nostr-tools/lib/cjs/nip18.js.map +2 -2
  27. package/node_modules/nostr-tools/lib/cjs/nip19.js.map +1 -1
  28. package/node_modules/nostr-tools/lib/cjs/nip21.js.map +1 -1
  29. package/node_modules/nostr-tools/lib/cjs/nip22.js +2 -0
  30. package/node_modules/nostr-tools/lib/cjs/nip22.js.map +2 -2
  31. package/node_modules/nostr-tools/lib/cjs/nip25.js +2 -0
  32. package/node_modules/nostr-tools/lib/cjs/nip25.js.map +2 -2
  33. package/node_modules/nostr-tools/lib/cjs/nip27.js.map +1 -1
  34. package/node_modules/nostr-tools/lib/cjs/nip28.js +2 -0
  35. package/node_modules/nostr-tools/lib/cjs/nip28.js.map +2 -2
  36. package/node_modules/nostr-tools/lib/cjs/nip29.js +288 -12
  37. package/node_modules/nostr-tools/lib/cjs/nip29.js.map +2 -2
  38. package/node_modules/nostr-tools/lib/cjs/nip44.js.map +1 -1
  39. package/node_modules/nostr-tools/lib/cjs/nip45.js +2 -0
  40. package/node_modules/nostr-tools/lib/cjs/nip45.js.map +2 -2
  41. package/node_modules/nostr-tools/lib/cjs/nip46.js +9 -5
  42. package/node_modules/nostr-tools/lib/cjs/nip46.js.map +2 -2
  43. package/node_modules/nostr-tools/lib/cjs/nip47.js +2 -0
  44. package/node_modules/nostr-tools/lib/cjs/nip47.js.map +2 -2
  45. package/node_modules/nostr-tools/lib/cjs/nip57.js +2 -0
  46. package/node_modules/nostr-tools/lib/cjs/nip57.js.map +2 -2
  47. package/node_modules/nostr-tools/lib/cjs/nip59.js +2 -0
  48. package/node_modules/nostr-tools/lib/cjs/nip59.js.map +2 -2
  49. package/node_modules/nostr-tools/lib/cjs/nip98.js +2 -0
  50. package/node_modules/nostr-tools/lib/cjs/nip98.js.map +2 -2
  51. package/node_modules/nostr-tools/lib/cjs/nipb0.js +2 -0
  52. package/node_modules/nostr-tools/lib/cjs/nipb0.js.map +2 -2
  53. package/node_modules/nostr-tools/lib/cjs/nipb7.js +2 -0
  54. package/node_modules/nostr-tools/lib/cjs/nipb7.js.map +2 -2
  55. package/node_modules/nostr-tools/lib/cjs/pool.js +9 -5
  56. package/node_modules/nostr-tools/lib/cjs/pool.js.map +2 -2
  57. package/node_modules/nostr-tools/lib/cjs/pure.js +10 -6
  58. package/node_modules/nostr-tools/lib/cjs/pure.js.map +2 -2
  59. package/node_modules/nostr-tools/lib/cjs/references.js.map +1 -1
  60. package/node_modules/nostr-tools/lib/cjs/relay.js +8 -4
  61. package/node_modules/nostr-tools/lib/cjs/relay.js.map +2 -2
  62. package/node_modules/nostr-tools/lib/cjs/signer.js +2 -0
  63. package/node_modules/nostr-tools/lib/cjs/signer.js.map +2 -2
  64. package/node_modules/nostr-tools/lib/cjs/utils.js +2 -0
  65. package/node_modules/nostr-tools/lib/cjs/utils.js.map +2 -2
  66. package/node_modules/nostr-tools/lib/cjs/wasm.js +10 -6
  67. package/node_modules/nostr-tools/lib/cjs/wasm.js.map +2 -2
  68. package/node_modules/nostr-tools/lib/esm/abstract-pool.js +7 -5
  69. package/node_modules/nostr-tools/lib/esm/abstract-pool.js.map +2 -2
  70. package/node_modules/nostr-tools/lib/esm/abstract-relay.js +6 -4
  71. package/node_modules/nostr-tools/lib/esm/abstract-relay.js.map +2 -2
  72. package/node_modules/nostr-tools/lib/esm/core.js +10 -6
  73. package/node_modules/nostr-tools/lib/esm/core.js.map +2 -2
  74. package/node_modules/nostr-tools/lib/esm/index.js +17 -11
  75. package/node_modules/nostr-tools/lib/esm/index.js.map +2 -2
  76. package/node_modules/nostr-tools/lib/esm/kinds.js +2 -0
  77. package/node_modules/nostr-tools/lib/esm/kinds.js.map +2 -2
  78. package/node_modules/nostr-tools/lib/esm/nip04.js.map +1 -1
  79. package/node_modules/nostr-tools/lib/esm/nip10.js +2 -0
  80. package/node_modules/nostr-tools/lib/esm/nip10.js.map +2 -2
  81. package/node_modules/nostr-tools/lib/esm/nip13.js.map +1 -1
  82. package/node_modules/nostr-tools/lib/esm/nip17.js +2 -0
  83. package/node_modules/nostr-tools/lib/esm/nip17.js.map +2 -2
  84. package/node_modules/nostr-tools/lib/esm/nip18.js +2 -0
  85. package/node_modules/nostr-tools/lib/esm/nip18.js.map +2 -2
  86. package/node_modules/nostr-tools/lib/esm/nip19.js.map +1 -1
  87. package/node_modules/nostr-tools/lib/esm/nip21.js.map +1 -1
  88. package/node_modules/nostr-tools/lib/esm/nip22.js +2 -0
  89. package/node_modules/nostr-tools/lib/esm/nip22.js.map +2 -2
  90. package/node_modules/nostr-tools/lib/esm/nip25.js +2 -0
  91. package/node_modules/nostr-tools/lib/esm/nip25.js.map +2 -2
  92. package/node_modules/nostr-tools/lib/esm/nip27.js.map +1 -1
  93. package/node_modules/nostr-tools/lib/esm/nip28.js +2 -0
  94. package/node_modules/nostr-tools/lib/esm/nip28.js.map +2 -2
  95. package/node_modules/nostr-tools/lib/esm/nip29.js +288 -12
  96. package/node_modules/nostr-tools/lib/esm/nip29.js.map +2 -2
  97. package/node_modules/nostr-tools/lib/esm/nip44.js.map +1 -1
  98. package/node_modules/nostr-tools/lib/esm/nip45.js +2 -0
  99. package/node_modules/nostr-tools/lib/esm/nip45.js.map +2 -2
  100. package/node_modules/nostr-tools/lib/esm/nip46.js +9 -5
  101. package/node_modules/nostr-tools/lib/esm/nip46.js.map +2 -2
  102. package/node_modules/nostr-tools/lib/esm/nip47.js +2 -0
  103. package/node_modules/nostr-tools/lib/esm/nip47.js.map +2 -2
  104. package/node_modules/nostr-tools/lib/esm/nip57.js +2 -0
  105. package/node_modules/nostr-tools/lib/esm/nip57.js.map +2 -2
  106. package/node_modules/nostr-tools/lib/esm/nip59.js +2 -0
  107. package/node_modules/nostr-tools/lib/esm/nip59.js.map +2 -2
  108. package/node_modules/nostr-tools/lib/esm/nip98.js +2 -0
  109. package/node_modules/nostr-tools/lib/esm/nip98.js.map +2 -2
  110. package/node_modules/nostr-tools/lib/esm/nipb0.js +2 -0
  111. package/node_modules/nostr-tools/lib/esm/nipb0.js.map +2 -2
  112. package/node_modules/nostr-tools/lib/esm/nipb7.js +2 -0
  113. package/node_modules/nostr-tools/lib/esm/nipb7.js.map +2 -2
  114. package/node_modules/nostr-tools/lib/esm/pool.js +9 -5
  115. package/node_modules/nostr-tools/lib/esm/pool.js.map +2 -2
  116. package/node_modules/nostr-tools/lib/esm/pure.js +10 -6
  117. package/node_modules/nostr-tools/lib/esm/pure.js.map +2 -2
  118. package/node_modules/nostr-tools/lib/esm/references.js.map +1 -1
  119. package/node_modules/nostr-tools/lib/esm/relay.js +8 -4
  120. package/node_modules/nostr-tools/lib/esm/relay.js.map +2 -2
  121. package/node_modules/nostr-tools/lib/esm/signer.js +2 -0
  122. package/node_modules/nostr-tools/lib/esm/signer.js.map +2 -2
  123. package/node_modules/nostr-tools/lib/esm/utils.js +2 -0
  124. package/node_modules/nostr-tools/lib/esm/utils.js.map +2 -2
  125. package/node_modules/nostr-tools/lib/esm/wasm.js +10 -6
  126. package/node_modules/nostr-tools/lib/esm/wasm.js.map +2 -2
  127. package/node_modules/nostr-tools/lib/nostr.bundle.js +17 -11
  128. package/node_modules/nostr-tools/lib/nostr.bundle.js.map +2 -2
  129. package/node_modules/nostr-tools/lib/types/core.d.ts +5 -0
  130. package/node_modules/nostr-tools/lib/types/nip29.d.ts +243 -3
  131. package/node_modules/nostr-tools/lib/types/nip29.test.d.ts +1 -0
  132. package/node_modules/nostr-tools/package.json +1 -1
  133. package/openclaw.plugin.json +24 -0
  134. package/package.json +5 -5
@@ -1,7 +1,7 @@
1
1
  import { a as resolveBuzzPublicKey, c as buildBuzzTarget, d as normalizeBuzzTarget, f as parseBuzzTarget, i as resolveBuzzAccount, l as isConfiguredBuzzChannel, n as decodeBuzzPrivateKey, o as resolveDefaultBuzzAccountId, r as listBuzzAccountIds, t as assertBuzzAccountAvailable, u as looksLikeBuzzTarget } from "./types-BN_HOsnq.js";
2
- import { a as BuzzConfigSchema, i as startBuzzRoomMembershipNotifications, n as buzzSetupContract, r as discoverBuzzRooms, t as buzzSetupWizard } from "./setup-surface-sJVYSY73.js";
2
+ import { a as BuzzConfigSchema, i as startBuzzRoomMembershipNotifications, n as buzzSetupContract, r as discoverBuzzRooms, t as buzzSetupWizard } from "./setup-surface-BvQgEJ7O.js";
3
3
  import { n as listBuzzDirectoryPeersFromConfig, t as listBuzzDirectoryGroupsFromConfig } from "./directory-config-BesCse0q.js";
4
- import { a as parseBuzzRoomMembershipEvent, c as parseBuzzAuthTag, i as parseBuzzRoomMembershipChangeEvent, l as openBuzzRelaySubscription, n as BUZZ_ROOM_SYSTEM_KIND, o as connectAuthenticatedBuzzRelay, r as isNewerBuzzRoomMembership, s as connectAuthenticatedBuzzRelaySession, t as BUZZ_ROOM_MEMBERSHIP_KIND } from "./room-membership-CRuRK8PE.js";
4
+ import { a as parseBuzzRoomMembershipEvent, c as parseBuzzAuthTag, i as parseBuzzRoomMembershipChangeEvent, l as openBuzzRelaySubscription, n as BUZZ_ROOM_SYSTEM_KIND, o as connectAuthenticatedBuzzRelay, r as isNewerBuzzRoomMembership, s as connectAuthenticatedBuzzRelaySession, t as BUZZ_ROOM_MEMBERSHIP_KIND, u as queryBuzzRelaySnapshot } from "./room-membership-DSAkyE0a.js";
5
5
  import { a as formatBuzzMessageForAgent, c as inspectBuzzMentionSyntax, i as buildBuzzMessageTags, l as resolveBuzzMessageMentions, n as BUZZ_INBOUND_MESSAGE_KINDS, o as isBuzzInboundMessageKind, r as BUZZ_TYPING_INDICATOR_KIND, s as parseBuzzMessageEvent, t as BUZZ_DIFF_MESSAGE_KIND } from "./message-event-CFw7gGLi.js";
6
6
  import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries } from "./secret-contract-D19RiJv_.js";
7
7
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
@@ -21,6 +21,7 @@ import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope } f
21
21
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
22
22
  import { createSubsystemLogger } from "openclaw/plugin-sdk/logging-core";
23
23
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
24
+ import { createHash } from "node:crypto";
24
25
  const BUZZ_ROOM_METADATA_KIND = 39e3;
25
26
  const DEFAULT_BUZZ_DIRECTORY_PROFILE_LIMIT = 2e3;
26
27
  const HEX_PUBLIC_KEY_PATTERN = /^[0-9a-f]{64}$/u;
@@ -251,49 +252,23 @@ const PROFILE_SUBSCRIPTION_REPLACED_REASON = "directory profile subscription rep
251
252
  const PROFILE_SUBSCRIPTION_FAILED_REASON = "directory profile subscription generation failed";
252
253
  const DIRECTORY_SHUTDOWN_REASON = "directory shutdown";
253
254
  const DIRECTORY_QUERY_COMPLETE_REASON = "directory query complete";
254
- const DIRECTORY_QUERY_TIMEOUT_MS = 1e4;
255
255
  const PROFILE_SUBSCRIPTION_READY_TIMEOUT_MS = 1e4;
256
256
  async function queryBuzzDirectoryBatch(params) {
257
257
  params.signal?.throwIfAborted();
258
- await new Promise((resolve, reject) => {
259
- let settled = false;
260
- let receivedEose = false;
261
- const timeout = setTimeout(() => {
262
- const error = /* @__PURE__ */ new Error("Timed out loading Buzz directory snapshot");
263
- finish(error);
264
- if (params.onTimeout) params.onTimeout(error);
265
- else params.relay.close();
266
- }, DIRECTORY_QUERY_TIMEOUT_MS);
267
- const subscriptionRef = {};
268
- const finish = (error) => {
269
- if (settled) return;
270
- settled = true;
271
- clearTimeout(timeout);
272
- params.signal?.removeEventListener("abort", onAbort);
273
- if (receivedEose) subscriptionRef.current?.close(DIRECTORY_QUERY_COMPLETE_REASON);
274
- if (error === void 0) resolve();
275
- else reject(error instanceof Error ? error : new Error("Buzz directory query failed", { cause: error }));
276
- };
277
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz directory query aborted"));
278
- params.signal?.addEventListener("abort", onAbort, { once: true });
279
- try {
280
- subscriptionRef.current = openBuzzRelaySubscription(params.relay, [params.filter], {
281
- onevent: params.onEvent,
282
- oneose: () => {
283
- receivedEose = true;
284
- if (settled) subscriptionRef.current?.close(DIRECTORY_QUERY_COMPLETE_REASON);
285
- else finish();
286
- },
287
- onclose: (reason) => {
288
- if (reason !== DIRECTORY_QUERY_COMPLETE_REASON) finish(/* @__PURE__ */ new Error(`Buzz directory query closed: ${reason}`));
289
- }
290
- });
291
- } catch (error) {
292
- finish(error);
293
- return;
294
- }
295
- if (settled && receivedEose) subscriptionRef.current.close(DIRECTORY_QUERY_COMPLETE_REASON);
296
- if (params.signal?.aborted) onAbort();
258
+ await queryBuzzRelaySnapshot({
259
+ relay: params.relay,
260
+ filters: [params.filter],
261
+ signal: params.signal,
262
+ timeoutMessage: "Timed out loading Buzz directory snapshot",
263
+ abortMessage: "Buzz directory query aborted",
264
+ failureMessage: "Buzz directory query failed",
265
+ closeReason: DIRECTORY_QUERY_COMPLETE_REASON,
266
+ closeMessage: (reason) => `Buzz directory query closed: ${reason}`,
267
+ onEvent: params.onEvent,
268
+ result: () => {},
269
+ onTimeout: params.onTimeout,
270
+ closeRelayOnTimeout: !params.onTimeout,
271
+ checkAbortAfterSubscribe: true
297
272
  });
298
273
  }
299
274
  async function queryBuzzDirectoryProfiles(params) {
@@ -469,7 +444,6 @@ const CHANNEL_ADD_POLICIES = /* @__PURE__ */ new Set([
469
444
  "owner_only",
470
445
  "nobody"
471
446
  ]);
472
- const PROFILE_QUERY_TIMEOUT_MS = 1e4;
473
447
  function parseProfileContent(event) {
474
448
  if (!event) return {};
475
449
  try {
@@ -495,33 +469,10 @@ function readNonEmptyString(content, key) {
495
469
  }
496
470
  async function queryCurrentProfiles(params) {
497
471
  params.signal?.throwIfAborted();
498
- return await new Promise((resolve, reject) => {
499
- const latestByKind = /* @__PURE__ */ new Map();
500
- const state = {
501
- settled: false,
502
- receivedEose: false
503
- };
504
- const timeout = setTimeout(() => {
505
- const error = /* @__PURE__ */ new Error("Timed out loading current Buzz profile");
506
- finish(error);
507
- params.onTimeout?.(error);
508
- params.relay.close();
509
- }, PROFILE_QUERY_TIMEOUT_MS);
510
- const finish = (error) => {
511
- if (state.settled) return;
512
- state.settled = true;
513
- clearTimeout(timeout);
514
- params.signal?.removeEventListener("abort", onAbort);
515
- if (state.receivedEose) state.subscription?.close("profile query complete");
516
- if (error !== void 0) {
517
- reject(error instanceof Error ? error : new Error("Buzz profile query failed", { cause: error }));
518
- return;
519
- }
520
- resolve(latestByKind);
521
- };
522
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz profile query aborted"));
523
- params.signal?.addEventListener("abort", onAbort, { once: true });
524
- state.subscription = openBuzzRelaySubscription(params.relay, [{
472
+ const latestByKind = /* @__PURE__ */ new Map();
473
+ return await queryBuzzRelaySnapshot({
474
+ relay: params.relay,
475
+ filters: [{
525
476
  kinds: [PROFILE_KIND],
526
477
  authors: [params.publicKey],
527
478
  limit: 1
@@ -529,21 +480,19 @@ async function queryCurrentProfiles(params) {
529
480
  kinds: [AGENT_PROFILE_KIND],
530
481
  authors: [params.publicKey],
531
482
  limit: 1
532
- }], {
533
- onevent: (event) => {
534
- const current = latestByKind.get(event.kind);
535
- if (!current || event.created_at > current.created_at) latestByKind.set(event.kind, event);
536
- },
537
- oneose: () => {
538
- state.receivedEose = true;
539
- if (state.settled) state.subscription?.close("profile query complete");
540
- else finish();
541
- },
542
- onclose: (reason) => {
543
- if (reason !== "profile query complete") finish(/* @__PURE__ */ new Error(`Buzz profile query closed: ${reason}`));
544
- }
545
- });
546
- if (state.settled && state.receivedEose) state.subscription.close("profile query complete");
483
+ }],
484
+ signal: params.signal,
485
+ timeoutMessage: "Timed out loading current Buzz profile",
486
+ abortMessage: "Buzz profile query aborted",
487
+ failureMessage: "Buzz profile query failed",
488
+ closeReason: "profile query complete",
489
+ closeMessage: (reason) => `Buzz profile query closed: ${reason}`,
490
+ onEvent: (event) => {
491
+ const current = latestByKind.get(event.kind);
492
+ if (!current || event.created_at > current.created_at) latestByKind.set(event.kind, event);
493
+ },
494
+ result: () => latestByKind,
495
+ onTimeout: params.onTimeout
547
496
  });
548
497
  }
549
498
  function buildProfileEvent(params) {
@@ -729,56 +678,29 @@ function resolveBuzzRoomHistoryLimit(roomCount) {
729
678
  //#region extensions/buzz/src/room-membership-query.ts
730
679
  const RELAY_QUERY_EVENT_LIMIT = 1e3;
731
680
  const MEMBERSHIP_QUERY_COMPLETE_REASON = "membership snapshot loaded";
732
- const MEMBERSHIP_QUERY_TIMEOUT_MS = 1e4;
733
681
  async function queryBuzzRoomMembershipBatch(params) {
734
682
  const configuredRooms = new Set(params.channelIds);
735
683
  const memberships = /* @__PURE__ */ new Map();
736
- return await new Promise((resolve, reject) => {
737
- let settled = false;
738
- let receivedEose = false;
739
- const timeout = setTimeout(() => {
740
- finish(/* @__PURE__ */ new Error("Timed out loading Buzz room membership snapshot"));
741
- params.relay.close();
742
- }, MEMBERSHIP_QUERY_TIMEOUT_MS);
743
- const subscriptionRef = {};
744
- const finish = (error) => {
745
- if (settled) return;
746
- settled = true;
747
- clearTimeout(timeout);
748
- params.signal?.removeEventListener("abort", onAbort);
749
- if (receivedEose) subscriptionRef.current?.close(MEMBERSHIP_QUERY_COMPLETE_REASON);
750
- if (error === void 0) resolve(memberships);
751
- else reject(error instanceof Error ? error : new Error("Buzz room membership query failed", { cause: error }));
752
- };
753
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz room membership query aborted"));
754
- params.signal?.addEventListener("abort", onAbort, { once: true });
755
- try {
756
- subscriptionRef.current = openBuzzRelaySubscription(params.relay, [{
757
- kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
758
- authors: [params.relayPublicKey],
759
- "#d": params.channelIds,
760
- limit: params.channelIds.length
761
- }], {
762
- onevent: (event) => {
763
- const membership = parseBuzzRoomMembershipEvent(event, params.relayPublicKey);
764
- if (!membership || !configuredRooms.has(membership.roomId) || !isNewerBuzzRoomMembership(membership, memberships.get(membership.roomId))) return;
765
- memberships.set(membership.roomId, membership);
766
- },
767
- oneose: () => {
768
- receivedEose = true;
769
- if (settled) subscriptionRef.current?.close(MEMBERSHIP_QUERY_COMPLETE_REASON);
770
- else finish();
771
- },
772
- onclose: (reason) => {
773
- if (reason !== MEMBERSHIP_QUERY_COMPLETE_REASON) finish(/* @__PURE__ */ new Error(`Buzz room membership query closed: ${reason}`));
774
- }
775
- });
776
- } catch (error) {
777
- finish(error);
778
- return;
779
- }
780
- if (settled && receivedEose) subscriptionRef.current.close(MEMBERSHIP_QUERY_COMPLETE_REASON);
781
- if (params.signal?.aborted) onAbort();
684
+ return await queryBuzzRelaySnapshot({
685
+ relay: params.relay,
686
+ filters: [{
687
+ kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
688
+ authors: [params.relayPublicKey],
689
+ "#d": params.channelIds,
690
+ limit: params.channelIds.length
691
+ }],
692
+ signal: params.signal,
693
+ timeoutMessage: "Timed out loading Buzz room membership snapshot",
694
+ abortMessage: "Buzz room membership query aborted",
695
+ failureMessage: "Buzz room membership query failed",
696
+ closeReason: MEMBERSHIP_QUERY_COMPLETE_REASON,
697
+ closeMessage: (reason) => `Buzz room membership query closed: ${reason}`,
698
+ onEvent: (event) => {
699
+ const membership = parseBuzzRoomMembershipEvent(event, params.relayPublicKey);
700
+ if (membership && configuredRooms.has(membership.roomId) && isNewerBuzzRoomMembership(membership, memberships.get(membership.roomId))) memberships.set(membership.roomId, membership);
701
+ },
702
+ result: () => memberships,
703
+ checkAbortAfterSubscribe: true
782
704
  });
783
705
  }
784
706
  async function queryBuzzRoomMemberships(params) {
@@ -794,62 +716,35 @@ async function queryBuzzRoomMemberships(params) {
794
716
  }
795
717
  //#endregion
796
718
  //#region extensions/buzz/src/history-catchup.ts
797
- const HISTORY_PAGE_TIMEOUT_MS = 1e4;
798
719
  const HISTORY_PAGE_COMPLETE_REASON = "buzz room history page loaded";
799
720
  async function queryBuzzRoomHistoryPage(params) {
800
721
  const events = [];
801
722
  let overLimit = false;
802
- return await new Promise((resolve, reject) => {
803
- let settled = false;
804
- let receivedEose = false;
805
- const timeout = setTimeout(() => {
806
- const error = /* @__PURE__ */ new Error(`Timed out loading Buzz room history for ${params.channelId}`);
807
- finish(error);
808
- params.relay.close();
809
- }, HISTORY_PAGE_TIMEOUT_MS);
810
- const subscriptionRef = {};
811
- const finish = (error) => {
812
- if (settled) return;
813
- settled = true;
814
- clearTimeout(timeout);
815
- params.signal?.removeEventListener("abort", onAbort);
816
- if (receivedEose) subscriptionRef.current?.close(HISTORY_PAGE_COMPLETE_REASON);
817
- if (error === void 0) resolve({
818
- events,
819
- overLimit
820
- });
821
- else reject(error instanceof Error ? error : new Error("Buzz room history query failed", { cause: error }));
822
- };
823
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz room history query aborted"));
824
- params.signal?.addEventListener("abort", onAbort, { once: true });
825
- try {
826
- subscriptionRef.current = openBuzzRelaySubscription(params.relay, [{
827
- kinds: [...BUZZ_INBOUND_MESSAGE_KINDS],
828
- "#h": [params.channelId],
829
- since: params.since,
830
- until: params.until,
831
- ...params.requestLimit === void 0 ? {} : { limit: params.requestLimit }
832
- }], {
833
- onevent: (event) => {
834
- if (params.skipEventIds?.has(event.id)) return;
835
- if (events.length < params.maxEvents) events.push(event);
836
- else overLimit = true;
837
- },
838
- oneose: () => {
839
- receivedEose = true;
840
- if (settled) subscriptionRef.current?.close(HISTORY_PAGE_COMPLETE_REASON);
841
- else finish();
842
- },
843
- onclose: (reason) => {
844
- if (reason !== HISTORY_PAGE_COMPLETE_REASON) finish(/* @__PURE__ */ new Error(`Buzz room history query closed for ${params.channelId}: ${reason}`));
845
- }
846
- });
847
- } catch (error) {
848
- finish(error);
849
- return;
850
- }
851
- if (settled && receivedEose) subscriptionRef.current.close(HISTORY_PAGE_COMPLETE_REASON);
852
- if (params.signal?.aborted) onAbort();
723
+ return await queryBuzzRelaySnapshot({
724
+ relay: params.relay,
725
+ filters: [{
726
+ kinds: [...BUZZ_INBOUND_MESSAGE_KINDS],
727
+ "#h": [params.channelId],
728
+ since: params.since,
729
+ until: params.until,
730
+ ...params.requestLimit === void 0 ? {} : { limit: params.requestLimit }
731
+ }],
732
+ signal: params.signal,
733
+ timeoutMessage: `Timed out loading Buzz room history for ${params.channelId}`,
734
+ abortMessage: "Buzz room history query aborted",
735
+ failureMessage: "Buzz room history query failed",
736
+ closeReason: HISTORY_PAGE_COMPLETE_REASON,
737
+ closeMessage: (reason) => `Buzz room history query closed for ${params.channelId}: ${reason}`,
738
+ onEvent: (event) => {
739
+ if (params.skipEventIds?.has(event.id)) return;
740
+ if (events.length < params.maxEvents) events.push(event);
741
+ else overLimit = true;
742
+ },
743
+ result: () => ({
744
+ events,
745
+ overLimit
746
+ }),
747
+ checkAbortAfterSubscribe: true
853
748
  });
854
749
  }
855
750
  async function drainBuzzRoomHistoryRange(params) {
@@ -1023,6 +918,14 @@ async function createBuzzRoomMembershipTracker(params) {
1023
918
  params.onFatalError?.(error instanceof Error ? error : new Error(String(error)));
1024
919
  params.relay.close();
1025
920
  };
921
+ const replaceMembership = (membership) => {
922
+ if (membership.roles.get(params.botPublicKey) !== "bot" || !membership.members.has(params.botPublicKey)) {
923
+ blockedRooms.add(membership.roomId);
924
+ throw new Error(`Buzz bot no longer has the Bot role in room ${membership.roomId}`);
925
+ }
926
+ memberships.set(membership.roomId, membership);
927
+ params.onMembershipsChanged?.(effectiveMemberships());
928
+ };
1026
929
  const queryMembership = (channelId) => {
1027
930
  const query = membershipQueryTail.then(async () => (await queryBuzzRoomMemberships({
1028
931
  relay: params.relay,
@@ -1034,8 +937,6 @@ async function createBuzzRoomMembershipTracker(params) {
1034
937
  return query;
1035
938
  };
1036
939
  const refreshMembership = async (channelId, state) => {
1037
- const baseline = memberships.get(channelId);
1038
- if (!baseline) throw new Error(`Missing Buzz room membership for ${channelId}`);
1039
940
  for (const delayMs of MEMBERSHIP_REFRESH_DELAYS_MS) {
1040
941
  const generation = state.generation;
1041
942
  state.lastAttemptedGeneration = generation;
@@ -1049,19 +950,14 @@ async function createBuzzRoomMembershipTracker(params) {
1049
950
  continue;
1050
951
  }
1051
952
  if (state.generation !== generation || !refreshed) continue;
953
+ const current = memberships.get(channelId);
954
+ if (current && isNewerBuzzRoomMembership(current, refreshed)) refreshed = current;
1052
955
  const pending = pendingMemberships.get(channelId);
1053
- const pendingMatches = !pending || [...pending].every(([publicKey, expected]) => refreshed.members.has(publicKey) === (expected === "present"));
1054
- const botMembershipChanged = pending?.has(params.botPublicKey) === true;
1055
- if (!pendingMatches || botMembershipChanged && !isNewerBuzzRoomMembership(refreshed, baseline)) continue;
1056
- if (refreshed.roles.get(params.botPublicKey) !== "bot" || !refreshed.members.has(params.botPublicKey)) {
1057
- blockedRooms.add(channelId);
1058
- throw new Error(`Buzz bot no longer has the Bot role in room ${channelId}`);
1059
- }
1060
- memberships.set(channelId, refreshed);
956
+ if (!(!pending || [...pending].every(([publicKey, expected]) => refreshed.members.has(publicKey) === (expected === "present")))) continue;
1061
957
  pendingMemberships.delete(channelId);
1062
958
  deniedMembers.delete(channelId);
1063
959
  blockedRooms.delete(channelId);
1064
- params.onMembershipsChanged?.(effectiveMemberships());
960
+ replaceMembership(refreshed);
1065
961
  return;
1066
962
  }
1067
963
  if (state.generation !== state.lastAttemptedGeneration) return;
@@ -1112,6 +1008,15 @@ async function createBuzzRoomMembershipTracker(params) {
1112
1008
  return refreshMembershipOnce(channelId);
1113
1009
  };
1114
1010
  const handleRoomEvent = (event, reservation) => {
1011
+ if (event.kind === 39002) {
1012
+ const membership = parseBuzzRoomMembershipEvent(event, params.relayPublicKey);
1013
+ if (membership && memberships.has(membership.roomId) && isNewerBuzzRoomMembership(membership, memberships.get(membership.roomId))) try {
1014
+ replaceMembership(membership);
1015
+ } catch (error) {
1016
+ reportSystemEventError(error);
1017
+ }
1018
+ return;
1019
+ }
1115
1020
  if (isBuzzInboundMessageKind(event.kind)) {
1116
1021
  params.onMessageEvent(event, isMember, reservation);
1117
1022
  return;
@@ -1131,38 +1036,50 @@ async function createBuzzRoomMembershipTracker(params) {
1131
1036
  }, MEMBERSHIP_READY_TIMEOUT_MS);
1132
1037
  const subscriptions = [];
1133
1038
  try {
1134
- for (const channelId of params.channelIds) subscriptions.push(openBuzzRelaySubscription(params.relay, [{
1135
- kinds: [BUZZ_ROOM_SYSTEM_KIND, BUZZ_ROOM_METADATA_EDIT_KIND],
1136
- "#h": [channelId],
1137
- since: params.since
1138
- }, {
1139
- kinds: [...BUZZ_INBOUND_MESSAGE_KINDS],
1140
- "#h": [channelId],
1141
- since: params.messageSince,
1142
- limit: params.messageLimit
1143
- }], {
1144
- onevent: (event) => {
1145
- if (!historicalRooms.has(channelId) && isBuzzInboundMessageKind(event.kind)) {
1146
- const page = historyPages.get(channelId);
1147
- if (page) {
1148
- page.count += 1;
1149
- page.oldest = Math.min(page.oldest, event.created_at);
1150
- } else historyPages.set(channelId, {
1151
- count: 1,
1152
- oldest: event.created_at
1153
- });
1039
+ for (const channelId of params.channelIds) {
1040
+ const filters = [
1041
+ {
1042
+ kinds: [BUZZ_ROOM_SYSTEM_KIND, BUZZ_ROOM_METADATA_EDIT_KIND],
1043
+ "#h": [channelId],
1044
+ since: params.since
1045
+ },
1046
+ {
1047
+ kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
1048
+ authors: [params.relayPublicKey],
1049
+ "#d": [channelId],
1050
+ limit: 1
1051
+ },
1052
+ {
1053
+ kinds: [...BUZZ_INBOUND_MESSAGE_KINDS],
1054
+ "#h": [channelId],
1055
+ since: params.messageSince(channelId),
1056
+ limit: params.messageLimit
1154
1057
  }
1155
- handleRoomEvent(event);
1156
- },
1157
- oneose: () => {
1158
- historicalRooms.add(channelId);
1159
- if (historicalRooms.size === params.channelIds.length) resolveHistorical?.();
1160
- },
1161
- onclose: (reason) => {
1162
- if (!historicalRooms.has(channelId)) rejectHistorical?.(/* @__PURE__ */ new Error(`Buzz membership subscription closed for ${channelId}: ${reason}`));
1163
- else if (reason !== "shutdown" && reason !== "relay connection closed by us" && reason !== MEMBERSHIP_TRACKER_SETUP_CLOSE_REASON && !params.signal?.aborted) params.onFatalError?.(/* @__PURE__ */ new Error(`Buzz membership subscription closed for ${channelId}: ${reason}`));
1164
- }
1165
- }));
1058
+ ];
1059
+ subscriptions.push(openBuzzRelaySubscription(params.relay, filters, {
1060
+ onevent: (event) => {
1061
+ if (!historicalRooms.has(channelId) && isBuzzInboundMessageKind(event.kind)) {
1062
+ const page = historyPages.get(channelId);
1063
+ if (page) {
1064
+ page.count += 1;
1065
+ page.oldest = Math.min(page.oldest, event.created_at);
1066
+ } else historyPages.set(channelId, {
1067
+ count: 1,
1068
+ oldest: event.created_at
1069
+ });
1070
+ }
1071
+ handleRoomEvent(event);
1072
+ },
1073
+ oneose: () => {
1074
+ historicalRooms.add(channelId);
1075
+ if (historicalRooms.size === params.channelIds.length) resolveHistorical?.();
1076
+ },
1077
+ onclose: (reason) => {
1078
+ if (!historicalRooms.has(channelId)) rejectHistorical?.(/* @__PURE__ */ new Error(`Buzz membership subscription closed for ${channelId}: ${reason}`));
1079
+ else if (reason !== "shutdown" && reason !== "relay connection closed by us" && reason !== MEMBERSHIP_TRACKER_SETUP_CLOSE_REASON && !params.signal?.aborted) params.onFatalError?.(/* @__PURE__ */ new Error(`Buzz membership subscription closed for ${channelId}: ${reason}`));
1080
+ }
1081
+ }, filters.map((filter) => Object.assign({}, filter, { "#h": [channelId] }))));
1082
+ }
1166
1083
  await historicalReady;
1167
1084
  } catch (error) {
1168
1085
  if (params.relay.connected) {
@@ -1183,7 +1100,7 @@ async function createBuzzRoomMembershipTracker(params) {
1183
1100
  if (await catchUpBuzzRoomHistory({
1184
1101
  relay: params.relay,
1185
1102
  channelId,
1186
- since: params.messageSince,
1103
+ since: params.messageSince(channelId),
1187
1104
  until: page.oldest,
1188
1105
  limit: params.messageLimit,
1189
1106
  reserveCapacity: params.reserveDispatchCapacity,
@@ -1204,10 +1121,11 @@ async function createBuzzRoomMembershipTracker(params) {
1204
1121
  const BUZZ_RELAY_MAX_SUBSCRIPTIONS = 1024;
1205
1122
  const BUZZ_RELAY_NON_ROOM_PROFILE_SUBSCRIPTION_RESERVE = 4;
1206
1123
  const BUZZ_DIRECTORY_MAX_PROFILE_SUBSCRIPTIONS = 10;
1124
+ const BUZZ_MAX_CONFIGURED_ROOMS = BUZZ_RELAY_MAX_SUBSCRIPTIONS - BUZZ_RELAY_NON_ROOM_PROFILE_SUBSCRIPTION_RESERVE;
1207
1125
  function resolveBuzzSubscriptionBudget(roomCount) {
1208
1126
  if (!Number.isSafeInteger(roomCount) || roomCount < 0) throw new Error("Buzz configured room count must be a non-negative integer");
1209
1127
  const availableProfileSubscriptions = BUZZ_RELAY_MAX_SUBSCRIPTIONS - BUZZ_RELAY_NON_ROOM_PROFILE_SUBSCRIPTION_RESERVE - roomCount;
1210
- if (availableProfileSubscriptions < 0) throw new Error(`Buzz supports at most ${BUZZ_RELAY_MAX_SUBSCRIPTIONS - BUZZ_RELAY_NON_ROOM_PROFILE_SUBSCRIPTION_RESERVE} configured rooms per account`);
1128
+ if (availableProfileSubscriptions < 0) throw new Error(`Buzz supports at most ${BUZZ_MAX_CONFIGURED_ROOMS} configured rooms per account`);
1211
1129
  return { profileLimit: Math.min(BUZZ_DIRECTORY_MAX_PROFILE_SUBSCRIPTIONS, availableProfileSubscriptions) * 200 };
1212
1130
  }
1213
1131
  //#endregion
@@ -1253,9 +1171,11 @@ function startBuzzPresenceHeartbeat(params) {
1253
1171
  await params.relay.publish(buildBuzzPresenceEvent(params.secretKey));
1254
1172
  errorReported = false;
1255
1173
  } catch (error) {
1256
- if (!stopped && !errorReported) {
1174
+ const failure = error instanceof Error ? error : new Error("Buzz presence heartbeat failed", { cause: error });
1175
+ if (!stopped && failure.message === "publish timed out") params.onFatalError(failure);
1176
+ else if (!stopped && !errorReported) {
1257
1177
  errorReported = true;
1258
- params.onError?.(error instanceof Error ? error : new Error("Buzz presence heartbeat failed", { cause: error }));
1178
+ params.onError?.(failure);
1259
1179
  }
1260
1180
  } finally {
1261
1181
  publishInFlight = false;
@@ -1341,10 +1261,9 @@ async function startBuzzBus(options) {
1341
1261
  const sessionStartedAt = Math.floor(Date.now() / 1e3);
1342
1262
  const lifecycleAbort = new AbortController();
1343
1263
  const signal = options.signal ? AbortSignal.any([options.signal, lifecycleAbort.signal]) : lifecycleAbort.signal;
1344
- let fatalErrorReported = false;
1345
1264
  const reportFatalError = (error) => {
1346
- if (signal.aborted || fatalErrorReported) return;
1347
- fatalErrorReported = true;
1265
+ if (signal.aborted) return;
1266
+ lifecycleAbort.abort(error);
1348
1267
  options.onFatalError?.(error);
1349
1268
  };
1350
1269
  const replayGuard = createChannelReplayGuard({
@@ -1454,7 +1373,7 @@ async function startBuzzBus(options) {
1454
1373
  channelIds: activeChannelIds,
1455
1374
  botPublicKey: publicKey,
1456
1375
  since: sessionStartedAt,
1457
- messageSince: options.since ?? sessionStartedAt,
1376
+ messageSince: (channelId) => options.since?.(channelId) ?? sessionStartedAt,
1458
1377
  messageLimit: resolveBuzzRoomHistoryLimit(activeChannelIds.length),
1459
1378
  reserveDispatchCapacity: (slots) => dispatchQueue.reserveCapacity(slots),
1460
1379
  onHistoryError: options.onHistoryError,
@@ -1464,7 +1383,12 @@ async function startBuzzBus(options) {
1464
1383
  if (!message || !isMember(message.channelId, event.pubkey)) return;
1465
1384
  if ((reservation ?? dispatchQueue).enqueue(async () => {
1466
1385
  await replayGuard.processGuarded(event, async () => {
1467
- await options.onMessage(message, bus, signal);
1386
+ const assertCurrent = () => {
1387
+ signal.throwIfAborted();
1388
+ if (!isMember(message.channelId, event.pubkey)) throw new Error("Buzz sender is no longer a room member");
1389
+ };
1390
+ assertCurrent();
1391
+ await options.onMessage(message, bus, signal, assertCurrent);
1468
1392
  });
1469
1393
  }) !== "overflow") return;
1470
1394
  if (reservation) {
@@ -1491,7 +1415,8 @@ async function startBuzzBus(options) {
1491
1415
  stopPresenceHeartbeat = startBuzzPresenceHeartbeat({
1492
1416
  relay,
1493
1417
  secretKey,
1494
- onError: options.onPresenceError
1418
+ onError: options.onPresenceError,
1419
+ onFatalError: reportFatalError
1495
1420
  });
1496
1421
  if (options.profileName?.trim()) syncBuzzProfile({
1497
1422
  relay,
@@ -1569,8 +1494,8 @@ async function handleBuzzInbound(params) {
1569
1494
  canDetectMention: true,
1570
1495
  wasMentioned
1571
1496
  },
1572
- groupPolicy: account.config.groupPolicy,
1573
- groupAllowFrom: account.config.groupAllowFrom,
1497
+ groupPolicy: groupConfig?.groupPolicy ?? account.config.groupPolicy,
1498
+ groupAllowFrom: groupConfig?.groupAllowFrom ?? account.config.groupAllowFrom,
1574
1499
  policy: { activation: {
1575
1500
  requireMention: groupConfig?.requireMention ?? true,
1576
1501
  allowTextCommands: true
@@ -1580,6 +1505,7 @@ async function handleBuzzInbound(params) {
1580
1505
  hasControlCommand
1581
1506
  } : void 0
1582
1507
  });
1508
+ params.assertCurrent();
1583
1509
  if (access.ingress.admission !== "dispatch") return;
1584
1510
  const senderName = bus.directory.resolveSenderName(message.senderPubkey);
1585
1511
  const roomName = bus.directory.resolveRoomName(channelId);
@@ -1639,6 +1565,11 @@ async function handleBuzzInbound(params) {
1639
1565
  BuzzEventKind: message.kind
1640
1566
  }
1641
1567
  });
1568
+ const replyTarget = {
1569
+ channelId,
1570
+ threadId: message.threadId,
1571
+ replyToId: message.threadId ?? message.id
1572
+ };
1642
1573
  await runtime.channel.inbound.dispatch({
1643
1574
  cfg,
1644
1575
  channel: "buzz",
@@ -1654,10 +1585,8 @@ async function handleBuzzInbound(params) {
1654
1585
  const text = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
1655
1586
  if (!text.trim()) return;
1656
1587
  await bus.sendText({
1657
- channelId,
1658
- text,
1659
- threadId: message.threadId,
1660
- replyToId: message.id
1588
+ ...replyTarget,
1589
+ text
1661
1590
  });
1662
1591
  },
1663
1592
  onError: (error) => {
@@ -1667,11 +1596,7 @@ async function handleBuzzInbound(params) {
1667
1596
  replyOptions: { abortSignal: signal },
1668
1597
  replyPipeline: { typing: {
1669
1598
  start: async () => {
1670
- await bus.sendTyping({
1671
- channelId,
1672
- threadId: message.threadId,
1673
- replyToId: message.id
1674
- });
1599
+ await bus.sendTyping(replyTarget);
1675
1600
  },
1676
1601
  keepaliveIntervalMs: 3e3,
1677
1602
  onStartError: (error) => {
@@ -1684,6 +1609,38 @@ async function handleBuzzInbound(params) {
1684
1609
  });
1685
1610
  }
1686
1611
  //#endregion
1612
+ //#region extensions/buzz/src/recovery-watermark.ts
1613
+ function roomCursorKey(channelId) {
1614
+ return `room:${channelId}`;
1615
+ }
1616
+ function openBuzzRecoveryWatermarkStore(params) {
1617
+ const accountScope = createHash("sha256").update(params.accountId).digest("hex").slice(0, 16);
1618
+ return getBuzzRuntime().state.openKeyedStore({
1619
+ namespace: `buzz.recovery-watermark-${accountScope}`,
1620
+ maxEntries: BUZZ_MAX_CONFIGURED_ROOMS,
1621
+ overflowPolicy: "reject-new"
1622
+ });
1623
+ }
1624
+ async function resolveBuzzRecoverySince(params) {
1625
+ const { store, channelIds, nowSeconds, lookbackSeconds } = params;
1626
+ const configuredKeys = new Set(channelIds.map(roomCursorKey));
1627
+ for (const { key } of await store.entries()) if (key.startsWith("room:") && !configuredKeys.has(key)) await store.delete(key);
1628
+ const sinceByRoom = /* @__PURE__ */ new Map();
1629
+ const retentionFloor = nowSeconds - lookbackSeconds;
1630
+ for (const channelId of channelIds) {
1631
+ const key = roomCursorKey(channelId);
1632
+ const activation = await store.lookup(key);
1633
+ if (activation === void 0) {
1634
+ await store.register(key, { seconds: nowSeconds });
1635
+ sinceByRoom.set(channelId, nowSeconds);
1636
+ continue;
1637
+ }
1638
+ if (typeof activation.seconds !== "number" || !Number.isFinite(activation.seconds)) throw new Error(`Invalid Buzz recovery watermark for room ${channelId}`);
1639
+ sinceByRoom.set(channelId, Math.min(Math.max(activation.seconds, retentionFloor), nowSeconds));
1640
+ }
1641
+ return sinceByRoom;
1642
+ }
1643
+ //#endregion
1687
1644
  //#region extensions/buzz/src/gateway.ts
1688
1645
  const activeBuses = /* @__PURE__ */ new Map();
1689
1646
  const RECONNECT_BACKOFF = {
@@ -1727,7 +1684,7 @@ async function startBuzzGatewayAccount(ctx) {
1727
1684
  account,
1728
1685
  channelIds
1729
1686
  });
1730
- let hasAttemptedSession = false;
1687
+ const watermarkStore = openBuzzRecoveryWatermarkStore({ accountId: account.accountId });
1731
1688
  let reconnectAttempt = 0;
1732
1689
  while (!ctx.abortSignal.aborted) {
1733
1690
  let bus;
@@ -1738,8 +1695,13 @@ async function startBuzzGatewayAccount(ctx) {
1738
1695
  reportBusFailure = resolve;
1739
1696
  });
1740
1697
  try {
1741
- const sessionSince = Math.floor(Date.now() / 1e3) - (hasAttemptedSession ? RECONNECT_LOOKBACK_SECONDS : 0);
1742
- hasAttemptedSession = true;
1698
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1699
+ const sinceByRoom = await resolveBuzzRecoverySince({
1700
+ store: watermarkStore,
1701
+ channelIds,
1702
+ nowSeconds,
1703
+ lookbackSeconds: RECONNECT_LOOKBACK_SECONDS
1704
+ });
1743
1705
  bus = await startBuzzBus({
1744
1706
  accountId: account.accountId,
1745
1707
  relayUrl: account.relayUrl,
@@ -1747,9 +1709,9 @@ async function startBuzzGatewayAccount(ctx) {
1747
1709
  authTag: account.authTag,
1748
1710
  profileName,
1749
1711
  channelIds,
1750
- since: sessionSince,
1712
+ since: (channelId) => sinceByRoom.get(channelId) ?? nowSeconds,
1751
1713
  signal: ctx.abortSignal,
1752
- onMessage: async (message, sessionBus, signal) => {
1714
+ onMessage: async (message, sessionBus, signal, assertCurrent) => {
1753
1715
  if (!isConfiguredBuzzChannel(configuredChannelIds, message.channelId)) return;
1754
1716
  await handleBuzzInbound({
1755
1717
  account,
@@ -1757,6 +1719,7 @@ async function startBuzzGatewayAccount(ctx) {
1757
1719
  bus: sessionBus,
1758
1720
  message,
1759
1721
  signal,
1722
+ assertCurrent,
1760
1723
  buildContext
1761
1724
  });
1762
1725
  },