a2a-xmtp 1.4.3 → 1.4.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/README.md CHANGED
@@ -17,37 +17,7 @@ Decentralized Agent-to-Agent E2EE messaging for [OpenClaw](https://openclaw.ai)
17
17
  openclaw plugins install a2a-xmtp
18
18
  ```
19
19
 
20
- ## Configure
21
-
22
- Edit `~/.openclaw/openclaw.json`:
23
-
24
- ```json
25
- {
26
- "tools": {
27
- "profile": "full"
28
- },
29
- "plugins": {
30
- "entries": {
31
- "a2a-xmtp": {
32
- "enabled": true,
33
- "config": {
34
- "xmtp": {
35
- "env": "dev",
36
- "dbPath": "~/.openclaw/xmtp-data"
37
- }
38
- }
39
- }
40
- }
41
- }
42
- }
43
- ```
44
-
45
- Then restart:
46
-
47
- ```bash
48
- mkdir -p ~/.openclaw/xmtp-data
49
- openclaw gateway restart
50
- ```
20
+ That's it. The plugin auto-generates an XMTP wallet, creates data directories, and starts with sensible defaults.
51
21
 
52
22
  ## Verify
53
23
 
@@ -2,6 +2,7 @@
2
2
  "id": "a2a-xmtp",
3
3
  "name": "Agent-to-Agent IM (XMTP)",
4
4
  "description": "Decentralized Agent-to-Agent E2EE messaging powered by XMTP protocol",
5
+ "enabledByDefault": true,
5
6
  "configSchema": {
6
7
  "type": "object",
7
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2a-xmtp",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "description": "Decentralized Agent-to-Agent E2EE messaging for OpenClaw via XMTP",
@@ -108,48 +108,49 @@ export class MessageOrchestrator {
108
108
  const result = await this.subagentApi.waitForRun({ runId, timeoutMs: 60000 });
109
109
  if (result.status === "error") {
110
110
  this.logger.error(`[a2a-xmtp] Subagent error: ${result.error}`);
111
- } else if (result.status === "timeout") {
112
- this.logger.warn(`[a2a-xmtp] Subagent timeout for ${sessionKey}`);
111
+ return;
112
+ }
113
+ if (result.status === "timeout") {
114
+ this.logger.warn(`[a2a-xmtp] Subagent timeout for ${sessionKey}, checking for late reply...`);
113
115
  }
114
116
 
115
- if (result.status === "ok") {
116
- const { messages } = await this.subagentApi.getSessionMessages({
117
- sessionKey,
118
- limit: 5,
119
- });
117
+ // timeout LLM 可能已经返回了内容(如限流重试后成功),仍尝试提取回复
118
+ const { messages } = await this.subagentApi.getSessionMessages({
119
+ sessionKey,
120
+ limit: 5,
121
+ });
120
122
 
121
- // 安全检查:白名单机制
122
- if (this.hasForbiddenToolCalls(messages)) {
123
- this.logger.warn(
124
- `[a2a-xmtp] SECURITY: Blocked reply — LLM called non-whitelisted tool, triggered by XMTP message from ${senderLabel}. Possible prompt injection.`,
125
- );
126
- return;
127
- }
123
+ // 安全检查:白名单机制
124
+ if (this.hasForbiddenToolCalls(messages)) {
125
+ this.logger.warn(
126
+ `[a2a-xmtp] SECURITY: Blocked reply — LLM called non-whitelisted tool, triggered by XMTP message from ${senderLabel}. Possible prompt injection.`,
127
+ );
128
+ return;
129
+ }
128
130
 
129
- // 提取回复文本
130
- const replyText = this.extractReplyText(messages);
131
- if (!replyText) return;
131
+ // 提取回复文本
132
+ const replyText = this.extractReplyText(messages);
133
+ if (!replyText) return;
132
134
 
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
-
141
- // recordTurn 由 bridge.sendMessage 内部调用,此处不重复
142
- await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
143
- conversationId: payload.conversation.id,
144
- });
135
+ // 发送前最终检查 turn budget(收窄 race window)
136
+ if (payload.conversation.isGroup && this.policyEngine.isTurnExhausted(payload.conversation.id)) {
137
+ this.logger.info(
138
+ `[a2a-xmtp] Turn budget exhausted before send in ${payload.conversation.id}, dropping reply`,
139
+ );
140
+ return;
141
+ }
145
142
 
146
- // 群聊:清除 claim(self 消息计数由 bridge.onSelfGroupMessage 处理)
147
- if (payload.conversation.isGroup) {
148
- this.groupScheduler.clearClaim(payload.conversation.id);
149
- }
143
+ // recordTurn bridge.sendMessage 内部调用,此处不重复
144
+ await bridge.sendMessage(payload.from.xmtpAddress, replyText, {
145
+ conversationId: payload.conversation.id,
146
+ });
150
147
 
151
- this.logger.info(`[a2a-xmtp] Replied to ${senderLabel} in ${payload.conversation.id}`);
148
+ // 群聊:清除 claim(self 消息计数由 bridge.onSelfGroupMessage 处理)
149
+ if (payload.conversation.isGroup) {
150
+ this.groupScheduler.clearClaim(payload.conversation.id);
152
151
  }
152
+
153
+ this.logger.info(`[a2a-xmtp] Replied to ${senderLabel} in ${payload.conversation.id}`);
153
154
  } catch (err) {
154
155
  this.logger.error(
155
156
  `[a2a-xmtp] Failed to trigger subagent: ${err instanceof Error ? err.message : String(err)}`,
@@ -208,7 +209,10 @@ export class MessageOrchestrator {
208
209
 
209
210
  // 发送 claim
210
211
  try {
211
- await bridge.sendClaim(convId, payload.message.id);
212
+ const claimMsgId = await bridge.sendClaim(convId, payload.message.id);
213
+ this.logger.info(
214
+ `[a2a-xmtp] Claim sent: ${claimMsgId} in ${convId}`,
215
+ );
212
216
  } catch (err) {
213
217
  this.logger.warn(
214
218
  `[a2a-xmtp] Failed to send claim: ${err instanceof Error ? err.message : String(err)}`,
@@ -229,27 +233,48 @@ export class MessageOrchestrator {
229
233
  const timedOut = await waitWithAbort(decision.timeoutMs, abortCtrl.signal);
230
234
 
231
235
  if (!timedOut) {
232
- // 被取消 = 收到了 claim 或回复 保持沉默
236
+ // 收到了 claim → 等待 claim 过期,看指定回复者是否真的发出了回复
233
237
  this.logger.info(
234
- `[a2a-xmtp] Saw claim/reply in ${convId}, staying silent`,
238
+ `[a2a-xmtp] Saw claim in ${convId}, waiting for actual reply...`,
235
239
  );
236
- return false;
237
- }
240
+ const claimExpireMs = this.groupScheduler.getConfig().claimExpireMs;
241
+ await sleep(claimExpireMs);
242
+
243
+ // claim 已被清除 = 回复已发送 → 不需要接管
244
+ if (!this.groupScheduler.hasActiveClaim(convId) && !this.groupScheduler.isClaimExpired(convId)) {
245
+ this.logger.info(
246
+ `[a2a-xmtp] Claim cleared (reply sent) in ${convId}, staying silent`,
247
+ );
248
+ return false;
249
+ }
250
+
251
+ // claim 过期且未清除 = 指定回复者失败 → failover
252
+ this.logger.warn(
253
+ `[a2a-xmtp] Claim expired without reply in ${convId}, failing over`,
254
+ );
255
+ } else {
256
+ // 超时且没有 claim → 检查是否有未过期的 claim(可能在等待期间收到)
257
+ if (this.groupScheduler.hasActiveClaim(convId)) {
258
+ this.logger.info(
259
+ `[a2a-xmtp] Active claim exists in ${convId}, staying silent`,
260
+ );
261
+ return false;
262
+ }
238
263
 
239
- // 超时且没有 claim → 检查是否有未过期的 claim(可能在等待期间收到)
240
- if (this.groupScheduler.hasActiveClaim(convId)) {
241
264
  this.logger.info(
242
- `[a2a-xmtp] Active claim exists in ${convId}, staying silent`,
265
+ `[a2a-xmtp] No claim received in ${convId}, failing over`,
243
266
  );
244
- return false;
245
267
  }
246
268
 
247
- // 接管:发送 claim 并回复
269
+ // Failover:接管回复
248
270
  this.logger.info(
249
271
  `[a2a-xmtp] Failover: taking over msg #${msgIndex} in ${convId}`,
250
272
  );
251
273
  try {
252
- await bridge.sendClaim(convId, payload.message.id);
274
+ const claimMsgId = await bridge.sendClaim(convId, payload.message.id);
275
+ this.logger.info(
276
+ `[a2a-xmtp] Failover claim sent: ${claimMsgId} in ${convId}`,
277
+ );
253
278
  } catch (err) {
254
279
  this.logger.warn(
255
280
  `[a2a-xmtp] Failed to send failover claim: ${err instanceof Error ? err.message : String(err)}`,
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@
5
5
 
6
6
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
7
7
  import { Type } from "@sinclair/typebox";
8
+ import { mkdirSync } from "node:fs";
9
+ import { join } from "node:path";
8
10
  import { IdentityRegistry } from "./transport/identity-registry.js";
9
11
  import { PolicyEngine } from "./coordination/policy-engine.js";
10
12
  import { GroupScheduler } from "./coordination/group-scheduler.js";
@@ -131,12 +133,13 @@ export default definePluginEntry({
131
133
  async start(ctx) {
132
134
  ctx.logger.info("[a2a-xmtp] Starting XMTP Bridge Service...");
133
135
 
134
- // 加载配置
136
+ // 加载配置(所有选项均有默认值,用户无需手动编辑配置文件)
135
137
  const pluginCfg = (ctx.config as any)?.plugins?.entries?.["a2a-xmtp"]?.config;
138
+ const defaultDbPath = join(ctx.stateDir, "xmtp-data");
136
139
  const config: PluginConfig = {
137
140
  xmtp: {
138
141
  env: pluginCfg?.xmtp?.env ?? DEFAULT_PLUGIN_CONFIG.xmtp.env,
139
- dbPath: pluginCfg?.xmtp?.dbPath ?? DEFAULT_PLUGIN_CONFIG.xmtp.dbPath,
142
+ dbPath: pluginCfg?.xmtp?.dbPath ?? defaultDbPath,
140
143
  },
141
144
  policy: {
142
145
  maxTurns: pluginCfg?.policy?.maxTurns ?? DEFAULT_PLUGIN_CONFIG.policy.maxTurns,
@@ -152,6 +155,9 @@ export default definePluginEntry({
152
155
  walletKey: pluginCfg?.walletKey,
153
156
  };
154
157
 
158
+ // 自动创建数据目录
159
+ mkdirSync(config.xmtp.dbPath, { recursive: true });
160
+
155
161
  // 初始化核心组件
156
162
  registry = new IdentityRegistry(ctx.stateDir, config.xmtp.env);
157
163
  policyEngine = new PolicyEngine(config.policy);
@@ -109,12 +109,13 @@ export class XmtpBridge {
109
109
  /**
110
110
  * 发送 claim 消息到群聊,表明本 agent 将要回复。
111
111
  */
112
- async sendClaim(conversationId: string, messageId: string): Promise<void> {
112
+ async sendClaim(conversationId: string, messageId: string): Promise<string> {
113
113
  if (!this.agent) throw new Error(`Bridge for ${this.agentId} not started`);
114
114
  const conv = await this.agent.client.conversations.getConversationById(conversationId);
115
115
  if (!conv) throw new Error(`Conversation ${conversationId} not found`);
116
116
  const claimText = GroupScheduler.formatClaimMessage(messageId);
117
- await conv.sendText(claimText);
117
+ const claimMsgId = await conv.sendText(claimText);
118
+ return String(claimMsgId);
118
119
  }
119
120
 
120
121
  async getInbox(opts?: { limit?: number; from?: string }): Promise<InboxMessage[]> {