gopherhole_openclaw_a2a 0.4.6 → 0.4.8

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.
@@ -261,7 +261,20 @@ export const a2aPlugin = {
261
261
  a2aLog.messageProcessing(message.taskId, sessionKey);
262
262
  // Add A2A context so the agent knows to relay its full response
263
263
  const a2aContext = `[A2A Request from agent "${message.from}"]\n\n${text}\n\n[Note: Your complete response will be sent back to the requesting agent. Include all relevant information in your reply.]`;
264
- const response = await sendChatMessage(sessionKey, a2aContext);
264
+ // A2A status updates are task-level keepalives. Publish one now and
265
+ // periodically while OpenClaw is working so callers do not mistake
266
+ // a long tool run for a dead connection.
267
+ connectionManager?.sendTaskStatusViaGopherHole(message.taskId, 'OpenClaw is working');
268
+ const keepalive = setInterval(() => {
269
+ connectionManager?.sendTaskStatusViaGopherHole(message.taskId, 'OpenClaw is still working');
270
+ }, 30_000);
271
+ let response;
272
+ try {
273
+ response = await sendChatMessage(sessionKey, a2aContext);
274
+ }
275
+ finally {
276
+ clearInterval(keepalive);
277
+ }
265
278
  // Log captured response
266
279
  if (response?.text) {
267
280
  a2aLog.responseCaptured(message.taskId, response.text);
@@ -44,6 +44,7 @@ export declare class A2AConnectionManager {
44
44
  * Uses SDK's respond() method to complete the original task
45
45
  */
46
46
  sendResponseViaGopherHole(_targetAgentId: string, taskId: string, text: string, _contextId?: string): void;
47
+ sendTaskStatusViaGopherHole(taskId: string, message?: string): void;
47
48
  /**
48
49
  * Legacy alias for sendPartsViaGopherHole with text-only
49
50
  */
@@ -271,6 +271,17 @@ export class A2AConnectionManager {
271
271
  console.error('[a2a] Error details:', err);
272
272
  }
273
273
  }
274
+ sendTaskStatusViaGopherHole(taskId, message) {
275
+ if (!this.gopherhole || !this.connected || !taskId || taskId.startsWith('gph-')) {
276
+ return;
277
+ }
278
+ try {
279
+ this.gopherhole.updateTaskStatus(taskId, 'working', message);
280
+ }
281
+ catch (err) {
282
+ console.error('[a2a] Failed to publish task status:', err.message);
283
+ }
284
+ }
274
285
  /**
275
286
  * Legacy alias for sendPartsViaGopherHole with text-only
276
287
  */
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * Gateway WebSocket Client
3
- * Sends messages to the Clawdbot gateway via its WebSocket protocol
3
+ * Sends messages to the OpenClaw gateway via its WebSocket protocol
4
4
  *
5
5
  * chat.send is non-blocking - it returns a runId immediately and
6
6
  * streams the response via chat events. We accumulate deltas and
7
7
  * resolve when we get state: "final".
8
8
  */
9
+ export declare const GATEWAY_PROTOCOL_VERSION = 4;
10
+ export declare const CHAT_INACTIVITY_TIMEOUT_MS: number;
9
11
  export declare function connectToGateway(port?: number): Promise<void>;
10
12
  export declare function callGateway(method: string, params: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
11
13
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Gateway WebSocket Client
3
- * Sends messages to the Clawdbot gateway via its WebSocket protocol
3
+ * Sends messages to the OpenClaw gateway via its WebSocket protocol
4
4
  *
5
5
  * chat.send is non-blocking - it returns a runId immediately and
6
6
  * streams the response via chat events. We accumulate deltas and
@@ -16,6 +16,18 @@ let pendingRequests = new Map();
16
16
  let pendingChats = new Map(); // keyed by runId
17
17
  let connected = false;
18
18
  let handshakeComplete = false;
19
+ // OpenClaw 2026.7+ requires gateway protocol v4. Keep this explicit so a
20
+ // protocol bump fails a focused compatibility test instead of silently
21
+ // breaking inbound A2A replies at runtime.
22
+ export const GATEWAY_PROTOCOL_VERSION = 4;
23
+ export const CHAT_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000;
24
+ function refreshChatTimeout(runId, pending) {
25
+ clearTimeout(pending.timeout);
26
+ pending.timeout = setTimeout(() => {
27
+ pendingChats.delete(runId);
28
+ pending.reject(new Error('Chat response inactivity timeout'));
29
+ }, pending.timeoutMs);
30
+ }
19
31
  function getGatewayToken() {
20
32
  // Try the current OpenClaw config location first, then the legacy
21
33
  // Clawdbot path as a fallback for users still on older installs.
@@ -56,12 +68,12 @@ export async function connectToGateway(port = 18789) {
56
68
  id: connectId,
57
69
  method: 'connect',
58
70
  params: {
59
- minProtocol: 3,
60
- maxProtocol: 3,
71
+ minProtocol: GATEWAY_PROTOCOL_VERSION,
72
+ maxProtocol: GATEWAY_PROTOCOL_VERSION,
61
73
  client: {
62
74
  id: 'gateway-client',
63
75
  displayName: 'A2A Channel Plugin',
64
- version: '0.3.4',
76
+ version: '0.4.8',
65
77
  platform: process.platform,
66
78
  mode: 'backend',
67
79
  },
@@ -170,6 +182,9 @@ function handleChatEvent(payload) {
170
182
  // Not a chat we're tracking
171
183
  return;
172
184
  }
185
+ // Any chat event proves the OpenClaw run is alive. Treat the timeout as an
186
+ // inactivity deadline rather than a fixed whole-task deadline.
187
+ refreshChatTimeout(payload.runId, pending);
173
188
  // Detailed logging for debugging relay issues
174
189
  console.log(`[a2a] Chat event: runId=${payload.runId}, state=${payload.state}, seq=${payload.seq}, role=${payload.message?.role}`);
175
190
  if (payload.state === 'delta' || payload.state === 'final') {
@@ -234,7 +249,7 @@ export async function callGateway(method, params, timeoutMs = 300000) {
234
249
  * Send a chat message and wait for the full response.
235
250
  * chat.send is non-blocking - returns runId immediately, response streams via events.
236
251
  */
237
- export async function sendChatMessage(sessionKey, message, timeoutMs = 300000) {
252
+ export async function sendChatMessage(sessionKey, message, timeoutMs = CHAT_INACTIVITY_TIMEOUT_MS) {
238
253
  if (!ws || !connected || !handshakeComplete) {
239
254
  await connectToGateway();
240
255
  }
@@ -252,16 +267,15 @@ export async function sendChatMessage(sessionKey, message, timeoutMs = 300000) {
252
267
  }
253
268
  // Now wait for chat events to complete
254
269
  return new Promise((resolve, reject) => {
255
- const timeout = setTimeout(() => {
256
- pendingChats.delete(result.runId);
257
- reject(new Error('Chat response timeout'));
258
- }, timeoutMs);
259
- pendingChats.set(result.runId, {
270
+ const pending = {
260
271
  resolve,
261
272
  reject,
262
- timeout,
273
+ timeout: undefined,
263
274
  latestText: '',
264
- });
275
+ timeoutMs,
276
+ };
277
+ pendingChats.set(result.runId, pending);
278
+ refreshChatTimeout(result.runId, pending);
265
279
  });
266
280
  }
267
281
  export function disconnectFromGateway() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gopherhole_openclaw_a2a",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "GopherHole A2A plugin for OpenClaw - connect your AI agent to the GopherHole network",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "license": "MIT",
37
37
  "dependencies": {
38
- "@gopherhole/sdk": "^0.7.5",
38
+ "@gopherhole/sdk": "^0.9.1",
39
39
  "uuid": "^10.0.0"
40
40
  },
41
41
  "devDependencies": {
@@ -53,4 +53,4 @@
53
53
  "README.md",
54
54
  "SKILL.md"
55
55
  ]
56
- }
56
+ }