a2a-xmtp 1.4.0 → 1.4.2

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.0",
3
+ "version": "1.4.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "Decentralized Agent-to-Agent E2EE messaging for OpenClaw via XMTP",
@@ -39,6 +39,7 @@ export class GroupScheduler {
39
39
 
40
40
  /**
41
41
  * 初始化或获取群组状态。成员变化时自动重算。
42
+ * @param members 参与轮询的成员地址(调用方已按角色过滤,仅含 agent)
42
43
  */
43
44
  getOrInitGroup(groupId: string, members: string[]): GroupConversationState {
44
45
  const existing = this.groups.get(groupId);
@@ -122,15 +122,16 @@ export class MessageOrchestrator {
122
122
  const replyText = this.extractReplyText(messages);
123
123
  if (!replyText) return;
124
124
 
125
- // 记录本 agent turn
126
- this.policyEngine.recordTurn(payload.conversation.id);
127
-
125
+ // recordTurn bridge.sendMessage 内部调用,此处不重复
128
126
  await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
129
127
  conversationId: payload.conversation.id,
130
128
  });
131
129
 
132
- // 群聊:清除 claim 状态(回复已发送)
130
+ // 群聊:计入自己的消息 + 清除 claim
133
131
  if (payload.conversation.isGroup) {
132
+ // 自己发的消息 handleIncoming 不会收到(跳过 self),
133
+ // 手动递增以保持与其他 agent 的 messageCount 同步
134
+ this.groupScheduler.recordMessage(payload.conversation.id);
134
135
  this.groupScheduler.clearClaim(payload.conversation.id);
135
136
  }
136
137
 
@@ -152,11 +153,17 @@ export class MessageOrchestrator {
152
153
  payload: A2AInjectPayload,
153
154
  ): Promise<boolean> {
154
155
  const convId = payload.conversation.id;
155
- const members = payload.conversation.participants;
156
156
  const myAddress = bridge.address;
157
157
 
158
+ // 用 XMTP 原生角色信息筛选:只有普通 Member (permissionLevel=0) 参与轮询
159
+ // Admin/SuperAdmin 通常是人类,不参与 round-robin
160
+ const details = payload.conversation.participantDetails;
161
+ const agentMembers = details
162
+ ? details.filter((p) => p.permissionLevel === 0).map((p) => p.address)
163
+ : payload.conversation.participants; // fallback:无角色信息时用全部成员
164
+
158
165
  // 初始化群组状态
159
- this.groupScheduler.getOrInitGroup(convId, members);
166
+ this.groupScheduler.getOrInitGroup(convId, agentMembers);
160
167
 
161
168
  // 检查 turn budget 是否耗尽
162
169
  if (this.policyEngine.isTurnExhausted(convId)) {
@@ -28,10 +28,11 @@ export class PolicyEngine {
28
28
  this.localAgentIds.add(agentId);
29
29
  }
30
30
 
31
- checkOutgoing(params: PolicyCheckParams): PolicyCheckResult {
32
- return this.check(params);
33
- }
34
-
31
+ /**
32
+ * 入站检查:consent + TTL(不检查 turn budget 和 cool-down)
33
+ * turn budget 仅在出站/调度层检查,确保 turn 耗尽后仍能接收消息、
34
+ * 缓存到 inbox、维持 messageCount 同步。
35
+ */
35
36
  checkIncoming(params: PolicyCheckParams): PolicyCheckResult {
36
37
  const consent = this.getConsent(params.from);
37
38
  if (consent === "deny") {
@@ -43,10 +44,19 @@ export class PolicyEngine {
43
44
  reason: `Sender ${params.from} not explicitly allowed (consent: ${consent})`,
44
45
  };
45
46
  }
46
- return this.check(params);
47
+ const state = this.getOrCreateState(params.conversationId);
48
+ const now = Date.now();
49
+ const ttlMs = this.policy.ttlMinutes * 60 * 1000;
50
+ if (now - state.createdAt > ttlMs) {
51
+ return { allowed: false, reason: `Conversation TTL expired (${this.policy.ttlMinutes} min)` };
52
+ }
53
+ return { allowed: true };
47
54
  }
48
55
 
49
- private check(params: PolicyCheckParams): PolicyCheckResult {
56
+ /**
57
+ * 出站检查:TTL + turn budget + cool-down
58
+ */
59
+ checkOutgoing(params: PolicyCheckParams): PolicyCheckResult {
50
60
  const state = this.getOrCreateState(params.conversationId);
51
61
  const now = Date.now();
52
62
 
@@ -7,7 +7,7 @@ import { Agent, createUser, createSigner } from "@xmtp/agent-sdk";
7
7
  import type { IdentityRegistry } from "./identity-registry.js";
8
8
  import { PolicyEngine } from "../coordination/policy-engine.js";
9
9
  import type { XmtpWalletConfig, InboxMessage, A2AContentType, A2AInjectPayload, GroupInfo } from "../types.js";
10
- import { createA2APayload, CLAIM_PREFIX } from "../types.js";
10
+ import { createA2APayload, CLAIM_PREFIX, type GroupParticipant } from "../types.js";
11
11
  import { GroupScheduler } from "../coordination/group-scheduler.js";
12
12
 
13
13
  /** IdentifierKind.Ethereum = 0 (const enum, 不能在 isolatedModules 下直接访问) */
@@ -258,8 +258,9 @@ export class XmtpBridge {
258
258
  const convState = this.policyEngine.getConversationState(ctx.conversation.id);
259
259
  const turn = convState?.turn ?? 0;
260
260
 
261
- // 群聊时获取参与者列表
261
+ // 群聊时获取参与者列表(含角色信息)
262
262
  let participants: string[] = [];
263
+ let participantDetails: GroupParticipant[] | undefined;
263
264
  const isGroup = ctx.conversation.isGroup ?? false;
264
265
  if (isGroup) {
265
266
  try {
@@ -267,6 +268,10 @@ export class XmtpBridge {
267
268
  participants = members.map((m: any) =>
268
269
  m.accountAddresses?.[0]?.toLowerCase() ?? m.inboxId,
269
270
  );
271
+ participantDetails = members.map((m: any) => ({
272
+ address: (m.accountAddresses?.[0]?.toLowerCase() ?? m.inboxId) as string,
273
+ permissionLevel: (m.permissionLevel ?? 0) as number,
274
+ }));
270
275
  } catch { /* 获取成员失败时保持空数组 */ }
271
276
  }
272
277
 
@@ -276,6 +281,7 @@ export class XmtpBridge {
276
281
  conversationId: ctx.conversation.id,
277
282
  isGroup,
278
283
  participants,
284
+ participantDetails,
279
285
  messageId: ctx.message.id ?? crypto.randomUUID(),
280
286
  content,
281
287
  contentType,
package/src/types.ts CHANGED
@@ -12,6 +12,13 @@ export interface XmtpWalletConfig {
12
12
 
13
13
  export type XmtpEnv = "dev" | "production";
14
14
 
15
+ /** 群成员信息(含角色) */
16
+ export interface GroupParticipant {
17
+ address: string;
18
+ /** XMTP PermissionLevel: 0=Member, 1=Admin, 2=SuperAdmin */
19
+ permissionLevel: number;
20
+ }
21
+
15
22
  /** 注入 Agent session 的消息格式 */
16
23
  export interface A2AInjectPayload {
17
24
  type: "a2a-xmtp";
@@ -24,6 +31,8 @@ export interface A2AInjectPayload {
24
31
  id: string;
25
32
  isGroup: boolean;
26
33
  participants: string[];
34
+ /** 群成员详细信息(含角色),仅群聊时有值 */
35
+ participantDetails?: GroupParticipant[];
27
36
  };
28
37
  message: {
29
38
  id: string;
@@ -86,8 +95,8 @@ export interface GroupConversationState {
86
95
  lastClaim: { sender: string; timestamp: number; messageId: string } | null;
87
96
  }
88
97
 
89
- /** Claim 消息前缀(零宽空格 + [processing]:) */
90
- export const CLAIM_PREFIX = "\u200B[processing]:";
98
+ /** Claim 消息前缀(可见标记,避免 XMTP 传输时被 strip) */
99
+ export const CLAIM_PREFIX = "__CLAIM__:";
91
100
 
92
101
  /** 默认群聊调度配置 */
93
102
  export const DEFAULT_GROUP_SCHEDULING: GroupSchedulingConfig = {
@@ -150,6 +159,7 @@ export function createA2APayload(params: {
150
159
  conversationId: string;
151
160
  isGroup: boolean;
152
161
  participants: string[];
162
+ participantDetails?: GroupParticipant[];
153
163
  messageId: string;
154
164
  content: string;
155
165
  contentType: A2AContentType;
@@ -168,6 +178,7 @@ export function createA2APayload(params: {
168
178
  id: params.conversationId,
169
179
  isGroup: params.isGroup,
170
180
  participants: params.participants,
181
+ participantDetails: params.participantDetails,
171
182
  },
172
183
  message: {
173
184
  id: params.messageId,