@zyclaw/webot 0.1.0
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/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/index.ts +731 -0
- package/openclaw.plugin.json +44 -0
- package/package.json +22 -0
- package/src/constants.ts +40 -0
- package/src/diary-prompt.md +46 -0
- package/src/diary-service.ts +295 -0
- package/src/message-handler.ts +446 -0
- package/src/protocol.ts +354 -0
- package/src/relay-client.ts +600 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// ── WeBot message handler — bridge between WeBot relay and local Gateway ──
|
|
2
|
+
//
|
|
3
|
+
// Message protocol between bots:
|
|
4
|
+
// [webot:task] <content> — task for the receiving bot to process (expects response)
|
|
5
|
+
// [webot:response] <content> — response to a previous task (no reply needed, notify owner)
|
|
6
|
+
// (no tag) — treated as [webot:task] for backward compat
|
|
7
|
+
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
10
|
+
import {
|
|
11
|
+
GATEWAY_RECONNECT_DELAY_MS,
|
|
12
|
+
GATEWAY_RPC_TIMEOUT_MS,
|
|
13
|
+
LOCAL_GATEWAY_WS,
|
|
14
|
+
} from "./constants.js";
|
|
15
|
+
import type { FriendRequestFrame, IncomingMessageFrame } from "./protocol.js";
|
|
16
|
+
import type { RelayClient } from "./relay-client.js";
|
|
17
|
+
|
|
18
|
+
type GatewayPendingCallback = (response: unknown) => void;
|
|
19
|
+
|
|
20
|
+
// ── Protocol tag helpers ──
|
|
21
|
+
|
|
22
|
+
const TAG_TASK = "[webot:task]";
|
|
23
|
+
const TAG_RESPONSE = "[webot:response]";
|
|
24
|
+
|
|
25
|
+
type MessageType = "task" | "response";
|
|
26
|
+
|
|
27
|
+
function parseMessageType(content: string): { type: MessageType; body: string } {
|
|
28
|
+
if (content.startsWith(TAG_RESPONSE)) {
|
|
29
|
+
return { type: "response", body: content.slice(TAG_RESPONSE.length).trim() };
|
|
30
|
+
}
|
|
31
|
+
if (content.startsWith(TAG_TASK)) {
|
|
32
|
+
return { type: "task", body: content.slice(TAG_TASK.length).trim() };
|
|
33
|
+
}
|
|
34
|
+
// Untagged → treat as task (backward compat)
|
|
35
|
+
return { type: "task", body: content };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type MessageHandler = ReturnType<typeof createMessageHandler>;
|
|
39
|
+
|
|
40
|
+
export function createMessageHandler(
|
|
41
|
+
relay: RelayClient,
|
|
42
|
+
logger: PluginLogger,
|
|
43
|
+
gatewayToken?: string,
|
|
44
|
+
) {
|
|
45
|
+
let gwWs: WebSocket | null = null;
|
|
46
|
+
let gwReqCounter = 0;
|
|
47
|
+
const gwPending = new Map<string, GatewayPendingCallback>();
|
|
48
|
+
// Completion callbacks for agent RPCs — called when the second res frame
|
|
49
|
+
// arrives with the actual agent result (the first is just "accepted").
|
|
50
|
+
const gwCompletionCallbacks = new Map<string, (frame: unknown) => void>();
|
|
51
|
+
let gwConnecting = false;
|
|
52
|
+
let gwStopping = false;
|
|
53
|
+
|
|
54
|
+
// Dedup incoming messages (messageId → timestamp) to avoid double-delivery
|
|
55
|
+
// from real-time + offline.messages overlap
|
|
56
|
+
const seenMessages = new Map<string, number>();
|
|
57
|
+
const SEEN_MSG_TTL_MS = 5 * 60_000; // 5 minutes
|
|
58
|
+
|
|
59
|
+
// Content-based dedup: server may send the same message with different
|
|
60
|
+
// messageIds (real-time + offline.messages). Key: `deviceId:contentPrefix`
|
|
61
|
+
const recentContent = new Map<string, number>();
|
|
62
|
+
const CONTENT_DEDUP_TTL_MS = 10_000; // 10 seconds
|
|
63
|
+
|
|
64
|
+
// ── Gateway self-connection ──
|
|
65
|
+
|
|
66
|
+
const connectToGateway = () => {
|
|
67
|
+
if (gwStopping || gwConnecting) return;
|
|
68
|
+
gwConnecting = true;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
gwWs = new WebSocket(LOCAL_GATEWAY_WS);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error(`WeBot: Gateway connect failed: ${String(err)}`);
|
|
74
|
+
gwConnecting = false;
|
|
75
|
+
scheduleGatewayReconnect();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
gwWs.onopen = () => {
|
|
80
|
+
gwConnecting = false;
|
|
81
|
+
logger.info("WeBot: gateway WS opened, waiting for connect.challenge…");
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
gwWs.onmessage = (ev) => {
|
|
85
|
+
try {
|
|
86
|
+
const frame = JSON.parse(String(ev.data));
|
|
87
|
+
|
|
88
|
+
// Handle connect.challenge — gateway sends this immediately after WS open
|
|
89
|
+
if (frame.type === "event" && frame.event === "connect.challenge") {
|
|
90
|
+
const nonce =
|
|
91
|
+
frame.payload && typeof frame.payload.nonce === "string"
|
|
92
|
+
? frame.payload.nonce
|
|
93
|
+
: undefined;
|
|
94
|
+
const connectFrame = {
|
|
95
|
+
type: "req",
|
|
96
|
+
id: `gw-${++gwReqCounter}`,
|
|
97
|
+
method: "connect",
|
|
98
|
+
params: {
|
|
99
|
+
minProtocol: 3,
|
|
100
|
+
maxProtocol: 3,
|
|
101
|
+
client: {
|
|
102
|
+
id: "gateway-client",
|
|
103
|
+
displayName: "WeBot Plugin",
|
|
104
|
+
version: "0.1.0",
|
|
105
|
+
platform: "plugin",
|
|
106
|
+
mode: "backend",
|
|
107
|
+
},
|
|
108
|
+
auth: gatewayToken ? { token: gatewayToken } : undefined,
|
|
109
|
+
role: "operator",
|
|
110
|
+
scopes: ["operator.admin"],
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
gwWs!.send(JSON.stringify(connectFrame));
|
|
114
|
+
logger.info("WeBot: connect.challenge received, handshake sent");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Handle response frames (RPC results + hello-ok)
|
|
119
|
+
if (frame.type === "res" && frame.id) {
|
|
120
|
+
const cb = gwPending.get(frame.id as string);
|
|
121
|
+
if (cb) {
|
|
122
|
+
gwPending.delete(frame.id as string);
|
|
123
|
+
cb(frame);
|
|
124
|
+
} else {
|
|
125
|
+
// Second res frame (agent completion) — invoke completion callback
|
|
126
|
+
const completionCb = gwCompletionCallbacks.get(frame.id as string);
|
|
127
|
+
if (completionCb) {
|
|
128
|
+
gwCompletionCallbacks.delete(frame.id as string);
|
|
129
|
+
try {
|
|
130
|
+
completionCb(frame);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
logger.error(`WeBot: completion callback error: ${String(err)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// ignore malformed frames
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
gwWs.onclose = () => {
|
|
143
|
+
gwConnecting = false;
|
|
144
|
+
if (!gwStopping) {
|
|
145
|
+
scheduleGatewayReconnect();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
gwWs.onerror = (err) => {
|
|
150
|
+
logger.error(`WeBot: Gateway WS error: ${String(err)}`);
|
|
151
|
+
gwWs?.close();
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const scheduleGatewayReconnect = () => {
|
|
156
|
+
if (gwStopping) return;
|
|
157
|
+
setTimeout(connectToGateway, GATEWAY_RECONNECT_DELAY_MS);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const callGatewayRpc = (
|
|
161
|
+
method: string,
|
|
162
|
+
params: Record<string, unknown>,
|
|
163
|
+
onCompletion?: (frame: unknown) => void,
|
|
164
|
+
): Promise<unknown> => {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
if (!gwWs || gwWs.readyState !== WebSocket.OPEN) {
|
|
167
|
+
reject(new Error("Gateway not connected"));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const id = `gw-${++gwReqCounter}`;
|
|
172
|
+
const timer = setTimeout(() => {
|
|
173
|
+
gwPending.delete(id);
|
|
174
|
+
gwCompletionCallbacks.delete(id);
|
|
175
|
+
reject(new Error("Gateway request timeout"));
|
|
176
|
+
}, GATEWAY_RPC_TIMEOUT_MS);
|
|
177
|
+
|
|
178
|
+
gwPending.set(id, (response) => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
resolve(response);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (onCompletion) {
|
|
184
|
+
gwCompletionCallbacks.set(id, onCompletion);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
gwWs.send(JSON.stringify({ type: "req", id, method, params }));
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ── Start Gateway connection (call after gateway is listening) ──
|
|
192
|
+
const startGatewayConnection = () => {
|
|
193
|
+
connectToGateway();
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// ── Listen for incoming WeBot messages ──
|
|
197
|
+
relay.onMessage(async (msg: IncomingMessageFrame) => {
|
|
198
|
+
// Deduplicate by messageId
|
|
199
|
+
if (seenMessages.has(msg.messageId)) {
|
|
200
|
+
logger.info(`WeBot: dedup — skipping already-seen messageId ${msg.messageId}`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
seenMessages.set(msg.messageId, Date.now());
|
|
204
|
+
|
|
205
|
+
// Content-based dedup: the server may send the same message under
|
|
206
|
+
// different messageIds (real-time delivery + offline.messages overlap).
|
|
207
|
+
const contentKey = `${msg.fromDeviceId}:${msg.content.slice(0, 200)}`;
|
|
208
|
+
if (recentContent.has(contentKey)) {
|
|
209
|
+
logger.info(`WeBot: dedup — skipping duplicate content from ${msg.fromDeviceId.slice(0, 8)}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
recentContent.set(contentKey, Date.now());
|
|
213
|
+
|
|
214
|
+
// Prune old entries from both dedup maps
|
|
215
|
+
const now = Date.now();
|
|
216
|
+
for (const [id, ts] of seenMessages) {
|
|
217
|
+
if (ts < now - SEEN_MSG_TTL_MS) seenMessages.delete(id);
|
|
218
|
+
}
|
|
219
|
+
for (const [key, ts] of recentContent) {
|
|
220
|
+
if (ts < now - CONTENT_DEDUP_TTL_MS) recentContent.delete(key);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const preview =
|
|
224
|
+
msg.content.length > 80
|
|
225
|
+
? `${msg.content.slice(0, 80)}...`
|
|
226
|
+
: msg.content;
|
|
227
|
+
logger.info(
|
|
228
|
+
`WeBot: message from ${msg.fromUsername} (${msg.fromBotName}): ${preview}`,
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// Acknowledge delivery (best-effort)
|
|
232
|
+
relay.ackMessage(msg.messageId, "delivered").catch(() => {});
|
|
233
|
+
|
|
234
|
+
const { type: msgType, body } = parseMessageType(msg.content);
|
|
235
|
+
const fromDeviceId = msg.fromDeviceId;
|
|
236
|
+
const sessionKey = `agent:main:webot:dm:${fromDeviceId}`;
|
|
237
|
+
const sessionLabel = `WeBot: ${msg.fromUsername}`;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
switch (msgType) {
|
|
241
|
+
// ── RESPONSE: result of a task we previously sent ──
|
|
242
|
+
// Do NOT reply back (break the loop). Notify owner via agent + message tool.
|
|
243
|
+
case "response": {
|
|
244
|
+
logger.info(`WeBot: received task response from ${msg.fromUsername}`);
|
|
245
|
+
// Use a brief trigger as the visible "user" message in session history;
|
|
246
|
+
// put the full content in extraSystemPrompt so the agent can process it
|
|
247
|
+
// without the full bot message appearing as a user message in Web UI.
|
|
248
|
+
const responsePreview = body.length > 60 ? `${body.slice(0, 60)}…` : body;
|
|
249
|
+
await callGatewayRpc("agent", {
|
|
250
|
+
message: `[WeBot response from @${msg.fromUsername}]: ${responsePreview}`,
|
|
251
|
+
sessionKey,
|
|
252
|
+
label: sessionLabel,
|
|
253
|
+
idempotencyKey: crypto.randomUUID(),
|
|
254
|
+
extraSystemPrompt: [
|
|
255
|
+
`This is a RESPONSE from @${msg.fromUsername}'s bot to a task you previously sent.`,
|
|
256
|
+
`Full message content:\n---\n${body}\n---`,
|
|
257
|
+
`DO NOT reply back to this bot via WeBot — the conversation is complete.`,
|
|
258
|
+
`Review the result and notify your owner using the "message" tool about the outcome.`,
|
|
259
|
+
`Summarize what was requested and what the result is.`,
|
|
260
|
+
].join("\n"),
|
|
261
|
+
});
|
|
262
|
+
// No completion callback → no reply sent back → loop broken
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── TASK (or untagged): task request for this bot to process ──
|
|
267
|
+
// Process via agent and send tagged response back.
|
|
268
|
+
case "task":
|
|
269
|
+
default: {
|
|
270
|
+
logger.info(`WeBot: task request from ${msg.fromUsername}`);
|
|
271
|
+
// Use a brief trigger as the visible "user" message in session history;
|
|
272
|
+
// put the full content in extraSystemPrompt so the agent can process it
|
|
273
|
+
// without the full bot message appearing as a user message in Web UI.
|
|
274
|
+
const taskPreview = body.length > 60 ? `${body.slice(0, 60)}…` : body;
|
|
275
|
+
await callGatewayRpc(
|
|
276
|
+
"agent",
|
|
277
|
+
{
|
|
278
|
+
message: `[WeBot message from @${msg.fromUsername}]: ${taskPreview}`,
|
|
279
|
+
sessionKey,
|
|
280
|
+
label: sessionLabel,
|
|
281
|
+
idempotencyKey: crypto.randomUUID(),
|
|
282
|
+
extraSystemPrompt: [
|
|
283
|
+
`You are receiving a task message via WeBot from @${msg.fromUsername} (bot: "${msg.fromBotName}").`,
|
|
284
|
+
`Full message content:\n---\n${body}\n---`,
|
|
285
|
+
`Your reply will be automatically sent back to the sender's bot.`,
|
|
286
|
+
``,
|
|
287
|
+
`CRITICAL — determine who the message is for:`,
|
|
288
|
+
`1. FOR YOUR OWNER (human): Messages that mention "my owner", "your owner", or are clearly social/personal`,
|
|
289
|
+
` (e.g. dinner invitations, availability questions, plans, favors, personal greetings).`,
|
|
290
|
+
` → You MUST use the "message" tool to forward this to your owner.`,
|
|
291
|
+
` → Reply to the sender: "I've forwarded this to my owner, will get back to you."`,
|
|
292
|
+
`2. FOR YOU (the bot): Technical requests, information queries, tasks you can handle directly`,
|
|
293
|
+
` (e.g. "search for X", "summarize Y", "what's the weather").`,
|
|
294
|
+
` → Handle it directly and reply with the result.`,
|
|
295
|
+
`3. MIXED: Some messages need both — handle what you can and forward the rest to your owner.`,
|
|
296
|
+
``,
|
|
297
|
+
`When in doubt about social/personal messages, ALWAYS forward to your owner.`,
|
|
298
|
+
`Keep responses concise and actionable.`,
|
|
299
|
+
].join("\n"),
|
|
300
|
+
},
|
|
301
|
+
// Tag the reply as [webot:response] so receiving bot won't reply again
|
|
302
|
+
(frame: unknown) => {
|
|
303
|
+
forwardTaggedResponse(frame, fromDeviceId);
|
|
304
|
+
},
|
|
305
|
+
);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
logger.error(`WeBot: failed to forward to Gateway: ${String(err)}`);
|
|
311
|
+
// Send error feedback to the sender (tagged as response to avoid loop)
|
|
312
|
+
relay
|
|
313
|
+
.sendMessage(`${TAG_RESPONSE} [Error: ${String(err)}]`, { toDeviceId: fromDeviceId })
|
|
314
|
+
.catch(() => { /* best-effort feedback */ });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Extract agent reply text from the completion frame and send it back
|
|
320
|
+
* to the WeBot sender, prefixed with [webot:response] to prevent loops.
|
|
321
|
+
*/
|
|
322
|
+
const forwardTaggedResponse = (frame: unknown, toDeviceId: string) => {
|
|
323
|
+
try {
|
|
324
|
+
const f = frame as {
|
|
325
|
+
payload?: {
|
|
326
|
+
status?: string;
|
|
327
|
+
error?: string;
|
|
328
|
+
result?: { payloads?: Array<{ text?: string }> };
|
|
329
|
+
};
|
|
330
|
+
};
|
|
331
|
+
if (f?.payload?.status === "ok" && f.payload.result?.payloads) {
|
|
332
|
+
const texts = f.payload.result.payloads
|
|
333
|
+
.map((p) => p.text)
|
|
334
|
+
.filter(Boolean);
|
|
335
|
+
const replyText = texts.join("\n").trim();
|
|
336
|
+
if (replyText) {
|
|
337
|
+
relay
|
|
338
|
+
.sendMessage(`${TAG_RESPONSE} ${replyText}`, { toDeviceId })
|
|
339
|
+
.then(() => {
|
|
340
|
+
logger.info(`WeBot: tagged response sent to ${toDeviceId.slice(0, 8)}...`);
|
|
341
|
+
})
|
|
342
|
+
.catch((err) => {
|
|
343
|
+
logger.error(`WeBot: failed to send response: ${String(err)}`);
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
logger.warn(`WeBot: agent returned empty reply for ${toDeviceId.slice(0, 8)}`);
|
|
347
|
+
}
|
|
348
|
+
} else if (f?.payload?.status === "error") {
|
|
349
|
+
const errorMsg = f.payload.error || "unknown error";
|
|
350
|
+
logger.warn(`WeBot: agent error for ${toDeviceId.slice(0, 8)}: ${errorMsg}`);
|
|
351
|
+
relay
|
|
352
|
+
.sendMessage(`${TAG_RESPONSE} [Task failed: ${errorMsg}]`, { toDeviceId })
|
|
353
|
+
.catch((err) => {
|
|
354
|
+
logger.error(`WeBot: failed to send error response: ${String(err)}`);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
} catch (err) {
|
|
358
|
+
logger.error(`WeBot: reply forwarding error: ${String(err)}`);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// ── Listen for incoming friend requests ──
|
|
363
|
+
relay.onFriendRequest(async (frame: FriendRequestFrame) => {
|
|
364
|
+
logger.info(
|
|
365
|
+
`WeBot: friend request from ${frame.fromUsername} (${frame.fromDisplayName})`,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
await callGatewayRpc("agent", {
|
|
370
|
+
message: [
|
|
371
|
+
`[WeBot: New friend request]`,
|
|
372
|
+
"",
|
|
373
|
+
`You received a friend request from ${frame.fromDisplayName} (@${frame.fromUsername}).`,
|
|
374
|
+
`Friendship ID: ${frame.friendshipId}`,
|
|
375
|
+
"",
|
|
376
|
+
`Use the webot_friend_accept or webot_friend_reject tool to respond, or tell your owner.`,
|
|
377
|
+
].join("\n"),
|
|
378
|
+
sessionKey: "agent:main:webot:system",
|
|
379
|
+
label: "WeBot: System",
|
|
380
|
+
idempotencyKey: crypto.randomUUID(),
|
|
381
|
+
});
|
|
382
|
+
logger.info("WeBot: agent notified of friend request");
|
|
383
|
+
} catch (err) {
|
|
384
|
+
logger.error(
|
|
385
|
+
`WeBot: failed to notify agent of friend request: ${String(err)}`,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
/** Start the gateway self-connection (call after gateway is ready) */
|
|
392
|
+
startGatewayConnection,
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Send a notification message to the local agent via Gateway RPC.
|
|
396
|
+
* Best-effort — if the gateway is not connected, retries once after a delay.
|
|
397
|
+
*/
|
|
398
|
+
notifyAgent: async (
|
|
399
|
+
message: string,
|
|
400
|
+
_metadata?: Record<string, unknown>,
|
|
401
|
+
) => {
|
|
402
|
+
const attempt = async () => {
|
|
403
|
+
await callGatewayRpc("agent", {
|
|
404
|
+
message,
|
|
405
|
+
sessionKey: "agent:main:webot:system",
|
|
406
|
+
label: "WeBot: System",
|
|
407
|
+
idempotencyKey: crypto.randomUUID(),
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
try {
|
|
411
|
+
await attempt();
|
|
412
|
+
} catch {
|
|
413
|
+
// Gateway may not be ready yet — retry once after a short delay
|
|
414
|
+
await new Promise((r) => setTimeout(r, 3_000));
|
|
415
|
+
try {
|
|
416
|
+
await attempt();
|
|
417
|
+
} catch (err) {
|
|
418
|
+
logger.warn(
|
|
419
|
+
`WeBot: failed to notify agent: ${String(err)}`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Hook handler for `message_sending` — kept for potential future use.
|
|
427
|
+
* Reply routing is handled via the agent RPC completion callback.
|
|
428
|
+
*/
|
|
429
|
+
onMessageSending: async (
|
|
430
|
+
_event: { to?: string; content?: string; metadata?: Record<string, unknown> },
|
|
431
|
+
_ctx: unknown,
|
|
432
|
+
) => {
|
|
433
|
+
// No-op: reply forwarding is handled by the completion callback
|
|
434
|
+
// in callGatewayRpc. The message_sending hook is currently not
|
|
435
|
+
// invoked by the gateway, so this is a no-op placeholder.
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
/** Stop the Gateway connection */
|
|
439
|
+
stop: () => {
|
|
440
|
+
gwStopping = true;
|
|
441
|
+
gwWs?.close();
|
|
442
|
+
gwPending.clear();
|
|
443
|
+
gwCompletionCallbacks.clear();
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|