@unicitylabs/openclaw-unicity 0.5.2 → 0.5.4
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 +1 -1
- package/src/channel.ts +56 -20
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -248,23 +248,24 @@ export const unicityChannelPlugin = {
|
|
|
248
248
|
const seenDmIds = new Set<string>();
|
|
249
249
|
const DM_SEEN_MAX = 1000;
|
|
250
250
|
|
|
251
|
+
// Collect all known representations of "self" so we can detect echoed-back
|
|
252
|
+
// messages. The SDK's built-in self-check compares transport pubkey against
|
|
253
|
+
// chainPubkey, but those may differ in format (33-byte compressed vs 32-byte
|
|
254
|
+
// x-only Nostr key), letting self-messages slip through.
|
|
255
|
+
const selfPubkeys = new Set<string>();
|
|
256
|
+
if (sphere.identity?.chainPubkey) selfPubkeys.add(sphere.identity.chainPubkey);
|
|
257
|
+
const myNostrPubkey = sphere.groupChat?.getMyPublicKey?.() ?? null;
|
|
258
|
+
if (myNostrPubkey) selfPubkeys.add(myNostrPubkey);
|
|
251
259
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const first = seenDmIds.values().next().value!;
|
|
259
|
-
seenDmIds.delete(first);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
260
|
+
// Per-sender queue: while a reply is being generated for a sender, hold
|
|
261
|
+
// the latest incoming message so it gets processed after the current reply
|
|
262
|
+
// finishes. Prevents duplicate replies while not dropping follow-ups.
|
|
263
|
+
type DmMsg = { id: string; senderPubkey: string; senderNametag?: string; recipientPubkey: string; content: string; timestamp: number; isRead: boolean };
|
|
264
|
+
const sendersInFlight = new Set<string>();
|
|
265
|
+
const pendingPerSender = new Map<string, DmMsg>();
|
|
262
266
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
ctx.log?.debug(`[${ctx.account.accountId}] Skipping historical DM (ts=${msg.timestamp} < start=${dmStartTime})`);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
267
|
+
function dispatchDm(msg: DmMsg): void {
|
|
268
|
+
sendersInFlight.add(msg.senderPubkey);
|
|
268
269
|
|
|
269
270
|
// Immediately signal that we're composing a reply
|
|
270
271
|
sphere.communications.sendComposingIndicator(msg.senderPubkey)
|
|
@@ -343,7 +344,47 @@ export const unicityChannelPlugin = {
|
|
|
343
344
|
})
|
|
344
345
|
.catch((err: unknown) => {
|
|
345
346
|
ctx.log?.error(`[${ctx.account.accountId}] Reply dispatch error: ${err}`);
|
|
347
|
+
})
|
|
348
|
+
.finally(() => {
|
|
349
|
+
sendersInFlight.delete(msg.senderPubkey);
|
|
350
|
+
// If a follow-up message arrived while we were replying, process it now
|
|
351
|
+
const pending = pendingPerSender.get(msg.senderPubkey);
|
|
352
|
+
if (pending) {
|
|
353
|
+
pendingPerSender.delete(msg.senderPubkey);
|
|
354
|
+
ctx.log?.info(`[${ctx.account.accountId}] Processing queued DM from ${peerId}`);
|
|
355
|
+
dispatchDm(pending);
|
|
356
|
+
}
|
|
346
357
|
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const unsub = sphere.communications.onDirectMessage((msg) => {
|
|
361
|
+
// Skip messages from self (own DMs echoed back by the relay)
|
|
362
|
+
if (selfPubkeys.has(msg.senderPubkey)) return;
|
|
363
|
+
|
|
364
|
+
// Deduplicate: skip already-processed messages (relays may deliver dupes)
|
|
365
|
+
if (msg.id && seenDmIds.has(msg.id)) return;
|
|
366
|
+
if (msg.id) {
|
|
367
|
+
seenDmIds.add(msg.id);
|
|
368
|
+
if (seenDmIds.size > DM_SEEN_MAX) {
|
|
369
|
+
const first = seenDmIds.values().next().value!;
|
|
370
|
+
seenDmIds.delete(first);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Skip historical messages replayed on connect
|
|
375
|
+
if (msg.timestamp && msg.timestamp < dmStartTime) {
|
|
376
|
+
ctx.log?.debug(`[${ctx.account.accountId}] Skipping historical DM (ts=${msg.timestamp} < start=${dmStartTime})`);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Per-sender lock: queue if we're already generating a reply for this sender
|
|
381
|
+
if (sendersInFlight.has(msg.senderPubkey)) {
|
|
382
|
+
ctx.log?.debug(`[${ctx.account.accountId}] Queuing DM from ${msg.senderPubkey.slice(0, 16)}… — reply in flight`);
|
|
383
|
+
pendingPerSender.set(msg.senderPubkey, msg);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
dispatchDm(msg);
|
|
347
388
|
});
|
|
348
389
|
|
|
349
390
|
ctx.log?.info(`[${ctx.account.accountId}] Unicity DM listener active`);
|
|
@@ -480,11 +521,6 @@ export const unicityChannelPlugin = {
|
|
|
480
521
|
replyToId?: string;
|
|
481
522
|
};
|
|
482
523
|
|
|
483
|
-
// Nostr pubkey for self-message detection and reply-to-self detection.
|
|
484
|
-
// Group messages use the 32-byte x-only Nostr pubkey (event.pubkey),
|
|
485
|
-
// NOT the 33-byte compressed chainPubkey.
|
|
486
|
-
const myNostrPubkey = sphere.groupChat?.getMyPublicKey?.() ?? null;
|
|
487
|
-
|
|
488
524
|
// Detect if a group message is a reply to one of the agent's own messages.
|
|
489
525
|
// Used to set WasMentioned so the mention gate treats replies-to-self as
|
|
490
526
|
// implicit mentions (same pattern Discord uses for thread replies).
|