@openclaw/buzz 2026.8.1-beta.2 → 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 (137) hide show
  1. package/dist/{adapter.runtime-D0uEmTwf.js → adapter.runtime-DA6guOQ_.js} +5 -5
  2. package/dist/api.js +2 -2
  3. package/dist/{channel-CCmyE6Yq.js → channel-BulZqShF.js} +234 -270
  4. package/dist/channel-plugin-api.js +1 -1
  5. package/dist/{directory-config-BXa3Vqzu.js → directory-config-BesCse0q.js} +1 -1
  6. package/dist/directory-contract-api.js +1 -1
  7. package/dist/qa-runner-api.js +2 -2
  8. package/dist/{room-membership-CNBlHjTj.js → room-membership-DSAkyE0a.js} +55 -5
  9. package/dist/setup-plugin-api.js +2 -2
  10. package/dist/{setup-surface-DCxlD2oY.js → setup-surface-BvQgEJ7O.js} +20 -42
  11. package/dist/{types-CPFsIZ5Z.js → types-BN_HOsnq.js} +18 -5
  12. package/node_modules/nostr-tools/lib/cjs/abstract-pool.js +7 -5
  13. package/node_modules/nostr-tools/lib/cjs/abstract-pool.js.map +2 -2
  14. package/node_modules/nostr-tools/lib/cjs/abstract-relay.js +6 -4
  15. package/node_modules/nostr-tools/lib/cjs/abstract-relay.js.map +2 -2
  16. package/node_modules/nostr-tools/lib/cjs/core.js +10 -6
  17. package/node_modules/nostr-tools/lib/cjs/core.js.map +2 -2
  18. package/node_modules/nostr-tools/lib/cjs/index.js +17 -11
  19. package/node_modules/nostr-tools/lib/cjs/index.js.map +2 -2
  20. package/node_modules/nostr-tools/lib/cjs/kinds.js +2 -0
  21. package/node_modules/nostr-tools/lib/cjs/kinds.js.map +2 -2
  22. package/node_modules/nostr-tools/lib/cjs/nip04.js.map +1 -1
  23. package/node_modules/nostr-tools/lib/cjs/nip10.js +2 -0
  24. package/node_modules/nostr-tools/lib/cjs/nip10.js.map +2 -2
  25. package/node_modules/nostr-tools/lib/cjs/nip13.js.map +1 -1
  26. package/node_modules/nostr-tools/lib/cjs/nip17.js +2 -0
  27. package/node_modules/nostr-tools/lib/cjs/nip17.js.map +2 -2
  28. package/node_modules/nostr-tools/lib/cjs/nip18.js +2 -0
  29. package/node_modules/nostr-tools/lib/cjs/nip18.js.map +2 -2
  30. package/node_modules/nostr-tools/lib/cjs/nip19.js.map +1 -1
  31. package/node_modules/nostr-tools/lib/cjs/nip21.js.map +1 -1
  32. package/node_modules/nostr-tools/lib/cjs/nip22.js +2 -0
  33. package/node_modules/nostr-tools/lib/cjs/nip22.js.map +2 -2
  34. package/node_modules/nostr-tools/lib/cjs/nip25.js +2 -0
  35. package/node_modules/nostr-tools/lib/cjs/nip25.js.map +2 -2
  36. package/node_modules/nostr-tools/lib/cjs/nip27.js.map +1 -1
  37. package/node_modules/nostr-tools/lib/cjs/nip28.js +2 -0
  38. package/node_modules/nostr-tools/lib/cjs/nip28.js.map +2 -2
  39. package/node_modules/nostr-tools/lib/cjs/nip29.js +288 -12
  40. package/node_modules/nostr-tools/lib/cjs/nip29.js.map +2 -2
  41. package/node_modules/nostr-tools/lib/cjs/nip44.js.map +1 -1
  42. package/node_modules/nostr-tools/lib/cjs/nip45.js +2 -0
  43. package/node_modules/nostr-tools/lib/cjs/nip45.js.map +2 -2
  44. package/node_modules/nostr-tools/lib/cjs/nip46.js +9 -5
  45. package/node_modules/nostr-tools/lib/cjs/nip46.js.map +2 -2
  46. package/node_modules/nostr-tools/lib/cjs/nip47.js +2 -0
  47. package/node_modules/nostr-tools/lib/cjs/nip47.js.map +2 -2
  48. package/node_modules/nostr-tools/lib/cjs/nip57.js +2 -0
  49. package/node_modules/nostr-tools/lib/cjs/nip57.js.map +2 -2
  50. package/node_modules/nostr-tools/lib/cjs/nip59.js +2 -0
  51. package/node_modules/nostr-tools/lib/cjs/nip59.js.map +2 -2
  52. package/node_modules/nostr-tools/lib/cjs/nip98.js +2 -0
  53. package/node_modules/nostr-tools/lib/cjs/nip98.js.map +2 -2
  54. package/node_modules/nostr-tools/lib/cjs/nipb0.js +2 -0
  55. package/node_modules/nostr-tools/lib/cjs/nipb0.js.map +2 -2
  56. package/node_modules/nostr-tools/lib/cjs/nipb7.js +2 -0
  57. package/node_modules/nostr-tools/lib/cjs/nipb7.js.map +2 -2
  58. package/node_modules/nostr-tools/lib/cjs/pool.js +9 -5
  59. package/node_modules/nostr-tools/lib/cjs/pool.js.map +2 -2
  60. package/node_modules/nostr-tools/lib/cjs/pure.js +10 -6
  61. package/node_modules/nostr-tools/lib/cjs/pure.js.map +2 -2
  62. package/node_modules/nostr-tools/lib/cjs/references.js.map +1 -1
  63. package/node_modules/nostr-tools/lib/cjs/relay.js +8 -4
  64. package/node_modules/nostr-tools/lib/cjs/relay.js.map +2 -2
  65. package/node_modules/nostr-tools/lib/cjs/signer.js +2 -0
  66. package/node_modules/nostr-tools/lib/cjs/signer.js.map +2 -2
  67. package/node_modules/nostr-tools/lib/cjs/utils.js +2 -0
  68. package/node_modules/nostr-tools/lib/cjs/utils.js.map +2 -2
  69. package/node_modules/nostr-tools/lib/cjs/wasm.js +10 -6
  70. package/node_modules/nostr-tools/lib/cjs/wasm.js.map +2 -2
  71. package/node_modules/nostr-tools/lib/esm/abstract-pool.js +7 -5
  72. package/node_modules/nostr-tools/lib/esm/abstract-pool.js.map +2 -2
  73. package/node_modules/nostr-tools/lib/esm/abstract-relay.js +6 -4
  74. package/node_modules/nostr-tools/lib/esm/abstract-relay.js.map +2 -2
  75. package/node_modules/nostr-tools/lib/esm/core.js +10 -6
  76. package/node_modules/nostr-tools/lib/esm/core.js.map +2 -2
  77. package/node_modules/nostr-tools/lib/esm/index.js +17 -11
  78. package/node_modules/nostr-tools/lib/esm/index.js.map +2 -2
  79. package/node_modules/nostr-tools/lib/esm/kinds.js +2 -0
  80. package/node_modules/nostr-tools/lib/esm/kinds.js.map +2 -2
  81. package/node_modules/nostr-tools/lib/esm/nip04.js.map +1 -1
  82. package/node_modules/nostr-tools/lib/esm/nip10.js +2 -0
  83. package/node_modules/nostr-tools/lib/esm/nip10.js.map +2 -2
  84. package/node_modules/nostr-tools/lib/esm/nip13.js.map +1 -1
  85. package/node_modules/nostr-tools/lib/esm/nip17.js +2 -0
  86. package/node_modules/nostr-tools/lib/esm/nip17.js.map +2 -2
  87. package/node_modules/nostr-tools/lib/esm/nip18.js +2 -0
  88. package/node_modules/nostr-tools/lib/esm/nip18.js.map +2 -2
  89. package/node_modules/nostr-tools/lib/esm/nip19.js.map +1 -1
  90. package/node_modules/nostr-tools/lib/esm/nip21.js.map +1 -1
  91. package/node_modules/nostr-tools/lib/esm/nip22.js +2 -0
  92. package/node_modules/nostr-tools/lib/esm/nip22.js.map +2 -2
  93. package/node_modules/nostr-tools/lib/esm/nip25.js +2 -0
  94. package/node_modules/nostr-tools/lib/esm/nip25.js.map +2 -2
  95. package/node_modules/nostr-tools/lib/esm/nip27.js.map +1 -1
  96. package/node_modules/nostr-tools/lib/esm/nip28.js +2 -0
  97. package/node_modules/nostr-tools/lib/esm/nip28.js.map +2 -2
  98. package/node_modules/nostr-tools/lib/esm/nip29.js +288 -12
  99. package/node_modules/nostr-tools/lib/esm/nip29.js.map +2 -2
  100. package/node_modules/nostr-tools/lib/esm/nip44.js.map +1 -1
  101. package/node_modules/nostr-tools/lib/esm/nip45.js +2 -0
  102. package/node_modules/nostr-tools/lib/esm/nip45.js.map +2 -2
  103. package/node_modules/nostr-tools/lib/esm/nip46.js +9 -5
  104. package/node_modules/nostr-tools/lib/esm/nip46.js.map +2 -2
  105. package/node_modules/nostr-tools/lib/esm/nip47.js +2 -0
  106. package/node_modules/nostr-tools/lib/esm/nip47.js.map +2 -2
  107. package/node_modules/nostr-tools/lib/esm/nip57.js +2 -0
  108. package/node_modules/nostr-tools/lib/esm/nip57.js.map +2 -2
  109. package/node_modules/nostr-tools/lib/esm/nip59.js +2 -0
  110. package/node_modules/nostr-tools/lib/esm/nip59.js.map +2 -2
  111. package/node_modules/nostr-tools/lib/esm/nip98.js +2 -0
  112. package/node_modules/nostr-tools/lib/esm/nip98.js.map +2 -2
  113. package/node_modules/nostr-tools/lib/esm/nipb0.js +2 -0
  114. package/node_modules/nostr-tools/lib/esm/nipb0.js.map +2 -2
  115. package/node_modules/nostr-tools/lib/esm/nipb7.js +2 -0
  116. package/node_modules/nostr-tools/lib/esm/nipb7.js.map +2 -2
  117. package/node_modules/nostr-tools/lib/esm/pool.js +9 -5
  118. package/node_modules/nostr-tools/lib/esm/pool.js.map +2 -2
  119. package/node_modules/nostr-tools/lib/esm/pure.js +10 -6
  120. package/node_modules/nostr-tools/lib/esm/pure.js.map +2 -2
  121. package/node_modules/nostr-tools/lib/esm/references.js.map +1 -1
  122. package/node_modules/nostr-tools/lib/esm/relay.js +8 -4
  123. package/node_modules/nostr-tools/lib/esm/relay.js.map +2 -2
  124. package/node_modules/nostr-tools/lib/esm/signer.js +2 -0
  125. package/node_modules/nostr-tools/lib/esm/signer.js.map +2 -2
  126. package/node_modules/nostr-tools/lib/esm/utils.js +2 -0
  127. package/node_modules/nostr-tools/lib/esm/utils.js.map +2 -2
  128. package/node_modules/nostr-tools/lib/esm/wasm.js +10 -6
  129. package/node_modules/nostr-tools/lib/esm/wasm.js.map +2 -2
  130. package/node_modules/nostr-tools/lib/nostr.bundle.js +17 -11
  131. package/node_modules/nostr-tools/lib/nostr.bundle.js.map +2 -2
  132. package/node_modules/nostr-tools/lib/types/core.d.ts +5 -0
  133. package/node_modules/nostr-tools/lib/types/nip29.d.ts +243 -3
  134. package/node_modules/nostr-tools/lib/types/nip29.test.d.ts +1 -0
  135. package/node_modules/nostr-tools/package.json +1 -1
  136. package/openclaw.plugin.json +24 -0
  137. package/package.json +5 -5
@@ -1,7 +1,7 @@
1
- import { a as resolveDefaultBuzzAccountId, c as isConfiguredBuzzChannel, d as parseBuzzTarget, i as resolveBuzzPublicKey, l as looksLikeBuzzTarget, n as listBuzzAccountIds, r as resolveBuzzAccount, s as buildBuzzTarget, t as decodeBuzzPrivateKey, u as normalizeBuzzTarget } from "./types-CPFsIZ5Z.js";
2
- import { a as BuzzConfigSchema, i as startBuzzRoomMembershipNotifications, n as buzzSetupContract, r as discoverBuzzRooms, t as buzzSetupWizard } from "./setup-surface-DCxlD2oY.js";
3
- import { n as listBuzzDirectoryPeersFromConfig, t as listBuzzDirectoryGroupsFromConfig } from "./directory-config-BXa3Vqzu.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-CNBlHjTj.js";
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-BvQgEJ7O.js";
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, 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";
@@ -11,6 +11,7 @@ import { createChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-run
11
11
  import { parseThreadSessionSuffix } from "openclaw/plugin-sdk/routing";
12
12
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
13
13
  import { finalizeEvent } from "nostr-tools";
14
+ import { chunkItems } from "openclaw/plugin-sdk/text-chunking";
14
15
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
15
16
  import { attachChannelToResult } from "openclaw/plugin-sdk/channel-send-result";
16
17
  import { channelReadyPatch } from "openclaw/plugin-sdk/gateway-runtime";
@@ -20,6 +21,7 @@ import { buildChannelInboundEventContext, resolveChannelInboundRouteEnvelope } f
20
21
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
21
22
  import { createSubsystemLogger } from "openclaw/plugin-sdk/logging-core";
22
23
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
24
+ import { createHash } from "node:crypto";
23
25
  const BUZZ_ROOM_METADATA_KIND = 39e3;
24
26
  const DEFAULT_BUZZ_DIRECTORY_PROFILE_LIMIT = 2e3;
25
27
  const HEX_PUBLIC_KEY_PATTERN = /^[0-9a-f]{64}$/u;
@@ -250,58 +252,27 @@ const PROFILE_SUBSCRIPTION_REPLACED_REASON = "directory profile subscription rep
250
252
  const PROFILE_SUBSCRIPTION_FAILED_REASON = "directory profile subscription generation failed";
251
253
  const DIRECTORY_SHUTDOWN_REASON = "directory shutdown";
252
254
  const DIRECTORY_QUERY_COMPLETE_REASON = "directory query complete";
253
- const DIRECTORY_QUERY_TIMEOUT_MS = 1e4;
254
255
  const PROFILE_SUBSCRIPTION_READY_TIMEOUT_MS = 1e4;
255
- function chunkValues(values, size) {
256
- const chunks = [];
257
- for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size));
258
- return chunks;
259
- }
260
256
  async function queryBuzzDirectoryBatch(params) {
261
257
  params.signal?.throwIfAborted();
262
- await new Promise((resolve, reject) => {
263
- let settled = false;
264
- let receivedEose = false;
265
- const timeout = setTimeout(() => {
266
- const error = /* @__PURE__ */ new Error("Timed out loading Buzz directory snapshot");
267
- finish(error);
268
- if (params.onTimeout) params.onTimeout(error);
269
- else params.relay.close();
270
- }, DIRECTORY_QUERY_TIMEOUT_MS);
271
- const subscriptionRef = {};
272
- const finish = (error) => {
273
- if (settled) return;
274
- settled = true;
275
- clearTimeout(timeout);
276
- params.signal?.removeEventListener("abort", onAbort);
277
- if (receivedEose) subscriptionRef.current?.close(DIRECTORY_QUERY_COMPLETE_REASON);
278
- if (error === void 0) resolve();
279
- else reject(error instanceof Error ? error : new Error("Buzz directory query failed", { cause: error }));
280
- };
281
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz directory query aborted"));
282
- params.signal?.addEventListener("abort", onAbort, { once: true });
283
- try {
284
- subscriptionRef.current = openBuzzRelaySubscription(params.relay, [params.filter], {
285
- onevent: params.onEvent,
286
- oneose: () => {
287
- receivedEose = true;
288
- if (settled) subscriptionRef.current?.close(DIRECTORY_QUERY_COMPLETE_REASON);
289
- else finish();
290
- },
291
- onclose: (reason) => {
292
- if (reason !== DIRECTORY_QUERY_COMPLETE_REASON) finish(/* @__PURE__ */ new Error(`Buzz directory query closed: ${reason}`));
293
- }
294
- });
295
- } catch (error) {
296
- finish(error);
297
- return;
298
- }
299
- if (settled && receivedEose) subscriptionRef.current.close(DIRECTORY_QUERY_COMPLETE_REASON);
300
- 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
301
272
  });
302
273
  }
303
274
  async function queryBuzzDirectoryProfiles(params) {
304
- for (const authors of chunkValues(params.publicKeys, 200)) await queryBuzzDirectoryBatch({
275
+ for (const authors of chunkItems(params.publicKeys, 200)) await queryBuzzDirectoryBatch({
305
276
  relay: params.relay,
306
277
  filter: {
307
278
  kinds: [0],
@@ -316,7 +287,7 @@ async function queryBuzzDirectoryProfiles(params) {
316
287
  });
317
288
  }
318
289
  async function queryBuzzDirectoryRooms(params) {
319
- for (const roomIds of chunkValues(params.channelIds, BUZZ_ROOM_QUERY_CHUNK_SIZE)) await queryBuzzDirectoryBatch({
290
+ for (const roomIds of chunkItems(params.channelIds, BUZZ_ROOM_QUERY_CHUNK_SIZE)) await queryBuzzDirectoryBatch({
320
291
  relay: params.relay,
321
292
  filter: {
322
293
  kinds: [BUZZ_ROOM_METADATA_KIND],
@@ -360,7 +331,7 @@ function startBuzzDirectoryRelay(params) {
360
331
  const publicKeys = queuedProfilePublicKeys;
361
332
  queuedProfilePublicKeys = void 0;
362
333
  closeProfileGeneration(PROFILE_SUBSCRIPTION_REPLACED_REASON);
363
- const authorChunks = chunkValues(publicKeys, 200);
334
+ const authorChunks = chunkItems(publicKeys, 200);
364
335
  if (authorChunks.length === 0) return;
365
336
  const generation = {
366
337
  subscriptions: [],
@@ -473,7 +444,6 @@ const CHANNEL_ADD_POLICIES = /* @__PURE__ */ new Set([
473
444
  "owner_only",
474
445
  "nobody"
475
446
  ]);
476
- const PROFILE_QUERY_TIMEOUT_MS = 1e4;
477
447
  function parseProfileContent(event) {
478
448
  if (!event) return {};
479
449
  try {
@@ -499,33 +469,10 @@ function readNonEmptyString(content, key) {
499
469
  }
500
470
  async function queryCurrentProfiles(params) {
501
471
  params.signal?.throwIfAborted();
502
- return await new Promise((resolve, reject) => {
503
- const latestByKind = /* @__PURE__ */ new Map();
504
- const state = {
505
- settled: false,
506
- receivedEose: false
507
- };
508
- const timeout = setTimeout(() => {
509
- const error = /* @__PURE__ */ new Error("Timed out loading current Buzz profile");
510
- finish(error);
511
- params.onTimeout?.(error);
512
- params.relay.close();
513
- }, PROFILE_QUERY_TIMEOUT_MS);
514
- const finish = (error) => {
515
- if (state.settled) return;
516
- state.settled = true;
517
- clearTimeout(timeout);
518
- params.signal?.removeEventListener("abort", onAbort);
519
- if (state.receivedEose) state.subscription?.close("profile query complete");
520
- if (error !== void 0) {
521
- reject(error instanceof Error ? error : new Error("Buzz profile query failed", { cause: error }));
522
- return;
523
- }
524
- resolve(latestByKind);
525
- };
526
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz profile query aborted"));
527
- params.signal?.addEventListener("abort", onAbort, { once: true });
528
- state.subscription = openBuzzRelaySubscription(params.relay, [{
472
+ const latestByKind = /* @__PURE__ */ new Map();
473
+ return await queryBuzzRelaySnapshot({
474
+ relay: params.relay,
475
+ filters: [{
529
476
  kinds: [PROFILE_KIND],
530
477
  authors: [params.publicKey],
531
478
  limit: 1
@@ -533,21 +480,19 @@ async function queryCurrentProfiles(params) {
533
480
  kinds: [AGENT_PROFILE_KIND],
534
481
  authors: [params.publicKey],
535
482
  limit: 1
536
- }], {
537
- onevent: (event) => {
538
- const current = latestByKind.get(event.kind);
539
- if (!current || event.created_at > current.created_at) latestByKind.set(event.kind, event);
540
- },
541
- oneose: () => {
542
- state.receivedEose = true;
543
- if (state.settled) state.subscription?.close("profile query complete");
544
- else finish();
545
- },
546
- onclose: (reason) => {
547
- if (reason !== "profile query complete") finish(/* @__PURE__ */ new Error(`Buzz profile query closed: ${reason}`));
548
- }
549
- });
550
- 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
551
496
  });
552
497
  }
553
498
  function buildProfileEvent(params) {
@@ -733,56 +678,29 @@ function resolveBuzzRoomHistoryLimit(roomCount) {
733
678
  //#region extensions/buzz/src/room-membership-query.ts
734
679
  const RELAY_QUERY_EVENT_LIMIT = 1e3;
735
680
  const MEMBERSHIP_QUERY_COMPLETE_REASON = "membership snapshot loaded";
736
- const MEMBERSHIP_QUERY_TIMEOUT_MS = 1e4;
737
681
  async function queryBuzzRoomMembershipBatch(params) {
738
682
  const configuredRooms = new Set(params.channelIds);
739
683
  const memberships = /* @__PURE__ */ new Map();
740
- return await new Promise((resolve, reject) => {
741
- let settled = false;
742
- let receivedEose = false;
743
- const timeout = setTimeout(() => {
744
- finish(/* @__PURE__ */ new Error("Timed out loading Buzz room membership snapshot"));
745
- params.relay.close();
746
- }, MEMBERSHIP_QUERY_TIMEOUT_MS);
747
- const subscriptionRef = {};
748
- const finish = (error) => {
749
- if (settled) return;
750
- settled = true;
751
- clearTimeout(timeout);
752
- params.signal?.removeEventListener("abort", onAbort);
753
- if (receivedEose) subscriptionRef.current?.close(MEMBERSHIP_QUERY_COMPLETE_REASON);
754
- if (error === void 0) resolve(memberships);
755
- else reject(error instanceof Error ? error : new Error("Buzz room membership query failed", { cause: error }));
756
- };
757
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz room membership query aborted"));
758
- params.signal?.addEventListener("abort", onAbort, { once: true });
759
- try {
760
- subscriptionRef.current = openBuzzRelaySubscription(params.relay, [{
761
- kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
762
- authors: [params.relayPublicKey],
763
- "#d": params.channelIds,
764
- limit: params.channelIds.length
765
- }], {
766
- onevent: (event) => {
767
- const membership = parseBuzzRoomMembershipEvent(event, params.relayPublicKey);
768
- if (!membership || !configuredRooms.has(membership.roomId) || !isNewerBuzzRoomMembership(membership, memberships.get(membership.roomId))) return;
769
- memberships.set(membership.roomId, membership);
770
- },
771
- oneose: () => {
772
- receivedEose = true;
773
- if (settled) subscriptionRef.current?.close(MEMBERSHIP_QUERY_COMPLETE_REASON);
774
- else finish();
775
- },
776
- onclose: (reason) => {
777
- if (reason !== MEMBERSHIP_QUERY_COMPLETE_REASON) finish(/* @__PURE__ */ new Error(`Buzz room membership query closed: ${reason}`));
778
- }
779
- });
780
- } catch (error) {
781
- finish(error);
782
- return;
783
- }
784
- if (settled && receivedEose) subscriptionRef.current.close(MEMBERSHIP_QUERY_COMPLETE_REASON);
785
- 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
786
704
  });
787
705
  }
788
706
  async function queryBuzzRoomMemberships(params) {
@@ -798,62 +716,35 @@ async function queryBuzzRoomMemberships(params) {
798
716
  }
799
717
  //#endregion
800
718
  //#region extensions/buzz/src/history-catchup.ts
801
- const HISTORY_PAGE_TIMEOUT_MS = 1e4;
802
719
  const HISTORY_PAGE_COMPLETE_REASON = "buzz room history page loaded";
803
720
  async function queryBuzzRoomHistoryPage(params) {
804
721
  const events = [];
805
722
  let overLimit = false;
806
- return await new Promise((resolve, reject) => {
807
- let settled = false;
808
- let receivedEose = false;
809
- const timeout = setTimeout(() => {
810
- const error = /* @__PURE__ */ new Error(`Timed out loading Buzz room history for ${params.channelId}`);
811
- finish(error);
812
- params.relay.close();
813
- }, HISTORY_PAGE_TIMEOUT_MS);
814
- const subscriptionRef = {};
815
- const finish = (error) => {
816
- if (settled) return;
817
- settled = true;
818
- clearTimeout(timeout);
819
- params.signal?.removeEventListener("abort", onAbort);
820
- if (receivedEose) subscriptionRef.current?.close(HISTORY_PAGE_COMPLETE_REASON);
821
- if (error === void 0) resolve({
822
- events,
823
- overLimit
824
- });
825
- else reject(error instanceof Error ? error : new Error("Buzz room history query failed", { cause: error }));
826
- };
827
- const onAbort = () => finish(params.signal?.reason ?? /* @__PURE__ */ new Error("Buzz room history query aborted"));
828
- params.signal?.addEventListener("abort", onAbort, { once: true });
829
- try {
830
- subscriptionRef.current = openBuzzRelaySubscription(params.relay, [{
831
- kinds: [...BUZZ_INBOUND_MESSAGE_KINDS],
832
- "#h": [params.channelId],
833
- since: params.since,
834
- until: params.until,
835
- ...params.requestLimit === void 0 ? {} : { limit: params.requestLimit }
836
- }], {
837
- onevent: (event) => {
838
- if (params.skipEventIds?.has(event.id)) return;
839
- if (events.length < params.maxEvents) events.push(event);
840
- else overLimit = true;
841
- },
842
- oneose: () => {
843
- receivedEose = true;
844
- if (settled) subscriptionRef.current?.close(HISTORY_PAGE_COMPLETE_REASON);
845
- else finish();
846
- },
847
- onclose: (reason) => {
848
- if (reason !== HISTORY_PAGE_COMPLETE_REASON) finish(/* @__PURE__ */ new Error(`Buzz room history query closed for ${params.channelId}: ${reason}`));
849
- }
850
- });
851
- } catch (error) {
852
- finish(error);
853
- return;
854
- }
855
- if (settled && receivedEose) subscriptionRef.current.close(HISTORY_PAGE_COMPLETE_REASON);
856
- 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
857
748
  });
858
749
  }
859
750
  async function drainBuzzRoomHistoryRange(params) {
@@ -1027,6 +918,14 @@ async function createBuzzRoomMembershipTracker(params) {
1027
918
  params.onFatalError?.(error instanceof Error ? error : new Error(String(error)));
1028
919
  params.relay.close();
1029
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
+ };
1030
929
  const queryMembership = (channelId) => {
1031
930
  const query = membershipQueryTail.then(async () => (await queryBuzzRoomMemberships({
1032
931
  relay: params.relay,
@@ -1038,8 +937,6 @@ async function createBuzzRoomMembershipTracker(params) {
1038
937
  return query;
1039
938
  };
1040
939
  const refreshMembership = async (channelId, state) => {
1041
- const baseline = memberships.get(channelId);
1042
- if (!baseline) throw new Error(`Missing Buzz room membership for ${channelId}`);
1043
940
  for (const delayMs of MEMBERSHIP_REFRESH_DELAYS_MS) {
1044
941
  const generation = state.generation;
1045
942
  state.lastAttemptedGeneration = generation;
@@ -1053,19 +950,14 @@ async function createBuzzRoomMembershipTracker(params) {
1053
950
  continue;
1054
951
  }
1055
952
  if (state.generation !== generation || !refreshed) continue;
953
+ const current = memberships.get(channelId);
954
+ if (current && isNewerBuzzRoomMembership(current, refreshed)) refreshed = current;
1056
955
  const pending = pendingMemberships.get(channelId);
1057
- const pendingMatches = !pending || [...pending].every(([publicKey, expected]) => refreshed.members.has(publicKey) === (expected === "present"));
1058
- const botMembershipChanged = pending?.has(params.botPublicKey) === true;
1059
- if (!pendingMatches || botMembershipChanged && !isNewerBuzzRoomMembership(refreshed, baseline)) continue;
1060
- if (refreshed.roles.get(params.botPublicKey) !== "bot" || !refreshed.members.has(params.botPublicKey)) {
1061
- blockedRooms.add(channelId);
1062
- throw new Error(`Buzz bot no longer has the Bot role in room ${channelId}`);
1063
- }
1064
- memberships.set(channelId, refreshed);
956
+ if (!(!pending || [...pending].every(([publicKey, expected]) => refreshed.members.has(publicKey) === (expected === "present")))) continue;
1065
957
  pendingMemberships.delete(channelId);
1066
958
  deniedMembers.delete(channelId);
1067
959
  blockedRooms.delete(channelId);
1068
- params.onMembershipsChanged?.(effectiveMemberships());
960
+ replaceMembership(refreshed);
1069
961
  return;
1070
962
  }
1071
963
  if (state.generation !== state.lastAttemptedGeneration) return;
@@ -1116,6 +1008,15 @@ async function createBuzzRoomMembershipTracker(params) {
1116
1008
  return refreshMembershipOnce(channelId);
1117
1009
  };
1118
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
+ }
1119
1020
  if (isBuzzInboundMessageKind(event.kind)) {
1120
1021
  params.onMessageEvent(event, isMember, reservation);
1121
1022
  return;
@@ -1135,38 +1036,50 @@ async function createBuzzRoomMembershipTracker(params) {
1135
1036
  }, MEMBERSHIP_READY_TIMEOUT_MS);
1136
1037
  const subscriptions = [];
1137
1038
  try {
1138
- for (const channelId of params.channelIds) subscriptions.push(openBuzzRelaySubscription(params.relay, [{
1139
- kinds: [BUZZ_ROOM_SYSTEM_KIND, BUZZ_ROOM_METADATA_EDIT_KIND],
1140
- "#h": [channelId],
1141
- since: params.since
1142
- }, {
1143
- kinds: [...BUZZ_INBOUND_MESSAGE_KINDS],
1144
- "#h": [channelId],
1145
- since: params.messageSince,
1146
- limit: params.messageLimit
1147
- }], {
1148
- onevent: (event) => {
1149
- if (!historicalRooms.has(channelId) && isBuzzInboundMessageKind(event.kind)) {
1150
- const page = historyPages.get(channelId);
1151
- if (page) {
1152
- page.count += 1;
1153
- page.oldest = Math.min(page.oldest, event.created_at);
1154
- } else historyPages.set(channelId, {
1155
- count: 1,
1156
- oldest: event.created_at
1157
- });
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
1158
1057
  }
1159
- handleRoomEvent(event);
1160
- },
1161
- oneose: () => {
1162
- historicalRooms.add(channelId);
1163
- if (historicalRooms.size === params.channelIds.length) resolveHistorical?.();
1164
- },
1165
- onclose: (reason) => {
1166
- if (!historicalRooms.has(channelId)) rejectHistorical?.(/* @__PURE__ */ new Error(`Buzz membership subscription closed for ${channelId}: ${reason}`));
1167
- 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}`));
1168
- }
1169
- }));
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
+ }
1170
1083
  await historicalReady;
1171
1084
  } catch (error) {
1172
1085
  if (params.relay.connected) {
@@ -1187,7 +1100,7 @@ async function createBuzzRoomMembershipTracker(params) {
1187
1100
  if (await catchUpBuzzRoomHistory({
1188
1101
  relay: params.relay,
1189
1102
  channelId,
1190
- since: params.messageSince,
1103
+ since: params.messageSince(channelId),
1191
1104
  until: page.oldest,
1192
1105
  limit: params.messageLimit,
1193
1106
  reserveCapacity: params.reserveDispatchCapacity,
@@ -1208,10 +1121,11 @@ async function createBuzzRoomMembershipTracker(params) {
1208
1121
  const BUZZ_RELAY_MAX_SUBSCRIPTIONS = 1024;
1209
1122
  const BUZZ_RELAY_NON_ROOM_PROFILE_SUBSCRIPTION_RESERVE = 4;
1210
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;
1211
1125
  function resolveBuzzSubscriptionBudget(roomCount) {
1212
1126
  if (!Number.isSafeInteger(roomCount) || roomCount < 0) throw new Error("Buzz configured room count must be a non-negative integer");
1213
1127
  const availableProfileSubscriptions = BUZZ_RELAY_MAX_SUBSCRIPTIONS - BUZZ_RELAY_NON_ROOM_PROFILE_SUBSCRIPTION_RESERVE - roomCount;
1214
- 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`);
1215
1129
  return { profileLimit: Math.min(BUZZ_DIRECTORY_MAX_PROFILE_SUBSCRIPTIONS, availableProfileSubscriptions) * 200 };
1216
1130
  }
1217
1131
  //#endregion
@@ -1257,9 +1171,11 @@ function startBuzzPresenceHeartbeat(params) {
1257
1171
  await params.relay.publish(buildBuzzPresenceEvent(params.secretKey));
1258
1172
  errorReported = false;
1259
1173
  } catch (error) {
1260
- 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) {
1261
1177
  errorReported = true;
1262
- params.onError?.(error instanceof Error ? error : new Error("Buzz presence heartbeat failed", { cause: error }));
1178
+ params.onError?.(failure);
1263
1179
  }
1264
1180
  } finally {
1265
1181
  publishInFlight = false;
@@ -1345,10 +1261,9 @@ async function startBuzzBus(options) {
1345
1261
  const sessionStartedAt = Math.floor(Date.now() / 1e3);
1346
1262
  const lifecycleAbort = new AbortController();
1347
1263
  const signal = options.signal ? AbortSignal.any([options.signal, lifecycleAbort.signal]) : lifecycleAbort.signal;
1348
- let fatalErrorReported = false;
1349
1264
  const reportFatalError = (error) => {
1350
- if (signal.aborted || fatalErrorReported) return;
1351
- fatalErrorReported = true;
1265
+ if (signal.aborted) return;
1266
+ lifecycleAbort.abort(error);
1352
1267
  options.onFatalError?.(error);
1353
1268
  };
1354
1269
  const replayGuard = createChannelReplayGuard({
@@ -1458,7 +1373,7 @@ async function startBuzzBus(options) {
1458
1373
  channelIds: activeChannelIds,
1459
1374
  botPublicKey: publicKey,
1460
1375
  since: sessionStartedAt,
1461
- messageSince: options.since ?? sessionStartedAt,
1376
+ messageSince: (channelId) => options.since?.(channelId) ?? sessionStartedAt,
1462
1377
  messageLimit: resolveBuzzRoomHistoryLimit(activeChannelIds.length),
1463
1378
  reserveDispatchCapacity: (slots) => dispatchQueue.reserveCapacity(slots),
1464
1379
  onHistoryError: options.onHistoryError,
@@ -1468,7 +1383,12 @@ async function startBuzzBus(options) {
1468
1383
  if (!message || !isMember(message.channelId, event.pubkey)) return;
1469
1384
  if ((reservation ?? dispatchQueue).enqueue(async () => {
1470
1385
  await replayGuard.processGuarded(event, async () => {
1471
- 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);
1472
1392
  });
1473
1393
  }) !== "overflow") return;
1474
1394
  if (reservation) {
@@ -1495,7 +1415,8 @@ async function startBuzzBus(options) {
1495
1415
  stopPresenceHeartbeat = startBuzzPresenceHeartbeat({
1496
1416
  relay,
1497
1417
  secretKey,
1498
- onError: options.onPresenceError
1418
+ onError: options.onPresenceError,
1419
+ onFatalError: reportFatalError
1499
1420
  });
1500
1421
  if (options.profileName?.trim()) syncBuzzProfile({
1501
1422
  relay,
@@ -1573,8 +1494,8 @@ async function handleBuzzInbound(params) {
1573
1494
  canDetectMention: true,
1574
1495
  wasMentioned
1575
1496
  },
1576
- groupPolicy: account.config.groupPolicy,
1577
- groupAllowFrom: account.config.groupAllowFrom,
1497
+ groupPolicy: groupConfig?.groupPolicy ?? account.config.groupPolicy,
1498
+ groupAllowFrom: groupConfig?.groupAllowFrom ?? account.config.groupAllowFrom,
1578
1499
  policy: { activation: {
1579
1500
  requireMention: groupConfig?.requireMention ?? true,
1580
1501
  allowTextCommands: true
@@ -1584,6 +1505,7 @@ async function handleBuzzInbound(params) {
1584
1505
  hasControlCommand
1585
1506
  } : void 0
1586
1507
  });
1508
+ params.assertCurrent();
1587
1509
  if (access.ingress.admission !== "dispatch") return;
1588
1510
  const senderName = bus.directory.resolveSenderName(message.senderPubkey);
1589
1511
  const roomName = bus.directory.resolveRoomName(channelId);
@@ -1643,6 +1565,11 @@ async function handleBuzzInbound(params) {
1643
1565
  BuzzEventKind: message.kind
1644
1566
  }
1645
1567
  });
1568
+ const replyTarget = {
1569
+ channelId,
1570
+ threadId: message.threadId,
1571
+ replyToId: message.threadId ?? message.id
1572
+ };
1646
1573
  await runtime.channel.inbound.dispatch({
1647
1574
  cfg,
1648
1575
  channel: "buzz",
@@ -1658,10 +1585,8 @@ async function handleBuzzInbound(params) {
1658
1585
  const text = payload && typeof payload === "object" && "text" in payload ? payload.text ?? "" : "";
1659
1586
  if (!text.trim()) return;
1660
1587
  await bus.sendText({
1661
- channelId,
1662
- text,
1663
- threadId: message.threadId,
1664
- replyToId: message.id
1588
+ ...replyTarget,
1589
+ text
1665
1590
  });
1666
1591
  },
1667
1592
  onError: (error) => {
@@ -1671,11 +1596,7 @@ async function handleBuzzInbound(params) {
1671
1596
  replyOptions: { abortSignal: signal },
1672
1597
  replyPipeline: { typing: {
1673
1598
  start: async () => {
1674
- await bus.sendTyping({
1675
- channelId,
1676
- threadId: message.threadId,
1677
- replyToId: message.id
1678
- });
1599
+ await bus.sendTyping(replyTarget);
1679
1600
  },
1680
1601
  keepaliveIntervalMs: 3e3,
1681
1602
  onStartError: (error) => {
@@ -1688,6 +1609,38 @@ async function handleBuzzInbound(params) {
1688
1609
  });
1689
1610
  }
1690
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
1691
1644
  //#region extensions/buzz/src/gateway.ts
1692
1645
  const activeBuses = /* @__PURE__ */ new Map();
1693
1646
  const RECONNECT_BACKOFF = {
@@ -1720,10 +1673,8 @@ function resolveBuzzProfileName(params) {
1720
1673
  }
1721
1674
  async function startBuzzGatewayAccount(ctx) {
1722
1675
  const buildContext = ctx.channelRuntime?.inbound.buildContext;
1723
- const account = resolveBuzzAccount({
1724
- cfg: ctx.cfg,
1725
- accountId: ctx.account.accountId
1726
- });
1676
+ const account = ctx.account;
1677
+ assertBuzzAccountAvailable(account);
1727
1678
  if (!account.configured) throw new Error(`Buzz is not configured for account "${account.accountId}"`);
1728
1679
  const channelIds = Object.entries(account.config.groups ?? {}).filter(([, config]) => config.enabled !== false).map(([channelId]) => parseBuzzTarget(channelId));
1729
1680
  if (channelIds.length === 0) throw new Error("Buzz requires at least one channels.buzz.groups entry");
@@ -1733,7 +1684,7 @@ async function startBuzzGatewayAccount(ctx) {
1733
1684
  account,
1734
1685
  channelIds
1735
1686
  });
1736
- let hasAttemptedSession = false;
1687
+ const watermarkStore = openBuzzRecoveryWatermarkStore({ accountId: account.accountId });
1737
1688
  let reconnectAttempt = 0;
1738
1689
  while (!ctx.abortSignal.aborted) {
1739
1690
  let bus;
@@ -1744,8 +1695,13 @@ async function startBuzzGatewayAccount(ctx) {
1744
1695
  reportBusFailure = resolve;
1745
1696
  });
1746
1697
  try {
1747
- const sessionSince = Math.floor(Date.now() / 1e3) - (hasAttemptedSession ? RECONNECT_LOOKBACK_SECONDS : 0);
1748
- 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
+ });
1749
1705
  bus = await startBuzzBus({
1750
1706
  accountId: account.accountId,
1751
1707
  relayUrl: account.relayUrl,
@@ -1753,9 +1709,9 @@ async function startBuzzGatewayAccount(ctx) {
1753
1709
  authTag: account.authTag,
1754
1710
  profileName,
1755
1711
  channelIds,
1756
- since: sessionSince,
1712
+ since: (channelId) => sinceByRoom.get(channelId) ?? nowSeconds,
1757
1713
  signal: ctx.abortSignal,
1758
- onMessage: async (message, sessionBus, signal) => {
1714
+ onMessage: async (message, sessionBus, signal, assertCurrent) => {
1759
1715
  if (!isConfiguredBuzzChannel(configuredChannelIds, message.channelId)) return;
1760
1716
  await handleBuzzInbound({
1761
1717
  account,
@@ -1763,6 +1719,7 @@ async function startBuzzGatewayAccount(ctx) {
1763
1719
  bus: sessionBus,
1764
1720
  message,
1765
1721
  signal,
1722
+ assertCurrent,
1766
1723
  buildContext
1767
1724
  });
1768
1725
  },
@@ -1810,8 +1767,8 @@ async function startBuzzGatewayAccount(ctx) {
1810
1767
  if (ctx.abortSignal.aborted) return;
1811
1768
  cycleError = error instanceof Error ? error : new Error(String(error));
1812
1769
  } finally {
1813
- await bus?.close();
1814
1770
  if (activeBuses.get(account.accountId) === bus) activeBuses.delete(account.accountId);
1771
+ await bus?.close();
1815
1772
  ctx.setStatus({
1816
1773
  accountId: account.accountId,
1817
1774
  running: false,
@@ -1847,6 +1804,7 @@ const buzzOutboundAdapter = {
1847
1804
  cfg,
1848
1805
  accountId: resolvedAccountId
1849
1806
  });
1807
+ assertBuzzAccountAvailable(account);
1850
1808
  if (!account.enabled) throw new Error(`Buzz is disabled for account ${resolvedAccountId}`);
1851
1809
  if (!account.configured) throw new Error(`Buzz is not configured for account ${resolvedAccountId}`);
1852
1810
  const bus = activeBuses.get(resolvedAccountId);
@@ -1893,6 +1851,7 @@ function createConfiguredDirectoryState(params) {
1893
1851
  cfg: params.cfg,
1894
1852
  accountId: params.accountId
1895
1853
  });
1854
+ assertBuzzAccountAvailable(account);
1896
1855
  if (!account.publicKey) return null;
1897
1856
  const channelIds = resolveConfiguredRoomIds(account);
1898
1857
  return {
@@ -2010,7 +1969,8 @@ const buzzPlugin = createChatChannelPlugin({
2010
1969
  configured: account.configured,
2011
1970
  extra: {
2012
1971
  baseUrl: account.relayUrl,
2013
- publicKey: account.publicKey
1972
+ publicKey: account.publicKey,
1973
+ tokenStatus: account.tokenStatus
2014
1974
  }
2015
1975
  }),
2016
1976
  resolveAllowFrom: ({ cfg, accountId }) => resolveBuzzAccount({
@@ -2081,11 +2041,15 @@ const buzzPlugin = createChatChannelPlugin({
2081
2041
  name: account.name,
2082
2042
  enabled: account.enabled,
2083
2043
  configured: account.configured,
2084
- baseUrl: account.relayUrl,
2085
- publicKey: account.publicKey
2044
+ extra: {
2045
+ baseUrl: account.relayUrl,
2046
+ publicKey: account.publicKey,
2047
+ tokenStatus: account.tokenStatus
2048
+ }
2086
2049
  })
2087
2050
  }),
2088
2051
  probeAccount: async ({ account, timeoutMs }) => {
2052
+ assertBuzzAccountAvailable(account);
2089
2053
  const rooms = await discoverBuzzRooms({
2090
2054
  relayUrl: account.relayUrl,
2091
2055
  privateKey: account.privateKey,