@unicitylabs/openclaw-unicity 0.5.3 → 0.5.5
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 +2 -2
- package/src/channel.ts +64 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unicitylabs/openclaw-unicity",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@clack/prompts": "^0.10.0",
|
|
46
46
|
"@sinclair/typebox": "^0.34.48",
|
|
47
|
-
"@unicitylabs/sphere-sdk": "^0.
|
|
47
|
+
"@unicitylabs/sphere-sdk": "^0.6.7"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"openclaw": "*"
|
package/src/channel.ts
CHANGED
|
@@ -244,38 +244,39 @@ export const unicityChannelPlugin = {
|
|
|
244
244
|
|
|
245
245
|
// Track DM start time and seen IDs to skip historical replays from the relay.
|
|
246
246
|
// The SDK fires onDirectMessage for every cached/replayed DM on connect.
|
|
247
|
-
|
|
247
|
+
// msg.timestamp is milliseconds, so dmStartTime must be ms too.
|
|
248
|
+
const dmStartTime = Date.now();
|
|
248
249
|
const seenDmIds = new Set<string>();
|
|
249
250
|
const DM_SEEN_MAX = 1000;
|
|
250
251
|
|
|
251
252
|
// Collect all known representations of "self" so we can detect echoed-back
|
|
252
|
-
// messages.
|
|
253
|
-
//
|
|
254
|
-
//
|
|
253
|
+
// messages. We must track BOTH the 33-byte compressed chainPubkey (02/03…)
|
|
254
|
+
// and the 32-byte x-only Nostr transport key (chainPubkey without prefix).
|
|
255
|
+
// The SDK's own self-check in CommunicationsModule has a bug where it
|
|
256
|
+
// compares transport pubkey (32-byte) against chainPubkey (33-byte), which
|
|
257
|
+
// never matches — so self-messages can leak through to dmHandlers.
|
|
255
258
|
const selfPubkeys = new Set<string>();
|
|
256
|
-
if (sphere.identity?.chainPubkey)
|
|
259
|
+
if (sphere.identity?.chainPubkey) {
|
|
260
|
+
const cpk = sphere.identity.chainPubkey;
|
|
261
|
+
selfPubkeys.add(cpk);
|
|
262
|
+
// x-only transport key = compressed pubkey without the 02/03 prefix
|
|
263
|
+
if (cpk.length === 66 && (cpk.startsWith("02") || cpk.startsWith("03"))) {
|
|
264
|
+
selfPubkeys.add(cpk.slice(2));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Also add the groupChat key (same value but grabbed independently for safety)
|
|
257
268
|
const myNostrPubkey = sphere.groupChat?.getMyPublicKey?.() ?? null;
|
|
258
269
|
if (myNostrPubkey) selfPubkeys.add(myNostrPubkey);
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
271
|
+
// Per-sender queue: while a reply is being generated for a sender, hold
|
|
272
|
+
// the latest incoming message so it gets processed after the current reply
|
|
273
|
+
// finishes. Prevents duplicate replies while not dropping follow-ups.
|
|
274
|
+
type DmMsg = { id: string; senderPubkey: string; senderNametag?: string; recipientPubkey: string; content: string; timestamp: number; isRead: boolean };
|
|
275
|
+
const sendersInFlight = new Set<string>();
|
|
276
|
+
const pendingPerSender = new Map<string, DmMsg>();
|
|
263
277
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (msg.id) {
|
|
267
|
-
seenDmIds.add(msg.id);
|
|
268
|
-
if (seenDmIds.size > DM_SEEN_MAX) {
|
|
269
|
-
const first = seenDmIds.values().next().value!;
|
|
270
|
-
seenDmIds.delete(first);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Skip historical messages replayed on connect
|
|
275
|
-
if (msg.timestamp && msg.timestamp < dmStartTime) {
|
|
276
|
-
ctx.log?.debug(`[${ctx.account.accountId}] Skipping historical DM (ts=${msg.timestamp} < start=${dmStartTime})`);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
278
|
+
function dispatchDm(msg: DmMsg): void {
|
|
279
|
+
sendersInFlight.add(msg.senderPubkey);
|
|
279
280
|
|
|
280
281
|
// Immediately signal that we're composing a reply
|
|
281
282
|
sphere.communications.sendComposingIndicator(msg.senderPubkey)
|
|
@@ -354,7 +355,47 @@ export const unicityChannelPlugin = {
|
|
|
354
355
|
})
|
|
355
356
|
.catch((err: unknown) => {
|
|
356
357
|
ctx.log?.error(`[${ctx.account.accountId}] Reply dispatch error: ${err}`);
|
|
358
|
+
})
|
|
359
|
+
.finally(() => {
|
|
360
|
+
sendersInFlight.delete(msg.senderPubkey);
|
|
361
|
+
// If a follow-up message arrived while we were replying, process it now
|
|
362
|
+
const pending = pendingPerSender.get(msg.senderPubkey);
|
|
363
|
+
if (pending) {
|
|
364
|
+
pendingPerSender.delete(msg.senderPubkey);
|
|
365
|
+
ctx.log?.info(`[${ctx.account.accountId}] Processing queued DM from ${peerId}`);
|
|
366
|
+
dispatchDm(pending);
|
|
367
|
+
}
|
|
357
368
|
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const unsub = sphere.communications.onDirectMessage((msg) => {
|
|
372
|
+
// Skip messages from self (own DMs echoed back by the relay)
|
|
373
|
+
if (selfPubkeys.has(msg.senderPubkey)) return;
|
|
374
|
+
|
|
375
|
+
// Deduplicate: skip already-processed messages (relays may deliver dupes)
|
|
376
|
+
if (msg.id && seenDmIds.has(msg.id)) return;
|
|
377
|
+
if (msg.id) {
|
|
378
|
+
seenDmIds.add(msg.id);
|
|
379
|
+
if (seenDmIds.size > DM_SEEN_MAX) {
|
|
380
|
+
const first = seenDmIds.values().next().value!;
|
|
381
|
+
seenDmIds.delete(first);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Skip historical messages replayed on connect
|
|
386
|
+
if (msg.timestamp && msg.timestamp < dmStartTime) {
|
|
387
|
+
ctx.log?.debug(`[${ctx.account.accountId}] Skipping historical DM (ts=${msg.timestamp} < start=${dmStartTime})`);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Per-sender lock: queue if we're already generating a reply for this sender
|
|
392
|
+
if (sendersInFlight.has(msg.senderPubkey)) {
|
|
393
|
+
ctx.log?.debug(`[${ctx.account.accountId}] Queuing DM from ${msg.senderPubkey.slice(0, 16)}… — reply in flight`);
|
|
394
|
+
pendingPerSender.set(msg.senderPubkey, msg);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
dispatchDm(msg);
|
|
358
399
|
});
|
|
359
400
|
|
|
360
401
|
ctx.log?.info(`[${ctx.account.accountId}] Unicity DM listener active`);
|