@ynhcj/xiaoyi-channel 0.0.1 → 0.0.2-next

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.
Files changed (71) hide show
  1. package/dist/src/bot.js +115 -35
  2. package/dist/src/channel.js +22 -1
  3. package/dist/src/client.d.ts +15 -0
  4. package/dist/src/client.js +97 -2
  5. package/dist/src/file-download.js +10 -1
  6. package/dist/src/file-upload.js +1 -1
  7. package/dist/src/formatter.d.ts +17 -0
  8. package/dist/src/formatter.js +86 -6
  9. package/dist/src/heartbeat.d.ts +2 -1
  10. package/dist/src/heartbeat.js +9 -1
  11. package/dist/src/monitor.d.ts +5 -0
  12. package/dist/src/monitor.js +131 -29
  13. package/dist/src/onboarding.js +7 -7
  14. package/dist/src/outbound.js +150 -71
  15. package/dist/src/parser.d.ts +5 -0
  16. package/dist/src/parser.js +15 -0
  17. package/dist/src/push.d.ts +7 -1
  18. package/dist/src/push.js +110 -19
  19. package/dist/src/reply-dispatcher.d.ts +1 -0
  20. package/dist/src/reply-dispatcher.js +210 -57
  21. package/dist/src/task-manager.d.ts +55 -0
  22. package/dist/src/task-manager.js +136 -0
  23. package/dist/src/tools/calendar-tool.d.ts +6 -0
  24. package/dist/src/tools/calendar-tool.js +167 -0
  25. package/dist/src/tools/call-phone-tool.d.ts +5 -0
  26. package/dist/src/tools/call-phone-tool.js +183 -0
  27. package/dist/src/tools/create-alarm-tool.d.ts +7 -0
  28. package/dist/src/tools/create-alarm-tool.js +444 -0
  29. package/dist/src/tools/delete-alarm-tool.d.ts +11 -0
  30. package/dist/src/tools/delete-alarm-tool.js +238 -0
  31. package/dist/src/tools/location-tool.js +48 -9
  32. package/dist/src/tools/modify-alarm-tool.d.ts +9 -0
  33. package/dist/src/tools/modify-alarm-tool.js +474 -0
  34. package/dist/src/tools/modify-note-tool.d.ts +9 -0
  35. package/dist/src/tools/modify-note-tool.js +163 -0
  36. package/dist/src/tools/note-tool.d.ts +5 -0
  37. package/dist/src/tools/note-tool.js +146 -0
  38. package/dist/src/tools/search-alarm-tool.d.ts +8 -0
  39. package/dist/src/tools/search-alarm-tool.js +389 -0
  40. package/dist/src/tools/search-calendar-tool.d.ts +12 -0
  41. package/dist/src/tools/search-calendar-tool.js +259 -0
  42. package/dist/src/tools/search-contact-tool.d.ts +5 -0
  43. package/dist/src/tools/search-contact-tool.js +168 -0
  44. package/dist/src/tools/search-file-tool.d.ts +5 -0
  45. package/dist/src/tools/search-file-tool.js +185 -0
  46. package/dist/src/tools/search-message-tool.d.ts +5 -0
  47. package/dist/src/tools/search-message-tool.js +173 -0
  48. package/dist/src/tools/search-note-tool.d.ts +5 -0
  49. package/dist/src/tools/search-note-tool.js +130 -0
  50. package/dist/src/tools/search-photo-gallery-tool.d.ts +8 -0
  51. package/dist/src/tools/search-photo-gallery-tool.js +184 -0
  52. package/dist/src/tools/search-photo-tool.d.ts +9 -0
  53. package/dist/src/tools/search-photo-tool.js +270 -0
  54. package/dist/src/tools/send-file-to-user-tool.d.ts +5 -0
  55. package/dist/src/tools/send-file-to-user-tool.js +318 -0
  56. package/dist/src/tools/send-message-tool.d.ts +5 -0
  57. package/dist/src/tools/send-message-tool.js +189 -0
  58. package/dist/src/tools/session-manager.d.ts +15 -0
  59. package/dist/src/tools/session-manager.js +126 -6
  60. package/dist/src/tools/upload-file-tool.d.ts +13 -0
  61. package/dist/src/tools/upload-file-tool.js +265 -0
  62. package/dist/src/tools/upload-photo-tool.d.ts +9 -0
  63. package/dist/src/tools/upload-photo-tool.js +223 -0
  64. package/dist/src/tools/xiaoyi-gui-tool.d.ts +6 -0
  65. package/dist/src/tools/xiaoyi-gui-tool.js +151 -0
  66. package/dist/src/types.d.ts +5 -9
  67. package/dist/src/utils/config-manager.d.ts +26 -0
  68. package/dist/src/utils/config-manager.js +56 -0
  69. package/dist/src/websocket.d.ts +41 -0
  70. package/dist/src/websocket.js +202 -9
  71. package/package.json +7 -2
package/dist/src/bot.js CHANGED
@@ -1,10 +1,12 @@
1
1
  import { getXYRuntime } from "./runtime.js";
2
2
  import { createXYReplyDispatcher } from "./reply-dispatcher.js";
3
- import { parseA2AMessage, extractTextFromParts, extractFileParts } from "./parser.js";
3
+ import { parseA2AMessage, extractTextFromParts, extractFileParts, extractPushId } from "./parser.js";
4
4
  import { downloadFilesFromParts } from "./file-download.js";
5
5
  import { resolveXYConfig } from "./config.js";
6
6
  import { sendStatusUpdate, sendClearContextResponse, sendTasksCancelResponse } from "./formatter.js";
7
- import { registerSession, unregisterSession } from "./tools/session-manager.js";
7
+ import { registerSession, unregisterSession, runWithSessionContext } from "./tools/session-manager.js";
8
+ import { configManager } from "./utils/config-manager.js";
9
+ import { registerTaskId, decrementTaskIdRef, lockTaskId, unlockTaskId, hasActiveTask, } from "./task-manager.js";
8
10
  /**
9
11
  * Handle an incoming A2A message.
10
12
  * This is the main entry point for message processing.
@@ -19,7 +21,8 @@ export async function handleXYMessage(params) {
19
21
  try {
20
22
  // Check for special messages BEFORE parsing (these have different param structures)
21
23
  const messageMethod = message.method;
22
- log(`[DEBUG] Received message with method: ${messageMethod}, id: ${message.id}`);
24
+ log(`[BOT-ENTRY] <<<<<<< Received message with method: ${messageMethod}, id: ${message.id} >>>>>>>`);
25
+ log(`[BOT-ENTRY] Stack trace for debugging:`, new Error().stack?.split('\n').slice(1, 4).join('\n'));
23
26
  // Handle clearContext messages (params only has sessionId)
24
27
  if (messageMethod === "clearContext" || messageMethod === "clear_context") {
25
28
  const sessionId = message.params?.sessionId;
@@ -54,6 +57,34 @@ export async function handleXYMessage(params) {
54
57
  }
55
58
  // Parse the A2A message (for regular messages)
56
59
  const parsed = parseA2AMessage(message);
60
+ // 🔑 检测steer模式和是否是第二条消息
61
+ const isSteerMode = cfg.messages?.queue?.mode === "steer";
62
+ const isSecondMessage = isSteerMode && hasActiveTask(parsed.sessionId);
63
+ if (isSecondMessage) {
64
+ log(`[BOT] 🔄 STEER MODE - Second message detected (will be follower)`);
65
+ log(`[BOT] - Session: ${parsed.sessionId}`);
66
+ log(`[BOT] - New taskId: ${parsed.taskId} (will replace current)`);
67
+ }
68
+ // 🔑 注册taskId(第二条消息会覆盖第一条的taskId)
69
+ const { isUpdate, refCount } = registerTaskId(parsed.sessionId, parsed.taskId, parsed.messageId, { incrementRef: true } // 增加引用计数
70
+ );
71
+ // 🔑 如果是第一条消息,锁定taskId防止被过早清理
72
+ if (!isUpdate) {
73
+ lockTaskId(parsed.sessionId);
74
+ log(`[BOT] 🔒 Locked taskId for first message`);
75
+ }
76
+ // Extract and update push_id if present
77
+ const pushId = extractPushId(parsed.parts);
78
+ if (pushId) {
79
+ log(`[BOT] 📌 Extracted push_id from user message`);
80
+ log(`[BOT] - Session ID: ${parsed.sessionId}`);
81
+ log(`[BOT] - Push ID preview: ${pushId.substring(0, 20)}...`);
82
+ log(`[BOT] - Full push_id: ${pushId}`);
83
+ configManager.updatePushId(parsed.sessionId, pushId);
84
+ }
85
+ else {
86
+ log(`[BOT] ℹ️ No push_id found in message, will use config default`);
87
+ }
57
88
  // Resolve configuration (needed for status updates)
58
89
  const config = resolveXYConfig(cfg);
59
90
  // ✅ Resolve agent route (following feishu pattern)
@@ -61,7 +92,7 @@ export async function handleXYMessage(params) {
61
92
  // Use sessionId as peer.id to ensure all messages in the same session share context
62
93
  let route = core.channel.routing.resolveAgentRoute({
63
94
  cfg,
64
- channel: "xy",
95
+ channel: "xiaoyi-channel",
65
96
  accountId, // "default"
66
97
  peer: {
67
98
  kind: "direct",
@@ -69,7 +100,12 @@ export async function handleXYMessage(params) {
69
100
  },
70
101
  });
71
102
  log(`xy: resolved route accountId=${route.accountId}, sessionKey=${route.sessionKey}`);
72
- // Register session context for tools
103
+ // 🔑 注册session(带引用计数)
104
+ log(`[BOT] 📝 About to register session for tools...`);
105
+ log(`[BOT] - sessionKey: ${route.sessionKey}`);
106
+ log(`[BOT] - sessionId: ${parsed.sessionId}`);
107
+ log(`[BOT] - taskId: ${parsed.taskId}`);
108
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
73
109
  registerSession(route.sessionKey, {
74
110
  config,
75
111
  sessionId: parsed.sessionId,
@@ -77,6 +113,19 @@ export async function handleXYMessage(params) {
77
113
  messageId: parsed.messageId,
78
114
  agentId: route.accountId,
79
115
  });
116
+ log(`[BOT] ✅ Session registered for tools`);
117
+ // 🔑 发送初始状态更新(第二条消息也要发,用新taskId)
118
+ log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
119
+ void sendStatusUpdate({
120
+ config,
121
+ sessionId: parsed.sessionId,
122
+ taskId: parsed.taskId,
123
+ messageId: parsed.messageId,
124
+ text: isSecondMessage ? "新消息已接收,正在处理..." : "任务正在处理中,请稍后~",
125
+ state: "working",
126
+ }).catch((err) => {
127
+ error(`Failed to send initial status update:`, err);
128
+ });
80
129
  // Extract text and files from parts
81
130
  const text = extractTextFromParts(parsed.parts);
82
131
  const fileParts = extractFileParts(parsed.parts);
@@ -93,7 +142,7 @@ export async function handleXYMessage(params) {
93
142
  messageBody = `${speaker}: ${messageBody}`;
94
143
  // Format agent envelope (following feishu pattern)
95
144
  const body = core.channel.reply.formatAgentEnvelope({
96
- channel: "XY",
145
+ channel: "xiaoyi-channel",
97
146
  from: speaker,
98
147
  timestamp: new Date(),
99
148
  envelope: envelopeOptions,
@@ -113,84 +162,115 @@ export async function handleXYMessage(params) {
113
162
  GroupSubject: undefined,
114
163
  SenderName: parsed.sessionId,
115
164
  SenderId: parsed.sessionId,
116
- Provider: "xy",
117
- Surface: "xy",
165
+ Provider: "xiaoyi-channel",
166
+ Surface: "xiaoyi-channel",
118
167
  MessageSid: parsed.messageId,
119
168
  Timestamp: Date.now(),
120
169
  WasMentioned: false,
121
170
  CommandAuthorized: true,
122
- OriginatingChannel: "xy",
171
+ OriginatingChannel: "xiaoyi-channel",
123
172
  OriginatingTo: parsed.sessionId, // Original message target
124
173
  ReplyToBody: undefined, // A2A protocol doesn't support reply/quote
125
174
  ...mediaPayload,
126
175
  });
127
- // Send initial status update immediately after parsing message
128
- log(`[STATUS] Sending initial status update for session ${parsed.sessionId}`);
129
- void sendStatusUpdate({
130
- config,
131
- sessionId: parsed.sessionId,
132
- taskId: parsed.taskId,
133
- messageId: parsed.messageId,
134
- text: "任务正在处理中,请稍后~",
135
- state: "working",
136
- }).catch((err) => {
137
- error(`Failed to send initial status update:`, err);
138
- });
139
- // Create reply dispatcher (following feishu pattern)
176
+ // 🔑 创建dispatcher(dispatcher会自动使用动态taskId)
177
+ log(`[BOT-DISPATCHER] 🎯 Creating reply dispatcher`);
178
+ log(`[BOT-DISPATCHER] - session: ${parsed.sessionId}`);
179
+ log(`[BOT-DISPATCHER] - taskId: ${parsed.taskId}`);
180
+ log(`[BOT-DISPATCHER] - isSecondMessage: ${isSecondMessage}`);
140
181
  const { dispatcher, replyOptions, markDispatchIdle, startStatusInterval } = createXYReplyDispatcher({
141
182
  cfg,
142
183
  runtime,
143
184
  sessionId: parsed.sessionId,
144
185
  taskId: parsed.taskId,
145
186
  messageId: parsed.messageId,
146
- accountId: route.accountId, // ✅ Use route.accountId
187
+ accountId: route.accountId,
188
+ isSteerFollower: isSecondMessage, // 🔑 标记第二条消息
147
189
  });
148
- // Start status update interval (will send updates every 60 seconds)
149
- // Interval will be automatically stopped when onIdle/onCleanup is triggered
150
- startStatusInterval();
190
+ log(`[BOT-DISPATCHER] Reply dispatcher created successfully`);
191
+ // 🔑 只有第一条消息启动状态定时器
192
+ // 第二条消息会很快返回,不需要定时器
193
+ if (!isSecondMessage) {
194
+ startStatusInterval();
195
+ log(`[BOT-DISPATCHER] ✅ Status interval started for first message`);
196
+ }
197
+ else {
198
+ log(`[BOT-DISPATCHER] ⏭️ Skipped status interval for steer follower`);
199
+ }
151
200
  log(`xy: dispatching to agent (session=${parsed.sessionId})`);
152
201
  // Dispatch to OpenClaw core using correct API (following feishu pattern)
202
+ log(`[BOT] 🚀 Starting dispatcher with session: ${route.sessionKey}`);
203
+ // Build session context for AsyncLocalStorage
204
+ const sessionContext = {
205
+ config,
206
+ sessionId: parsed.sessionId,
207
+ taskId: parsed.taskId,
208
+ messageId: parsed.messageId,
209
+ agentId: route.accountId,
210
+ };
153
211
  await core.channel.reply.withReplyDispatcher({
154
212
  dispatcher,
155
213
  onSettled: () => {
214
+ log(`[BOT] 🏁 onSettled called for session: ${route.sessionKey}`);
215
+ log(`[BOT] - isSecondMessage: ${isSecondMessage}`);
156
216
  markDispatchIdle();
157
- // Unregister session context when done
217
+ // 🔑 减少引用计数
218
+ decrementTaskIdRef(parsed.sessionId);
219
+ // 🔑 如果是第一条消息完成,解锁
220
+ if (!isSecondMessage) {
221
+ unlockTaskId(parsed.sessionId);
222
+ log(`[BOT] 🔓 Unlocked taskId (first message completed)`);
223
+ }
224
+ // 减少session引用计数
158
225
  unregisterSession(route.sessionKey);
226
+ log(`[BOT] ✅ Cleanup completed`);
159
227
  },
160
- run: () => core.channel.reply.dispatchReplyFromConfig({
228
+ run: () =>
229
+ // 🔐 Use AsyncLocalStorage to provide session context to tools
230
+ runWithSessionContext(sessionContext, () => core.channel.reply.dispatchReplyFromConfig({
161
231
  ctx: ctxPayload,
162
232
  cfg,
163
233
  dispatcher,
164
234
  replyOptions,
165
- }),
235
+ })),
166
236
  });
237
+ log(`[BOT] ✅ Dispatcher completed for session: ${parsed.sessionId}`);
167
238
  log(`xy: dispatch complete (session=${parsed.sessionId})`);
168
239
  }
169
240
  catch (err) {
241
+ // ✅ Only log error, don't re-throw to prevent gateway restart
170
242
  error("Failed to handle XY message:", err);
171
243
  runtime.error?.(`xy: Failed to handle message: ${String(err)}`);
172
- // Try to unregister session on error (if route was established)
244
+ log(`[BOT] Error occurred, attempting cleanup...`);
245
+ // 🔑 错误时也要清理taskId和session
173
246
  try {
174
- const core = getXYRuntime();
175
247
  const params = message.params;
176
248
  const sessionId = params?.sessionId;
177
249
  if (sessionId) {
250
+ log(`[BOT] 🧹 Cleaning up after error: ${sessionId}`);
251
+ // 清理 taskId
252
+ decrementTaskIdRef(sessionId);
253
+ unlockTaskId(sessionId);
254
+ // 清理 session
255
+ const core = getXYRuntime();
178
256
  const route = core.channel.routing.resolveAgentRoute({
179
257
  cfg,
180
- channel: "xy",
258
+ channel: "xiaoyi-channel",
181
259
  accountId,
182
260
  peer: {
183
261
  kind: "direct",
184
- id: sessionId, // ✅ Use sessionId for cleanup consistency
262
+ id: sessionId,
185
263
  },
186
264
  });
187
265
  unregisterSession(route.sessionKey);
266
+ log(`[BOT] ✅ Cleanup completed after error`);
188
267
  }
189
268
  }
190
- catch {
269
+ catch (cleanupErr) {
270
+ log(`[BOT] ⚠️ Cleanup failed:`, cleanupErr);
191
271
  // Ignore cleanup errors
192
272
  }
193
- throw err;
273
+ // ❌ Don't re-throw: message processing error should not affect gateway stability
194
274
  }
195
275
  }
196
276
  /**
@@ -3,6 +3,25 @@ import { xyConfigSchema } from "./config-schema.js";
3
3
  import { xyOutbound } from "./outbound.js";
4
4
  import { xyOnboardingAdapter } from "./onboarding.js";
5
5
  import { locationTool } from "./tools/location-tool.js";
6
+ import { noteTool } from "./tools/note-tool.js";
7
+ import { searchNoteTool } from "./tools/search-note-tool.js";
8
+ import { modifyNoteTool } from "./tools/modify-note-tool.js";
9
+ import { calendarTool } from "./tools/calendar-tool.js";
10
+ import { searchCalendarTool } from "./tools/search-calendar-tool.js";
11
+ // import { searchContactTool } from "./tools/search-contact-tool.js"; // 暂时禁用
12
+ import { searchPhotoGalleryTool } from "./tools/search-photo-gallery-tool.js";
13
+ import { uploadPhotoTool } from "./tools/upload-photo-tool.js";
14
+ import { xiaoyiGuiTool } from "./tools/xiaoyi-gui-tool.js";
15
+ import { callPhoneTool } from "./tools/call-phone-tool.js";
16
+ import { searchMessageTool } from "./tools/search-message-tool.js";
17
+ import { searchFileTool } from "./tools/search-file-tool.js";
18
+ import { uploadFileTool } from "./tools/upload-file-tool.js";
19
+ import { createAlarmTool } from "./tools/create-alarm-tool.js";
20
+ import { searchAlarmTool } from "./tools/search-alarm-tool.js";
21
+ import { modifyAlarmTool } from "./tools/modify-alarm-tool.js";
22
+ import { deleteAlarmTool } from "./tools/delete-alarm-tool.js";
23
+ import { sendMessageTool } from "./tools/send-message-tool.js";
24
+ import { sendFileToUserTool } from "./tools/send-file-to-user-tool.js";
6
25
  /**
7
26
  * Xiaoyi Channel Plugin for OpenClaw.
8
27
  * Implements Xiaoyi A2A protocol with dual WebSocket connections.
@@ -20,6 +39,7 @@ export const xyPlugin = {
20
39
  agentPrompt: {
21
40
  messageToolHints: () => [
22
41
  "- xiaoyi targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `default`",
42
+ "- If the user requests a file, you can call the message tool with the xiaoyi-channel channel to return it. Note: sendMedia requires a text reply."
23
43
  ],
24
44
  },
25
45
  capabilities: {
@@ -41,7 +61,7 @@ export const xyPlugin = {
41
61
  },
42
62
  outbound: xyOutbound,
43
63
  onboarding: xyOnboardingAdapter,
44
- agentTools: [locationTool],
64
+ agentTools: [locationTool, noteTool, searchNoteTool, modifyNoteTool, calendarTool, searchCalendarTool, searchPhotoGalleryTool, uploadPhotoTool, xiaoyiGuiTool, callPhoneTool, searchMessageTool, searchFileTool, uploadFileTool, createAlarmTool, searchAlarmTool, modifyAlarmTool, deleteAlarmTool, sendMessageTool, sendFileToUserTool], // searchContactTool 已暂时禁用
45
65
  messaging: {
46
66
  normalizeTarget: (raw) => {
47
67
  const trimmed = raw.trim();
@@ -77,6 +97,7 @@ export const xyPlugin = {
77
97
  runtime: context.runtime,
78
98
  abortSignal: context.abortSignal,
79
99
  accountId: context.accountId,
100
+ setStatus: context.setStatus,
80
101
  });
81
102
  },
82
103
  },
@@ -10,6 +10,11 @@ export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
10
10
  * Reuses existing managers if config matches.
11
11
  */
12
12
  export declare function getXYWebSocketManager(config: XYChannelConfig): XYWebSocketManager;
13
+ /**
14
+ * Remove a specific WebSocket manager from cache.
15
+ * Disconnects the manager and removes it from the cache.
16
+ */
17
+ export declare function removeXYWebSocketManager(config: XYChannelConfig): void;
13
18
  /**
14
19
  * Clear all cached WebSocket managers.
15
20
  */
@@ -18,3 +23,13 @@ export declare function clearXYWebSocketManagers(): void;
18
23
  * Get the number of cached managers.
19
24
  */
20
25
  export declare function getCachedManagerCount(): number;
26
+ /**
27
+ * Diagnose all cached WebSocket managers.
28
+ * Helps identify connection issues and orphan connections.
29
+ */
30
+ export declare function diagnoseAllManagers(): void;
31
+ /**
32
+ * Clean up orphan connections across all managers.
33
+ * Returns the number of managers that had orphan connections.
34
+ */
35
+ export declare function cleanupOrphanConnections(): number;
@@ -23,16 +23,34 @@ export function getXYWebSocketManager(config) {
23
23
  let cached = wsManagerCache.get(cacheKey);
24
24
  if (cached && cached.isConfigMatch(config)) {
25
25
  const log = runtime?.log ?? console.log;
26
- log(`[DEBUG] Reusing cached WebSocket manager: ${cacheKey}`);
26
+ log(`[WS-MANAGER-CACHE] Reusing cached WebSocket manager: ${cacheKey}, total managers: ${wsManagerCache.size}`);
27
27
  return cached;
28
28
  }
29
29
  // Create new manager
30
30
  const log = runtime?.log ?? console.log;
31
- log(`Creating new WebSocket manager: ${cacheKey}`);
31
+ log(`[WS-MANAGER-CACHE] 🆕 Creating new WebSocket manager: ${cacheKey}, total managers before: ${wsManagerCache.size}`);
32
32
  cached = new XYWebSocketManager(config, runtime);
33
33
  wsManagerCache.set(cacheKey, cached);
34
+ log(`[WS-MANAGER-CACHE] 📊 Total managers after creation: ${wsManagerCache.size}`);
34
35
  return cached;
35
36
  }
37
+ /**
38
+ * Remove a specific WebSocket manager from cache.
39
+ * Disconnects the manager and removes it from the cache.
40
+ */
41
+ export function removeXYWebSocketManager(config) {
42
+ const cacheKey = `${config.apiKey}-${config.agentId}`;
43
+ const manager = wsManagerCache.get(cacheKey);
44
+ if (manager) {
45
+ console.log(`🗑️ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
46
+ manager.disconnect();
47
+ wsManagerCache.delete(cacheKey);
48
+ console.log(`🗑️ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
49
+ }
50
+ else {
51
+ console.log(`⚠️ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
52
+ }
53
+ }
36
54
  /**
37
55
  * Clear all cached WebSocket managers.
38
56
  */
@@ -50,3 +68,80 @@ export function clearXYWebSocketManagers() {
50
68
  export function getCachedManagerCount() {
51
69
  return wsManagerCache.size;
52
70
  }
71
+ /**
72
+ * Diagnose all cached WebSocket managers.
73
+ * Helps identify connection issues and orphan connections.
74
+ */
75
+ export function diagnoseAllManagers() {
76
+ console.log("========================================");
77
+ console.log("📊 WebSocket Manager Global Diagnostics");
78
+ console.log("========================================");
79
+ console.log(`Total cached managers: ${wsManagerCache.size}`);
80
+ console.log("");
81
+ if (wsManagerCache.size === 0) {
82
+ console.log("ℹ️ No managers in cache");
83
+ console.log("========================================");
84
+ return;
85
+ }
86
+ let orphanCount = 0;
87
+ wsManagerCache.forEach((manager, key) => {
88
+ const diag = manager.getConnectionDiagnostics();
89
+ console.log(`📌 Manager: ${key}`);
90
+ console.log(` Shutting down: ${diag.isShuttingDown}`);
91
+ console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
92
+ // Server 1
93
+ console.log(` 🔌 Server1:`);
94
+ console.log(` - Exists: ${diag.server1.exists}`);
95
+ console.log(` - ReadyState: ${diag.server1.readyState}`);
96
+ console.log(` - State connected/ready: ${diag.server1.stateConnected}/${diag.server1.stateReady}`);
97
+ console.log(` - Reconnect attempts: ${diag.server1.reconnectAttempts}`);
98
+ console.log(` - Listeners on WebSocket: ${diag.server1.listenerCount}`);
99
+ console.log(` - Heartbeat active: ${diag.server1.heartbeatActive}`);
100
+ console.log(` - Has reconnect timer: ${diag.server1.hasReconnectTimer}`);
101
+ if (diag.server1.isOrphan) {
102
+ console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
103
+ orphanCount++;
104
+ }
105
+ // Server 2
106
+ console.log(` 🔌 Server2:`);
107
+ console.log(` - Exists: ${diag.server2.exists}`);
108
+ console.log(` - ReadyState: ${diag.server2.readyState}`);
109
+ console.log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
110
+ console.log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
111
+ console.log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
112
+ console.log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
113
+ console.log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
114
+ if (diag.server2.isOrphan) {
115
+ console.log(` ⚠️ ORPHAN CONNECTION DETECTED!`);
116
+ orphanCount++;
117
+ }
118
+ console.log("");
119
+ });
120
+ if (orphanCount > 0) {
121
+ console.log(`⚠️ Total orphan connections found: ${orphanCount}`);
122
+ console.log(`💡 Suggestion: These connections should be cleaned up`);
123
+ }
124
+ else {
125
+ console.log(`✅ No orphan connections found`);
126
+ }
127
+ console.log("========================================");
128
+ }
129
+ /**
130
+ * Clean up orphan connections across all managers.
131
+ * Returns the number of managers that had orphan connections.
132
+ */
133
+ export function cleanupOrphanConnections() {
134
+ let cleanedCount = 0;
135
+ wsManagerCache.forEach((manager, key) => {
136
+ const diag = manager.getConnectionDiagnostics();
137
+ if (diag.server1.isOrphan || diag.server2.isOrphan) {
138
+ console.log(`🧹 Cleaning up orphan connections in manager: ${key}`);
139
+ manager.disconnect();
140
+ cleanedCount++;
141
+ }
142
+ });
143
+ if (cleanedCount > 0) {
144
+ console.log(`🧹 Cleaned up ${cleanedCount} manager(s) with orphan connections`);
145
+ }
146
+ return cleanedCount;
147
+ }
@@ -8,8 +8,10 @@ import { logger } from "./utils/logger.js";
8
8
  */
9
9
  export async function downloadFile(url, destPath) {
10
10
  logger.debug(`Downloading file from ${url} to ${destPath}`);
11
+ const controller = new AbortController();
12
+ const timeout = setTimeout(() => controller.abort(), 30000); // 30 seconds timeout
11
13
  try {
12
- const response = await fetch(url);
14
+ const response = await fetch(url, { signal: controller.signal });
13
15
  if (!response.ok) {
14
16
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
15
17
  }
@@ -19,9 +21,16 @@ export async function downloadFile(url, destPath) {
19
21
  logger.debug(`File downloaded successfully: ${destPath}`);
20
22
  }
21
23
  catch (error) {
24
+ if (error.name === 'AbortError') {
25
+ logger.error(`Download timeout (30s) for ${url}`);
26
+ throw new Error(`Download timeout after 30 seconds`);
27
+ }
22
28
  logger.error(`Failed to download file from ${url}:`, error);
23
29
  throw error;
24
30
  }
31
+ finally {
32
+ clearTimeout(timeout);
33
+ }
25
34
  }
26
35
  /**
27
36
  * Download files from A2A file parts.
@@ -48,7 +48,7 @@ export class XYFileUploadService {
48
48
  uid: this.uid,
49
49
  teamId: this.uid,
50
50
  },
51
- useEdge: true,
51
+ useEdge: false,
52
52
  }),
53
53
  });
54
54
  if (!prepareResp.ok) {
@@ -20,6 +20,23 @@ export interface SendA2AResponseParams {
20
20
  * Send an A2A artifact update response.
21
21
  */
22
22
  export declare function sendA2AResponse(params: SendA2AResponseParams): Promise<void>;
23
+ /**
24
+ * Parameters for sending a reasoning text update (intermediate, streamed).
25
+ */
26
+ export interface SendReasoningTextUpdateParams {
27
+ config: XYChannelConfig;
28
+ sessionId: string;
29
+ taskId: string;
30
+ messageId: string;
31
+ text: string;
32
+ append?: boolean;
33
+ }
34
+ /**
35
+ * Send an A2A artifact-update with reasoningText part.
36
+ * Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
37
+ * append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
38
+ */
39
+ export declare function sendReasoningTextUpdate(params: SendReasoningTextUpdateParams): Promise<void>;
23
40
  /**
24
41
  * Parameters for sending a status update.
25
42
  */
@@ -51,8 +51,64 @@ export async function sendA2AResponse(params) {
51
51
  taskId,
52
52
  msgDetail: JSON.stringify(jsonRpcResponse),
53
53
  };
54
+ // 📋 Log complete response body
55
+ log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response:`);
56
+ log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
57
+ log(`[A2A_RESPONSE] - taskId: ${taskId}`);
58
+ log(`[A2A_RESPONSE] - messageId: ${messageId}`);
59
+ log(`[A2A_RESPONSE] - append: ${append}`);
60
+ log(`[A2A_RESPONSE] - final: ${final}`);
61
+ log(`[A2A_RESPONSE] - text length: ${text?.length ?? 0}`);
62
+ log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
63
+ log(`[A2A_RESPONSE] 📦 Complete outbound message:`);
64
+ log(JSON.stringify(outboundMessage, null, 2));
65
+ log(`[A2A_RESPONSE] 📦 JSON-RPC response body:`);
66
+ log(JSON.stringify(jsonRpcResponse, null, 2));
54
67
  await wsManager.sendMessage(sessionId, outboundMessage);
55
- log(`Sent A2A response: sessionId=${sessionId}, taskId=${taskId}, final=${final}`);
68
+ log(`[A2A_RESPONSE] Message sent successfully`);
69
+ }
70
+ /**
71
+ * Send an A2A artifact-update with reasoningText part.
72
+ * Used for onToolStart, onToolResult, onReasoningStream, onReasoningEnd, onPartialReply.
73
+ * append=true, final=false, lastChunk=true, text is suffixed with newline for markdown rendering.
74
+ */
75
+ export async function sendReasoningTextUpdate(params) {
76
+ const { config, sessionId, taskId, messageId, text, append = true } = params;
77
+ const runtime = getXYRuntime();
78
+ const log = runtime?.log ?? console.log;
79
+ const error = runtime?.error ?? console.error;
80
+ const artifact = {
81
+ taskId,
82
+ kind: "artifact-update",
83
+ append,
84
+ lastChunk: true,
85
+ final: false,
86
+ artifact: {
87
+ artifactId: uuidv4(),
88
+ parts: [
89
+ {
90
+ kind: "reasoningText",
91
+ reasoningText: text,
92
+ },
93
+ ],
94
+ },
95
+ };
96
+ const jsonRpcResponse = {
97
+ jsonrpc: "2.0",
98
+ id: messageId,
99
+ result: artifact,
100
+ };
101
+ const wsManager = getXYWebSocketManager(config);
102
+ const outboundMessage = {
103
+ msgType: "agent_response",
104
+ agentId: config.agentId,
105
+ sessionId,
106
+ taskId,
107
+ msgDetail: JSON.stringify(jsonRpcResponse),
108
+ };
109
+ log(`[REASONING_TEXT] 📤 Sending reasoningText update: sessionId=${sessionId}, taskId=${taskId}, text.length=${text.length}`);
110
+ await wsManager.sendMessage(sessionId, outboundMessage);
111
+ log(`[REASONING_TEXT] ✅ Sent successfully`);
56
112
  }
57
113
  /**
58
114
  * Send an A2A task status update.
@@ -96,8 +152,19 @@ export async function sendStatusUpdate(params) {
96
152
  taskId,
97
153
  msgDetail: JSON.stringify(jsonRpcResponse),
98
154
  };
155
+ // 📋 Log complete response body
156
+ log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
157
+ log(`[A2A_STATUS] - sessionId: ${sessionId}`);
158
+ log(`[A2A_STATUS] - taskId: ${taskId}`);
159
+ log(`[A2A_STATUS] - messageId: ${messageId}`);
160
+ log(`[A2A_STATUS] - state: ${state}`);
161
+ log(`[A2A_STATUS] - text: "${text}"`);
162
+ log(`[A2A_STATUS] 📦 Complete outbound message:`);
163
+ log(JSON.stringify(outboundMessage, null, 2));
164
+ log(`[A2A_STATUS] 📦 JSON-RPC response body:`);
165
+ log(JSON.stringify(jsonRpcResponse, null, 2));
99
166
  await wsManager.sendMessage(sessionId, outboundMessage);
100
- log(`Sent status update: sessionId=${sessionId}, state=${state}, text="${text}"`);
167
+ log(`[A2A_STATUS] Status update sent successfully`);
101
168
  }
102
169
  /**
103
170
  * Send a command as an artifact update (final=false).
@@ -107,7 +174,8 @@ export async function sendCommand(params) {
107
174
  const runtime = getXYRuntime();
108
175
  const log = runtime?.log ?? console.log;
109
176
  const error = runtime?.error ?? console.error;
110
- // Build artifact update with command
177
+ // Build artifact update with command as data
178
+ // Wrap command in commands array as per protocol requirement
111
179
  const artifact = {
112
180
  taskId,
113
181
  kind: "artifact-update",
@@ -118,8 +186,10 @@ export async function sendCommand(params) {
118
186
  artifactId: uuidv4(),
119
187
  parts: [
120
188
  {
121
- kind: "command",
122
- command,
189
+ kind: "data",
190
+ data: {
191
+ commands: [command],
192
+ },
123
193
  },
124
194
  ],
125
195
  },
@@ -139,8 +209,18 @@ export async function sendCommand(params) {
139
209
  taskId,
140
210
  msgDetail: JSON.stringify(jsonRpcResponse),
141
211
  };
212
+ // 📋 Log complete response body
213
+ log(`[A2A_COMMAND] 📤 Sending A2A command:`);
214
+ log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
215
+ log(`[A2A_COMMAND] - taskId: ${taskId}`);
216
+ log(`[A2A_COMMAND] - messageId: ${messageId}`);
217
+ log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
218
+ log(`[A2A_COMMAND] 📦 Complete outbound message:`);
219
+ log(JSON.stringify(outboundMessage, null, 2));
220
+ log(`[A2A_COMMAND] 📦 JSON-RPC response body:`);
221
+ log(JSON.stringify(jsonRpcResponse, null, 2));
142
222
  await wsManager.sendMessage(sessionId, outboundMessage);
143
- log(`Sent command: sessionId=${sessionId}, command=${command.header.name}`);
223
+ log(`[A2A_COMMAND] Command sent successfully`);
144
224
  }
145
225
  /**
146
226
  * Send a clearContext response.
@@ -13,12 +13,13 @@ export declare class HeartbeatManager {
13
13
  private config;
14
14
  private onTimeout;
15
15
  private serverName;
16
+ private onHeartbeatSuccess?;
16
17
  private intervalTimer;
17
18
  private timeoutTimer;
18
19
  private lastPongTime;
19
20
  private log;
20
21
  private error;
21
- constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void);
22
+ constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void, onHeartbeatSuccess?: () => void);
22
23
  /**
23
24
  * Start heartbeat monitoring.
24
25
  */