@unicitylabs/openclaw-unicity 0.3.0 → 0.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unicitylabs/openclaw-unicity",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Unicity wallet identity and encrypted DMs for OpenClaw agents — powered by Sphere SDK",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/channel.ts CHANGED
@@ -10,6 +10,24 @@ import type { UnicityConfig } from "./config.js";
10
10
 
11
11
  const DEFAULT_ACCOUNT_ID = "default";
12
12
 
13
+ /** How long (ms) to wait after the last group message before declaring backfill complete. */
14
+ export const GROUP_BACKFILL_DEBOUNCE_MS = 3_000;
15
+
16
+ interface GroupBackfillState {
17
+ phase: "buffering" | "live";
18
+ latestMsg: {
19
+ id?: string;
20
+ groupId: string;
21
+ senderPubkey: string;
22
+ senderNametag?: string;
23
+ content: string;
24
+ timestamp: number;
25
+ replyToId?: string;
26
+ } | null;
27
+ bufferedCount: number;
28
+ timer: ReturnType<typeof setTimeout> | null;
29
+ }
30
+
13
31
  // ---------------------------------------------------------------------------
14
32
  // Account config shape (read from openclaw config under channels.unicity)
15
33
  // ---------------------------------------------------------------------------
@@ -422,21 +440,22 @@ export const unicityChannelPlugin = {
422
440
  });
423
441
  });
424
442
 
425
- // Subscribe to incoming group messages
426
- const unsubGroupMessage = sphere.groupChat?.onMessage?.((msg: {
427
- id: string;
443
+ // -- Group message dispatch helper & backfill debounce --------------------
444
+
445
+ type GroupMsg = {
446
+ id?: string;
428
447
  groupId: string;
429
- groupName?: string;
430
448
  senderPubkey: string;
431
449
  senderNametag?: string;
432
450
  content: string;
433
- timestamp?: number;
434
- }) => {
435
- // Skip messages from self
436
- if (msg.senderPubkey === sphere.identity?.chainPubkey) return;
451
+ timestamp: number;
452
+ replyToId?: string;
453
+ };
437
454
 
455
+ function dispatchGroupMessage(msg: GroupMsg): void {
438
456
  const senderName = msg.senderNametag ?? msg.senderPubkey.slice(0, 12);
439
- const groupName = msg.groupName ?? msg.groupId;
457
+ const groupData = sphere.groupChat?.getGroup?.(msg.groupId);
458
+ const groupName = groupData?.name ?? msg.groupId;
440
459
  const isOwner = isSenderOwner(msg.senderPubkey, msg.senderNametag);
441
460
  const metadataHeader = `[SenderName: ${senderName} | SenderId: ${msg.senderPubkey} | GroupId: ${msg.groupId} | GroupName: ${groupName} | IsOwner: ${isOwner} | CommandAuthorized: ${isOwner}]`;
442
461
  const sanitizedContent = msg.content.replace(/\[(?:SenderName|SenderId|IsOwner|CommandAuthorized|GroupId|GroupName)\s*:/gi, "[BLOCKED:");
@@ -487,40 +506,89 @@ export const unicityChannelPlugin = {
487
506
  .catch((err: unknown) => {
488
507
  ctx.log?.error(`[${ctx.account.accountId}] Group message dispatch error: ${err}`);
489
508
  });
509
+ }
510
+
511
+ // Per-group backfill state: buffer messages during the initial burst, then
512
+ // switch to live dispatch once the burst settles.
513
+ const groupBackfillStates = new Map<string, GroupBackfillState>();
514
+
515
+ // Subscribe to incoming group messages
516
+ const unsubGroupMessage = sphere.groupChat?.onMessage?.((msg: GroupMsg) => {
517
+ // Skip messages from self
518
+ if (msg.senderPubkey === sphere.identity?.chainPubkey) return;
519
+
520
+ // Lookup or create per-group backfill state
521
+ let state = groupBackfillStates.get(msg.groupId);
522
+ if (!state) {
523
+ state = { phase: "buffering", latestMsg: null, bufferedCount: 0, timer: null };
524
+ groupBackfillStates.set(msg.groupId, state);
525
+ }
526
+
527
+ // Already past backfill — dispatch immediately
528
+ if (state.phase === "live") {
529
+ dispatchGroupMessage(msg);
530
+ return;
531
+ }
532
+
533
+ // BUFFERING: keep only the latest message, reset the debounce timer
534
+ state.latestMsg = msg;
535
+ state.bufferedCount++;
536
+ if (state.timer) clearTimeout(state.timer);
537
+ state.timer = setTimeout(() => {
538
+ state!.phase = "live";
539
+ state!.timer = null;
540
+ ctx.log?.info(
541
+ `[${ctx.account.accountId}] Group backfill settled for ${msg.groupId}, ${state!.bufferedCount} message(s) buffered`,
542
+ );
543
+ // Dispatch the most recent buffered message so the agent has context
544
+ if (state!.latestMsg) {
545
+ dispatchGroupMessage(state!.latestMsg);
546
+ state!.latestMsg = null;
547
+ }
548
+ }, GROUP_BACKFILL_DEBOUNCE_MS);
490
549
  }) ?? (() => {});
491
550
 
492
551
  // Subscribe to group lifecycle events and notify owner
493
- const unsubGroupJoined = sphere.on?.("groupchat:joined", (event: { id: string; name?: string }) => {
552
+ const unsubGroupJoined = sphere.on?.("groupchat:joined", (event: { groupId: string; groupName: string }) => {
494
553
  const owner = getOwnerIdentity();
495
554
  if (owner) {
496
- const label = event.name ? `${event.name} (${event.id})` : event.id;
555
+ const label = event.groupName ? `${event.groupName} (${event.groupId})` : event.groupId;
497
556
  sphere.communications.sendDM(`@${owner}`, `I joined group ${label}`).catch((err) => {
498
557
  ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group join: ${err}`);
499
558
  });
500
559
  }
501
560
  }) ?? (() => {});
502
561
 
503
- const unsubGroupLeft = sphere.on?.("groupchat:left", (event: { id: string; name?: string }) => {
562
+ const unsubGroupLeft = sphere.on?.("groupchat:left", (event: { groupId: string }) => {
504
563
  const owner = getOwnerIdentity();
505
564
  if (owner) {
506
- const label = event.name ?? event.id;
565
+ const groupData = sphere.groupChat?.getGroup?.(event.groupId);
566
+ const label = groupData?.name ?? event.groupId;
507
567
  sphere.communications.sendDM(`@${owner}`, `I left group ${label}`).catch((err) => {
508
568
  ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group leave: ${err}`);
509
569
  });
510
570
  }
511
571
  }) ?? (() => {});
512
572
 
513
- const unsubGroupKicked = sphere.on?.("groupchat:kicked", (event: { id: string; name?: string }) => {
573
+ const unsubGroupKicked = sphere.on?.("groupchat:kicked", (event: { groupId: string; groupName: string }) => {
514
574
  const owner = getOwnerIdentity();
515
575
  if (owner) {
516
- const label = event.name ?? event.id;
576
+ const label = event.groupName ? `${event.groupName} (${event.groupId})` : event.groupId;
517
577
  sphere.communications.sendDM(`@${owner}`, `I was kicked from group ${label}`).catch((err) => {
518
578
  ctx.log?.error(`[${ctx.account.accountId}] Failed to notify owner about group kick: ${err}`);
519
579
  });
520
580
  }
521
581
  }) ?? (() => {});
522
582
 
583
+ function clearBackfillTimers(): void {
584
+ for (const state of groupBackfillStates.values()) {
585
+ if (state.timer) clearTimeout(state.timer);
586
+ }
587
+ groupBackfillStates.clear();
588
+ }
589
+
523
590
  ctx.abortSignal.addEventListener("abort", () => {
591
+ clearBackfillTimers();
524
592
  unsub();
525
593
  unsubTransfer();
526
594
  unsubPaymentRequest();
@@ -532,6 +600,7 @@ export const unicityChannelPlugin = {
532
600
 
533
601
  return {
534
602
  stop: () => {
603
+ clearBackfillTimers();
535
604
  unsub();
536
605
  unsubTransfer();
537
606
  unsubPaymentRequest();
@@ -28,8 +28,10 @@ export const createPrivateGroupTool = {
28
28
  visibility: "private",
29
29
  });
30
30
 
31
- const invite = await sphere.groupChat.createInvite(group.id);
32
- const joinCode = invite.code;
31
+ const joinCode = await sphere.groupChat.createInvite(group.id);
32
+ if (!joinCode) {
33
+ throw new Error("Failed to generate invite code for the private group");
34
+ }
33
35
 
34
36
  const lines = [
35
37
  `Private group created: ${group.name} (${group.id})`,