a2a-xmtp 1.4.5 → 1.4.6

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": "a2a-xmtp",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "Decentralized Agent-to-Agent E2EE messaging for OpenClaw via XMTP",
@@ -25,6 +25,9 @@
25
25
  "@xmtp/agent-sdk": "^2.3.0",
26
26
  "@sinclair/typebox": "^0.34.0"
27
27
  },
28
+ "peerDependencies": {
29
+ "openclaw": "*"
30
+ },
28
31
  "devDependencies": {
29
32
  "typescript": "^5.7.0",
30
33
  "vitest": "^3.0.0",
@@ -22,6 +22,14 @@ export class XmtpBridge {
22
22
  private inboxBuffer: InboxMessage[] = [];
23
23
  private readonly maxInboxBuffer = 100;
24
24
 
25
+ /** 已处理消息 ID 去重集合(防止 SDK 双投或 sync 重放) */
26
+ private processedMsgIds = new Set<string>();
27
+ private readonly maxProcessedIds = 200;
28
+
29
+ /** 出站去重:conversationId+contentHash → 发送结果(10s TTL) */
30
+ private recentSends = new Map<string, { conversationId: string; messageId: string }>();
31
+ private readonly sendDedupWindowMs = 3_000;
32
+
25
33
  /** 消息回调(由 plugin 入口设置,用于触发 subagent 处理) */
26
34
  onMessage?: (agentId: string, payload: A2AInjectPayload) => void;
27
35
 
@@ -97,13 +105,22 @@ export class XmtpBridge {
97
105
  conversation = await (this.agent as any).createDmWithAddress(toAddress);
98
106
  }
99
107
 
108
+ // 出站去重:同一会话 + 相同内容在 10s 内只发一次
109
+ const dedupKey = `${conversation.id}:${simpleHash(content)}`;
110
+ const cached = this.recentSends.get(dedupKey);
111
+ if (cached) return cached;
112
+
100
113
  const messageId = opts?.contentType === "markdown"
101
114
  ? await conversation.sendMarkdown(content)
102
115
  : await conversation.sendText(content);
103
116
 
104
117
  this.policyEngine.recordTurn(conversation.id);
105
118
 
106
- return { conversationId: conversation.id, messageId: String(messageId) };
119
+ const result = { conversationId: conversation.id, messageId: String(messageId) };
120
+ this.recentSends.set(dedupKey, result);
121
+ setTimeout(() => this.recentSends.delete(dedupKey), this.sendDedupWindowMs);
122
+
123
+ return result;
107
124
  }
108
125
 
109
126
  /**
@@ -235,6 +252,19 @@ export class XmtpBridge {
235
252
  }
236
253
 
237
254
  private async handleIncoming(ctx: any, contentType: A2AContentType): Promise<void> {
255
+ // 消息去重:同一 XMTP 消息 ID 只处理一次
256
+ // 防止 SDK 双投(text+markdown 同时触发)或 conversations.sync() 重放
257
+ const rawMsgId: string | undefined = ctx.message.id;
258
+ if (rawMsgId) {
259
+ if (this.processedMsgIds.has(rawMsgId)) return;
260
+ this.processedMsgIds.add(rawMsgId);
261
+ if (this.processedMsgIds.size > this.maxProcessedIds) {
262
+ // 淘汰最旧的条目
263
+ const first = this.processedMsgIds.values().next().value!;
264
+ this.processedMsgIds.delete(first);
265
+ }
266
+ }
267
+
238
268
  const senderAddress: string = await ctx.getSenderAddress();
239
269
  const isSelf = senderAddress.toLowerCase() === this.address.toLowerCase();
240
270
  const content = String(ctx.message.content);
@@ -328,3 +358,13 @@ export class XmtpBridge {
328
358
  return this.walletConfig.env;
329
359
  }
330
360
  }
361
+
362
+ /** FNV-1a 32-bit hash — fast, no crypto dependency */
363
+ function simpleHash(str: string): string {
364
+ let h = 0x811c9dc5;
365
+ for (let i = 0; i < str.length; i++) {
366
+ h ^= str.charCodeAt(i);
367
+ h = Math.imul(h, 0x01000193);
368
+ }
369
+ return (h >>> 0).toString(16);
370
+ }