a2a-xmtp 1.4.2 → 1.4.3

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.2",
3
+ "version": "1.4.3",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "Decentralized Agent-to-Agent E2EE messaging for OpenClaw via XMTP",
@@ -66,6 +66,14 @@ export class MessageOrchestrator {
66
66
  }
67
67
  }
68
68
 
69
+ /**
70
+ * 群聊 self 消息计数(由 bridge.onSelfGroupMessage 触发)
71
+ * 保持 messageCount 与其他 agent 同步。
72
+ */
73
+ handleSelfGroupMessage(conversationId: string): void {
74
+ this.groupScheduler.recordMessage(conversationId);
75
+ }
76
+
69
77
  /**
70
78
  * 处理收到的 XMTP 消息:群聊 round-robin 调度 → LLM 推理 → 安全校验 → 回复
71
79
  */
@@ -122,16 +130,21 @@ export class MessageOrchestrator {
122
130
  const replyText = this.extractReplyText(messages);
123
131
  if (!replyText) return;
124
132
 
133
+ // 发送前最终检查 turn budget(收窄 race window)
134
+ if (payload.conversation.isGroup && this.policyEngine.isTurnExhausted(payload.conversation.id)) {
135
+ this.logger.info(
136
+ `[a2a-xmtp] Turn budget exhausted before send in ${payload.conversation.id}, dropping reply`,
137
+ );
138
+ return;
139
+ }
140
+
125
141
  // recordTurn 由 bridge.sendMessage 内部调用,此处不重复
126
142
  await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
127
143
  conversationId: payload.conversation.id,
128
144
  });
129
145
 
130
- // 群聊:计入自己的消息 + 清除 claim
146
+ // 群聊:清除 claim(self 消息计数由 bridge.onSelfGroupMessage 处理)
131
147
  if (payload.conversation.isGroup) {
132
- // 自己发的消息 handleIncoming 不会收到(跳过 self),
133
- // 手动递增以保持与其他 agent 的 messageCount 同步
134
- this.groupScheduler.recordMessage(payload.conversation.id);
135
148
  this.groupScheduler.clearClaim(payload.conversation.id);
136
149
  }
137
150
 
@@ -29,9 +29,10 @@ export class PolicyEngine {
29
29
  }
30
30
 
31
31
  /**
32
- * 入站检查:consent + TTL(不检查 turn budget cool-down
33
- * turn budget 仅在出站/调度层检查,确保 turn 耗尽后仍能接收消息、
34
- * 缓存到 inbox、维持 messageCount 同步。
32
+ * 入站检查:consent + TTL + turn budget(不检查 cool-down
33
+ * 否则刚发完消息后收到的回复会被静默丢弃)。
34
+ * 注意:turn budget 拦截不影响 messageCount 同步,因为
35
+ * self 消息通过 bridge.onSelfGroupMessage 独立计数。
35
36
  */
36
37
  checkIncoming(params: PolicyCheckParams): PolicyCheckResult {
37
38
  const consent = this.getConsent(params.from);
@@ -50,6 +51,9 @@ export class PolicyEngine {
50
51
  if (now - state.createdAt > ttlMs) {
51
52
  return { allowed: false, reason: `Conversation TTL expired (${this.policy.ttlMinutes} min)` };
52
53
  }
54
+ if (state.turn >= this.policy.maxTurns) {
55
+ return { allowed: false, reason: `Turn budget exhausted (${state.turn}/${this.policy.maxTurns})` };
56
+ }
53
57
  return { allowed: true };
54
58
  }
55
59
 
package/src/index.ts CHANGED
@@ -186,6 +186,10 @@ export default definePluginEntry({
186
186
  bridge.onClaim = (conversationId, senderAddress, messageId) =>
187
187
  orchestrator.handleClaim(conversationId, senderAddress, messageId);
188
188
 
189
+ // 群聊 self 消息计数回调:保持 messageCount 同步
190
+ bridge.onSelfGroupMessage = (conversationId) =>
191
+ orchestrator.handleSelfGroupMessage(conversationId);
192
+
189
193
  await bridge.start();
190
194
  bridges.set(AGENT_ID, bridge);
191
195
 
@@ -28,6 +28,9 @@ export class XmtpBridge {
28
28
  /** Claim 消息回调(由 orchestrator 设置,用于群聊调度) */
29
29
  onClaim?: (conversationId: string, senderAddress: string, messageId: string) => void;
30
30
 
31
+ /** 群聊 self 消息计数回调(由 orchestrator 设置,用于 round-robin 同步) */
32
+ onSelfGroupMessage?: (conversationId: string) => void;
33
+
31
34
  constructor(
32
35
  readonly agentId: string,
33
36
  private walletConfig: XmtpWalletConfig,
@@ -232,9 +235,7 @@ export class XmtpBridge {
232
235
 
233
236
  private async handleIncoming(ctx: any, contentType: A2AContentType): Promise<void> {
234
237
  const senderAddress: string = await ctx.getSenderAddress();
235
-
236
- if (senderAddress.toLowerCase() === this.address.toLowerCase()) return;
237
-
238
+ const isSelf = senderAddress.toLowerCase() === this.address.toLowerCase();
238
239
  const content = String(ctx.message.content);
239
240
 
240
241
  // 识别 claim 消息:不计数、不缓存、通知调度器
@@ -246,6 +247,15 @@ export class XmtpBridge {
246
247
  return;
247
248
  }
248
249
 
250
+ // 群聊 self 消息:仅计数(保持 messageCount 同步),不触发响应
251
+ const isGroup = ctx.conversation.isGroup ?? false;
252
+ if (isSelf) {
253
+ if (isGroup && this.onSelfGroupMessage) {
254
+ this.onSelfGroupMessage(ctx.conversation.id);
255
+ }
256
+ return;
257
+ }
258
+
249
259
  const senderAgentId = await this.registry.resolveAgentId(senderAddress);
250
260
 
251
261
  const policyResult = this.policyEngine.checkIncoming({
@@ -261,7 +271,6 @@ export class XmtpBridge {
261
271
  // 群聊时获取参与者列表(含角色信息)
262
272
  let participants: string[] = [];
263
273
  let participantDetails: GroupParticipant[] | undefined;
264
- const isGroup = ctx.conversation.isGroup ?? false;
265
274
  if (isGroup) {
266
275
  try {
267
276
  const members = await ctx.conversation.members();