agentschat-mcp 0.33.5 → 0.34.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 +81 -0
- package/README.md +93 -52
- package/connector/README.md +181 -120
- package/connector/identities.ts +40 -0
- package/connector/ingest.ts +42 -0
- package/connector/run.ts +245 -52
- package/connector/server.ts +174 -104
- package/dist/connector.js +517 -156
- package/dist/server.js +92 -35
- package/package.json +5 -3
- package/skills/onboarding.md +236 -39
- package/src/cli.mjs +67 -23
- package/src/identity.ts +12 -0
- package/src/server.ts +75 -31
package/dist/connector.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// connector/run.ts
|
|
3
3
|
import WS from "ws";
|
|
4
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
4
5
|
|
|
5
6
|
// connector/server.ts
|
|
6
7
|
import { createServer } from "node:http";
|
|
@@ -104,15 +105,36 @@ function toWireEvent(msg, platform = "agentschat") {
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
// src/
|
|
108
|
-
function
|
|
109
|
-
if (!
|
|
108
|
+
// src/dedup.ts
|
|
109
|
+
function messageDedupKey(data) {
|
|
110
|
+
if (!data || typeof data.id !== "string" || typeof data.channel_id !== "string")
|
|
111
|
+
return null;
|
|
112
|
+
return `${data.channel_id}:${data.id}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class MessageDedup {
|
|
116
|
+
max;
|
|
117
|
+
dropOnEvict;
|
|
118
|
+
seen = new Set;
|
|
119
|
+
constructor(max = 5000, dropOnEvict = 1000) {
|
|
120
|
+
this.max = max;
|
|
121
|
+
this.dropOnEvict = dropOnEvict;
|
|
122
|
+
}
|
|
123
|
+
recordOrSkip(key) {
|
|
124
|
+
if (this.seen.has(key))
|
|
125
|
+
return true;
|
|
126
|
+
this.seen.add(key);
|
|
127
|
+
if (this.seen.size > this.max) {
|
|
128
|
+
const arr = [...this.seen];
|
|
129
|
+
this.seen.clear();
|
|
130
|
+
for (const item of arr.slice(this.dropOnEvict))
|
|
131
|
+
this.seen.add(item);
|
|
132
|
+
}
|
|
110
133
|
return false;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return displayMentionRe.test(content);
|
|
134
|
+
}
|
|
135
|
+
get size() {
|
|
136
|
+
return this.seen.size;
|
|
137
|
+
}
|
|
116
138
|
}
|
|
117
139
|
|
|
118
140
|
// connector/identities.ts
|
|
@@ -138,25 +160,38 @@ class IdentityTable {
|
|
|
138
160
|
all() {
|
|
139
161
|
return [...this.byBot.values()];
|
|
140
162
|
}
|
|
163
|
+
replace(identities) {
|
|
164
|
+
const next = new Map;
|
|
165
|
+
for (const id of identities) {
|
|
166
|
+
if (next.has(id.botId)) {
|
|
167
|
+
throw new Error(`duplicate identity botId "${id.botId}" — ambiguous routing`);
|
|
168
|
+
}
|
|
169
|
+
next.set(id.botId, id);
|
|
170
|
+
}
|
|
171
|
+
this.byBot.clear();
|
|
172
|
+
for (const [k, v] of next)
|
|
173
|
+
this.byBot.set(k, v);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function requireIdentity(identities, botId) {
|
|
177
|
+
const id = identities.find((id2) => id2.botId === botId);
|
|
178
|
+
if (!id)
|
|
179
|
+
throw new Error(`unknown identity: ${botId}`);
|
|
180
|
+
return id;
|
|
181
|
+
}
|
|
182
|
+
function matchesExactMention(content, id) {
|
|
183
|
+
if (!id)
|
|
184
|
+
return false;
|
|
185
|
+
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
186
|
+
return new RegExp(`(?:^|[^\\p{L}\\p{N}_@-])@(?:${escaped}(?![\\p{L}\\p{N}_(-])|[^\\s@()]+\\(${escaped}\\))`, "u").test(content);
|
|
141
187
|
}
|
|
142
|
-
function
|
|
188
|
+
function routeInboundTargets(table, ctx) {
|
|
143
189
|
if (ctx.channel_id?.startsWith("dm-")) {
|
|
144
|
-
|
|
190
|
+
const owner = ctx.dmOwnerBotId ? table.forBot(ctx.dmOwnerBotId) : null;
|
|
191
|
+
return owner ? [owner] : [];
|
|
145
192
|
}
|
|
146
193
|
const mentioned = Array.isArray(ctx.mentioned_ids) ? ctx.mentioned_ids : [];
|
|
147
|
-
|
|
148
|
-
const id = table.forBot(mid);
|
|
149
|
-
if (id)
|
|
150
|
-
return id;
|
|
151
|
-
}
|
|
152
|
-
const content = ctx.content ?? "";
|
|
153
|
-
if (content) {
|
|
154
|
-
for (const id of table.all()) {
|
|
155
|
-
if (matchesMention(content, id.agentId) || matchesMention(content, id.botId))
|
|
156
|
-
return id;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
194
|
+
return table.all().filter((id) => mentioned.includes(id.botId) || matchesExactMention(ctx.content ?? "", id.agentId) || matchesExactMention(ctx.content ?? "", id.botId));
|
|
160
195
|
}
|
|
161
196
|
function hermesSourceProfile(id, frontedCount) {
|
|
162
197
|
const named = typeof id.profile === "string" ? id.profile.trim() : "";
|
|
@@ -168,12 +203,32 @@ function hermesSourceProfile(id, frontedCount) {
|
|
|
168
203
|
}
|
|
169
204
|
|
|
170
205
|
// connector/server.ts
|
|
206
|
+
class PlatformHttpError extends Error {
|
|
207
|
+
status;
|
|
208
|
+
constructor(status) {
|
|
209
|
+
super("platform HTTP failure");
|
|
210
|
+
this.status = status;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
class PlatformNetworkError extends Error {
|
|
215
|
+
constructor() {
|
|
216
|
+
super("platform network failure");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
171
219
|
function startConnector(config) {
|
|
172
220
|
const log = config.logger ?? (() => {});
|
|
173
221
|
const descriptor = buildDescriptor(config.descriptor);
|
|
174
222
|
const legacy = !config.identities || config.identities.length === 0;
|
|
175
223
|
const table = new IdentityTable(legacy ? [{ botId: "default", agentId: "default", token: "", gatewayId: "", secret: "" }] : config.identities);
|
|
176
|
-
|
|
224
|
+
function safeLabel(value) {
|
|
225
|
+
for (const id of table.all())
|
|
226
|
+
for (const secret of [id.token, id.secret]) {
|
|
227
|
+
if (secret)
|
|
228
|
+
value = value.split(secret).join("[redacted]");
|
|
229
|
+
}
|
|
230
|
+
return JSON.stringify(value.slice(0, 128));
|
|
231
|
+
}
|
|
177
232
|
const hooks = legacy ? {
|
|
178
233
|
sendMessage: (_b, chatId, content, replyTo) => config.agentschat.sendMessage(chatId, content, replyTo),
|
|
179
234
|
getChatInfo: (_b, chatId) => config.agentschat.getChatInfo(chatId),
|
|
@@ -182,6 +237,8 @@ function startConnector(config) {
|
|
|
182
237
|
} : config.agentschat;
|
|
183
238
|
const sockets = new Set;
|
|
184
239
|
const lastAddressed = new Map;
|
|
240
|
+
const delivered = new MessageDedup;
|
|
241
|
+
const inboundQueues = new Map;
|
|
185
242
|
const http = createServer((_req, res) => {
|
|
186
243
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
187
244
|
res.end(JSON.stringify({ ok: true, service: "agentschat-connector", contract_version: 1, identities: table.size }));
|
|
@@ -197,7 +254,8 @@ function startConnector(config) {
|
|
|
197
254
|
const token = auth.startsWith("Bearer ") ? auth.slice(7).trim() : "";
|
|
198
255
|
const payload = peekPayload(token);
|
|
199
256
|
const secrets = payload ? config.secrets[payload] : undefined;
|
|
200
|
-
const
|
|
257
|
+
const secret = secrets?.find((s) => verifyUpgradeToken(token, [s]) === payload);
|
|
258
|
+
const gatewayId = secret ? payload : null;
|
|
201
259
|
if (!gatewayId) {
|
|
202
260
|
log(`[connector] rejecting upgrade: bad/absent token (path=${pathname})`);
|
|
203
261
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -206,13 +264,13 @@ function startConnector(config) {
|
|
|
206
264
|
return;
|
|
207
265
|
}
|
|
208
266
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
209
|
-
onConnection(ws, gatewayId);
|
|
267
|
+
onConnection(ws, gatewayId, secret);
|
|
210
268
|
});
|
|
211
269
|
});
|
|
212
|
-
function onConnection(ws, gatewayId) {
|
|
213
|
-
const conn = { ws, gatewayId, fronted: new Set };
|
|
270
|
+
function onConnection(ws, gatewayId, secret) {
|
|
271
|
+
const conn = { ws, gatewayId, secret, fronted: new Set };
|
|
214
272
|
sockets.add(conn);
|
|
215
|
-
log(`[connector] gateway connected: ${gatewayId} (${sockets.size} total)`);
|
|
273
|
+
log(`[connector] gateway connected: ${safeLabel(gatewayId)} (${sockets.size} total)`);
|
|
216
274
|
ws.on("message", async (data) => {
|
|
217
275
|
const text = data.toString();
|
|
218
276
|
for (const line of text.split(`
|
|
@@ -230,7 +288,7 @@ function startConnector(config) {
|
|
|
230
288
|
});
|
|
231
289
|
ws.on("close", () => {
|
|
232
290
|
sockets.delete(conn);
|
|
233
|
-
log(`[connector] gateway disconnected: ${gatewayId} (${sockets.size} left)`);
|
|
291
|
+
log(`[connector] gateway disconnected: ${safeLabel(gatewayId)} (${sockets.size} left)`);
|
|
234
292
|
});
|
|
235
293
|
ws.on("error", () => sockets.delete(conn));
|
|
236
294
|
}
|
|
@@ -238,17 +296,32 @@ function startConnector(config) {
|
|
|
238
296
|
const t = frame?.type;
|
|
239
297
|
if (t === "hello") {
|
|
240
298
|
const botId = String(frame.botId ?? "");
|
|
241
|
-
const identity =
|
|
242
|
-
|
|
243
|
-
|
|
299
|
+
const identity = legacy ? table.all()[0] : table.forBot(botId);
|
|
300
|
+
const reason = frame.platform !== "agentschat" ? "unsupported_platform" : !identity ? "unknown_identity" : !legacy && (identity.gatewayId !== conn.gatewayId || identity.secret !== conn.secret) ? "credential_mismatch" : null;
|
|
301
|
+
const labels = `gatewayId=${safeLabel(conn.gatewayId)} platform=${safeLabel(frame.platform === "agentschat" ? "agentschat" : "(unsupported)")} botId=${safeLabel(identity ? botId : "(unknown)")}`;
|
|
302
|
+
if (reason) {
|
|
303
|
+
log(`[connector] hello rejected ${labels} reason=${reason}`);
|
|
304
|
+
conn.fronted.clear();
|
|
305
|
+
send(conn.ws, { type: "error", error: `hello rejected: ${reason}` });
|
|
306
|
+
conn.ws.close(1002, `hello rejected: ${reason}`);
|
|
244
307
|
return;
|
|
245
308
|
}
|
|
246
309
|
conn.fronted.add(identity.botId);
|
|
310
|
+
if (legacy)
|
|
311
|
+
conn.legacyAlias = botId;
|
|
312
|
+
log(`[connector] hello accepted ${labels}`);
|
|
247
313
|
send(conn.ws, { type: "descriptor", descriptor: { ...descriptor, platform: "agentschat" } });
|
|
248
314
|
return;
|
|
249
315
|
}
|
|
250
316
|
if (t === "outbound") {
|
|
251
|
-
|
|
317
|
+
let result;
|
|
318
|
+
try {
|
|
319
|
+
result = await handleOutbound(conn, frame);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const status = error instanceof PlatformHttpError && Number.isInteger(error.status) && error.status >= 400 && error.status <= 599 ? error.status : undefined;
|
|
322
|
+
result = status !== undefined ? { success: false, error: `agentschat HTTP ${status}`, code: "platform_http_error", status, ambiguous: status >= 500 } : error instanceof PlatformNetworkError ? { success: false, error: "agentschat network failure; outcome unknown", code: "platform_network_error", ambiguous: true } : { success: false, error: "platform operation failed; outcome unknown", code: "platform_operation_failed", ambiguous: true };
|
|
323
|
+
log(`[connector] outbound failed: ${result.code}${status ? ` status=${status}` : ""}`);
|
|
324
|
+
}
|
|
252
325
|
send(conn.ws, { type: "outbound_result", requestId: frame.requestId, result });
|
|
253
326
|
return;
|
|
254
327
|
}
|
|
@@ -257,12 +330,16 @@ function startConnector(config) {
|
|
|
257
330
|
const action = frame?.action ?? {};
|
|
258
331
|
const op = action?.op;
|
|
259
332
|
const chatId = action?.chat_id ?? "";
|
|
260
|
-
const firstFronted = [...conn.fronted][0];
|
|
261
|
-
|
|
262
|
-
|
|
333
|
+
const firstFronted = conn.fronted.size === 1 ? [...conn.fronted][0] : undefined;
|
|
334
|
+
let requested = frame.botId === undefined ? firstFronted ?? null : typeof frame.botId === "string" && frame.botId ? frame.botId : null;
|
|
335
|
+
if (legacy && requested && requested === conn.legacyAlias)
|
|
336
|
+
requested = "default";
|
|
337
|
+
let identity = requested && conn.fronted.has(requested) ? table.forBot(requested) : null;
|
|
338
|
+
if (identity && (!legacy && (identity.gatewayId !== conn.gatewayId || identity.secret !== conn.secret) || frame.platform !== undefined && frame.platform !== "agentschat"))
|
|
339
|
+
identity = null;
|
|
263
340
|
if (!identity) {
|
|
264
|
-
log(`[connector] outbound failed: no
|
|
265
|
-
return { success: false, error: `no
|
|
341
|
+
log(`[connector] outbound failed: no usable identity for botId=${requested ?? "?"}`);
|
|
342
|
+
return { success: false, error: `no usable identity for outbound (botId=${requested ?? "?"})` };
|
|
266
343
|
}
|
|
267
344
|
switch (op) {
|
|
268
345
|
case "send": {
|
|
@@ -287,11 +364,17 @@ function startConnector(config) {
|
|
|
287
364
|
ws.send(JSON.stringify(obj) + `
|
|
288
365
|
`);
|
|
289
366
|
}
|
|
367
|
+
const ready = new Promise((resolve, reject) => {
|
|
368
|
+
http.once("listening", resolve);
|
|
369
|
+
http.once("error", reject);
|
|
370
|
+
});
|
|
290
371
|
http.listen(config.port, config.host ?? "127.0.0.1");
|
|
291
|
-
const address = http.address();
|
|
292
|
-
const port = typeof address === "object" && address ? address.port : config.port;
|
|
293
372
|
return {
|
|
294
|
-
|
|
373
|
+
ready,
|
|
374
|
+
get port() {
|
|
375
|
+
const address = http.address();
|
|
376
|
+
return typeof address === "object" && address ? address.port : config.port;
|
|
377
|
+
},
|
|
295
378
|
stop() {
|
|
296
379
|
for (const c of sockets) {
|
|
297
380
|
try {
|
|
@@ -303,53 +386,102 @@ function startConnector(config) {
|
|
|
303
386
|
},
|
|
304
387
|
async injectAgentsChatMessage(msg) {
|
|
305
388
|
const isDm = typeof msg.channel_id === "string" && msg.channel_id.startsWith("dm-");
|
|
306
|
-
const
|
|
307
|
-
const
|
|
308
|
-
channel_id: msg.channel_id,
|
|
309
|
-
dmOwnerBotId: msg.dm_owner
|
|
310
|
-
}) ?? (table.isSingle() ? table.all()[0] : null) : routeInbound(table, {
|
|
389
|
+
const owner = msg.__botId ?? msg.dm_owner;
|
|
390
|
+
const targets = isDm ? [owner !== undefined ? typeof owner === "string" ? table.forBot(owner) : null : table.isSingle() ? table.all()[0] : null].filter((id) => !!id) : routeInboundTargets(table, {
|
|
311
391
|
channel_id: msg.channel_id,
|
|
312
392
|
mentioned_ids: msg.mentioned_ids,
|
|
313
393
|
content: msg.content
|
|
314
394
|
});
|
|
315
|
-
if (!
|
|
395
|
+
if (!targets.length) {
|
|
316
396
|
log(`[connector] inbound unaddressed (channel=${msg.channel_id ?? "?"} mentions=${JSON.stringify(msg.mentioned_ids ?? [])}) — dropped`);
|
|
317
397
|
return;
|
|
318
398
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
399
|
+
await Promise.all(targets.map((target) => {
|
|
400
|
+
const key = JSON.stringify([target.botId, msg.channel_id]);
|
|
401
|
+
const previous = inboundQueues.get(key) ?? Promise.resolve();
|
|
402
|
+
const job = previous.catch(() => {}).then(async () => {
|
|
403
|
+
if (msg.sender_id === target.agentId)
|
|
404
|
+
return;
|
|
405
|
+
const baseEvent = toWireEvent(msg, "agentschat");
|
|
406
|
+
if (!baseEvent)
|
|
407
|
+
return;
|
|
408
|
+
if (![...sockets].some((c) => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId)))
|
|
409
|
+
return;
|
|
410
|
+
const deliveryKey = typeof msg.id === "string" && msg.id ? JSON.stringify([msg.channel_id, msg.id, target.botId]) : null;
|
|
411
|
+
if (deliveryKey && delivered.recordOrSkip(deliveryKey))
|
|
412
|
+
return;
|
|
413
|
+
if (!isDm) {
|
|
414
|
+
const since = lastAddressed.get(key);
|
|
415
|
+
if (hooks.getChannelContext) {
|
|
416
|
+
const controller = new AbortController;
|
|
417
|
+
let timer;
|
|
418
|
+
try {
|
|
419
|
+
const ctx = await Promise.race([
|
|
420
|
+
hooks.getChannelContext(target.botId, msg.channel_id, since, msg.id, controller.signal),
|
|
421
|
+
new Promise((resolve) => {
|
|
422
|
+
timer = setTimeout(() => {
|
|
423
|
+
controller.abort();
|
|
424
|
+
resolve(null);
|
|
425
|
+
}, 1000);
|
|
426
|
+
})
|
|
427
|
+
]);
|
|
428
|
+
if (ctx && ctx.length) {
|
|
429
|
+
baseEvent.context = ctx.slice(-10).map((c) => ({
|
|
430
|
+
text: String(c?.text ?? "").slice(0, 500),
|
|
431
|
+
source: { user_name: c?.user_name, user_id: c?.user_id }
|
|
432
|
+
}));
|
|
433
|
+
}
|
|
434
|
+
} catch {
|
|
435
|
+
log(`[connector] context fetch failed (delivering without it)`);
|
|
436
|
+
} finally {
|
|
437
|
+
if (timer)
|
|
438
|
+
clearTimeout(timer);
|
|
439
|
+
}
|
|
333
440
|
}
|
|
334
|
-
|
|
335
|
-
|
|
441
|
+
if (msg.timestamp && Number.isFinite(Date.parse(msg.timestamp)) && (!since || Date.parse(msg.timestamp) > Date.parse(since)))
|
|
442
|
+
lastAddressed.set(key, msg.timestamp);
|
|
336
443
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
444
|
+
const deliverTo = [...sockets].filter((c) => c.ws.readyState === WebSocket.OPEN && c.fronted.has(target.botId)).slice(0, 1);
|
|
445
|
+
if (deliverTo.length === 0) {
|
|
446
|
+
log(`[connector] inbound dropped for botId=${target.botId}: no agentschat-fronted gateway socket`);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
for (const conn of deliverTo) {
|
|
450
|
+
const event = { ...baseEvent, source: { ...baseEvent.source } };
|
|
451
|
+
const profile = hermesSourceProfile(target, conn.fronted.size);
|
|
452
|
+
if (profile)
|
|
453
|
+
event.source.profile = profile;
|
|
454
|
+
send(conn.ws, { type: "inbound", event });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
inboundQueues.set(key, job);
|
|
458
|
+
return job.finally(() => {
|
|
459
|
+
if (inboundQueues.get(key) === job)
|
|
460
|
+
inboundQueues.delete(key);
|
|
461
|
+
});
|
|
462
|
+
}));
|
|
350
463
|
},
|
|
351
464
|
connections() {
|
|
352
465
|
return sockets.size;
|
|
466
|
+
},
|
|
467
|
+
reloadIdentities(next) {
|
|
468
|
+
if (!next || next.length === 0) {
|
|
469
|
+
throw new Error("reloadIdentities requires a non-empty identity list");
|
|
470
|
+
}
|
|
471
|
+
table.replace(next);
|
|
472
|
+
for (const conn of sockets) {
|
|
473
|
+
for (const botId of conn.fronted) {
|
|
474
|
+
const id = table.forBot(botId);
|
|
475
|
+
if (!id || id.gatewayId !== conn.gatewayId || id.secret !== conn.secret)
|
|
476
|
+
conn.fronted.delete(botId);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
for (const k of Object.keys(config.secrets))
|
|
480
|
+
delete config.secrets[k];
|
|
481
|
+
for (const id of next) {
|
|
482
|
+
(config.secrets[id.gatewayId] ??= []).push(id.secret);
|
|
483
|
+
}
|
|
484
|
+
log(`[connector] identities reloaded: ${table.size}`);
|
|
353
485
|
}
|
|
354
486
|
};
|
|
355
487
|
}
|
|
@@ -424,38 +556,6 @@ function normalizeTimestampForCursor(ts, mode) {
|
|
|
424
556
|
return ts;
|
|
425
557
|
}
|
|
426
558
|
|
|
427
|
-
// src/dedup.ts
|
|
428
|
-
function messageDedupKey(data) {
|
|
429
|
-
if (!data || typeof data.id !== "string" || typeof data.channel_id !== "string")
|
|
430
|
-
return null;
|
|
431
|
-
return `${data.channel_id}:${data.id}`;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
class MessageDedup {
|
|
435
|
-
max;
|
|
436
|
-
dropOnEvict;
|
|
437
|
-
seen = new Set;
|
|
438
|
-
constructor(max = 5000, dropOnEvict = 1000) {
|
|
439
|
-
this.max = max;
|
|
440
|
-
this.dropOnEvict = dropOnEvict;
|
|
441
|
-
}
|
|
442
|
-
recordOrSkip(key) {
|
|
443
|
-
if (this.seen.has(key))
|
|
444
|
-
return true;
|
|
445
|
-
this.seen.add(key);
|
|
446
|
-
if (this.seen.size > this.max) {
|
|
447
|
-
const arr = [...this.seen];
|
|
448
|
-
this.seen.clear();
|
|
449
|
-
for (const item of arr.slice(this.dropOnEvict))
|
|
450
|
-
this.seen.add(item);
|
|
451
|
-
}
|
|
452
|
-
return false;
|
|
453
|
-
}
|
|
454
|
-
get size() {
|
|
455
|
-
return this.seen.size;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
559
|
// connector/backfill.ts
|
|
460
560
|
function planBackfill(after, msgs, agentId) {
|
|
461
561
|
const list = Array.isArray(msgs) ? msgs : [];
|
|
@@ -479,6 +579,93 @@ function planBackfill(after, msgs, agentId) {
|
|
|
479
579
|
return { replay };
|
|
480
580
|
}
|
|
481
581
|
|
|
582
|
+
// connector/ingest.ts
|
|
583
|
+
function ingestAgentsChatFrame(id, frame, deps) {
|
|
584
|
+
if (frame.content !== "__typing__") {
|
|
585
|
+
deps.advanceCursor(id, frame.channel_id, frame.timestamp);
|
|
586
|
+
}
|
|
587
|
+
const key = messageDedupKey(frame);
|
|
588
|
+
const isDm = frame.channel_id?.startsWith("dm-");
|
|
589
|
+
const scopedKey = key && (isDm ? JSON.stringify([id.botId, frame.channel_id, frame.id]) : key);
|
|
590
|
+
if (scopedKey && deps.dedup.recordOrSkip(scopedKey))
|
|
591
|
+
return false;
|
|
592
|
+
return (!frame.channel_id?.startsWith("dm-") || frame.sender_id !== id.agentId) && frame.content !== "__typing__";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/heartbeat.ts
|
|
596
|
+
var WS_CONNECTING = 0;
|
|
597
|
+
var WS_OPEN = 1;
|
|
598
|
+
var WS_CLOSING = 2;
|
|
599
|
+
var WS_CLOSED = 3;
|
|
600
|
+
|
|
601
|
+
class HeartbeatMonitor {
|
|
602
|
+
deps;
|
|
603
|
+
pingInterval;
|
|
604
|
+
pongTimeout;
|
|
605
|
+
connectTimeout;
|
|
606
|
+
lastPong;
|
|
607
|
+
timer = null;
|
|
608
|
+
connectingSince = null;
|
|
609
|
+
reconnecting = false;
|
|
610
|
+
constructor(deps, pingInterval = 30000, pongTimeout = 90000, connectTimeout = 30000) {
|
|
611
|
+
this.deps = deps;
|
|
612
|
+
this.pingInterval = pingInterval;
|
|
613
|
+
this.pongTimeout = pongTimeout;
|
|
614
|
+
this.connectTimeout = connectTimeout;
|
|
615
|
+
this.lastPong = Date.now();
|
|
616
|
+
}
|
|
617
|
+
receivedPong() {
|
|
618
|
+
this.lastPong = Date.now();
|
|
619
|
+
this.connectingSince = null;
|
|
620
|
+
this.reconnecting = false;
|
|
621
|
+
}
|
|
622
|
+
start() {
|
|
623
|
+
this.stop();
|
|
624
|
+
this.lastPong = Date.now();
|
|
625
|
+
this.connectingSince = null;
|
|
626
|
+
this.reconnecting = false;
|
|
627
|
+
this.timer = setInterval(() => this.tick(), this.pingInterval);
|
|
628
|
+
}
|
|
629
|
+
stop() {
|
|
630
|
+
if (this.timer) {
|
|
631
|
+
clearInterval(this.timer);
|
|
632
|
+
this.timer = null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
resetReconnecting() {
|
|
636
|
+
this.reconnecting = false;
|
|
637
|
+
}
|
|
638
|
+
tick() {
|
|
639
|
+
const state = this.deps.getReadyState();
|
|
640
|
+
if (state === WS_OPEN) {
|
|
641
|
+
this.connectingSince = null;
|
|
642
|
+
if (Date.now() - this.lastPong > this.pongTimeout) {
|
|
643
|
+
this.safeReconnect("pong timeout");
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
this.deps.sendPing();
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (state === WS_CONNECTING) {
|
|
650
|
+
if (!this.connectingSince) {
|
|
651
|
+
this.connectingSince = Date.now();
|
|
652
|
+
} else if (Date.now() - this.connectingSince > this.connectTimeout) {
|
|
653
|
+
this.connectingSince = null;
|
|
654
|
+
this.safeReconnect("connect timeout");
|
|
655
|
+
}
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
this.connectingSince = null;
|
|
659
|
+
this.safeReconnect(state === WS_CLOSING ? "stuck closing" : "closed");
|
|
660
|
+
}
|
|
661
|
+
safeReconnect(reason) {
|
|
662
|
+
if (this.reconnecting)
|
|
663
|
+
return;
|
|
664
|
+
this.reconnecting = true;
|
|
665
|
+
this.deps.reconnect();
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
482
669
|
// connector/run.ts
|
|
483
670
|
var log = (m) => process.stderr.write(`[agentschat-connector] ${m}
|
|
484
671
|
`);
|
|
@@ -494,34 +681,52 @@ var API = (process.env.AGENTCHAT_API_URL || "https://agents-chat.com").replace(/
|
|
|
494
681
|
var WS_URL = process.env.AGENTCHAT_WS_URL || API.replace(/^http/, "ws") + "/ws";
|
|
495
682
|
var PORT = Number(process.env.RELAY_PORT || 8765);
|
|
496
683
|
var HOST = process.env.RELAY_HOST || "127.0.0.1";
|
|
497
|
-
function
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
684
|
+
function parseIdentitiesJson(raw, source) {
|
|
685
|
+
let parsed;
|
|
686
|
+
try {
|
|
687
|
+
parsed = JSON.parse(raw);
|
|
688
|
+
} catch {
|
|
689
|
+
throw new Error(`${source} is not valid JSON`);
|
|
690
|
+
}
|
|
691
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
692
|
+
throw new Error(`${source} must be a non-empty JSON array`);
|
|
693
|
+
}
|
|
694
|
+
const seen = new Set;
|
|
695
|
+
for (const [index, it] of parsed.entries()) {
|
|
696
|
+
const invalid = ["botId", "token", "gatewayId", "secret"].filter((k) => typeof it?.[k] !== "string" || !it[k].trim());
|
|
697
|
+
for (const k of ["agentId", "profile"]) {
|
|
698
|
+
if (it?.[k] !== undefined && (typeof it[k] !== "string" || !it[k].trim()))
|
|
699
|
+
invalid.push(k);
|
|
510
700
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
701
|
+
if (invalid.length)
|
|
702
|
+
throw new Error(`${source} entry ${index}: missing or invalid string fields: ${invalid.join(", ")}`);
|
|
703
|
+
if (seen.has(it.botId))
|
|
704
|
+
throw new Error(`${source} entry ${index}: duplicate botId`);
|
|
705
|
+
seen.add(it.botId);
|
|
706
|
+
}
|
|
707
|
+
return parsed.map((it) => ({
|
|
708
|
+
botId: String(it.botId),
|
|
709
|
+
agentId: String(it.agentId ?? it.botId),
|
|
710
|
+
token: String(it.token),
|
|
711
|
+
gatewayId: String(it.gatewayId),
|
|
712
|
+
secret: String(it.secret),
|
|
713
|
+
...it.profile ? { profile: String(it.profile) } : {}
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
function loadIdentities() {
|
|
717
|
+
const file = (process.env.RELAY_IDENTITIES_FILE || "").trim();
|
|
718
|
+
if (file) {
|
|
719
|
+
let raw2;
|
|
720
|
+
try {
|
|
721
|
+
raw2 = readFileSync2(file, "utf8");
|
|
722
|
+
} catch {
|
|
723
|
+
throw new Error("RELAY_IDENTITIES_FILE could not be read");
|
|
516
724
|
}
|
|
517
|
-
return
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
secret: String(it.secret),
|
|
523
|
-
...it.profile ? { profile: String(it.profile) } : {}
|
|
524
|
-
}));
|
|
725
|
+
return parseIdentitiesJson(raw2, "RELAY_IDENTITIES_FILE");
|
|
726
|
+
}
|
|
727
|
+
const raw = (process.env.RELAY_IDENTITIES || "").trim();
|
|
728
|
+
if (raw) {
|
|
729
|
+
return parseIdentitiesJson(raw, "RELAY_IDENTITIES");
|
|
525
730
|
}
|
|
526
731
|
const agentId = need("AGENTCHAT_AGENT_ID");
|
|
527
732
|
const token = need("AGENTCHAT_TOKEN");
|
|
@@ -529,17 +734,56 @@ function resolveIdentities() {
|
|
|
529
734
|
const secret = need("RELAY_GATEWAY_SECRET");
|
|
530
735
|
return [{ botId: agentId, agentId, token, gatewayId, secret }];
|
|
531
736
|
}
|
|
532
|
-
|
|
533
|
-
|
|
737
|
+
function resolveIdentitiesAtStartup() {
|
|
738
|
+
try {
|
|
739
|
+
return loadIdentities();
|
|
740
|
+
} catch (e) {
|
|
741
|
+
log(`ERROR: ${e?.message ?? e}`);
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
var identities = resolveIdentitiesAtStartup();
|
|
534
746
|
var secrets = {};
|
|
535
747
|
for (const id of identities) {
|
|
536
748
|
(secrets[id.gatewayId] ??= []).push(id.secret);
|
|
537
749
|
}
|
|
538
750
|
var broadcast = null;
|
|
539
751
|
var socketsByBot = new Map;
|
|
752
|
+
var reconnectTimerByBot = new Map;
|
|
753
|
+
var currentIdentity = (id) => !process.__shutdown && identities.includes(id);
|
|
754
|
+
function cancelReconnect(botId) {
|
|
755
|
+
const timer = reconnectTimerByBot.get(botId);
|
|
756
|
+
if (timer)
|
|
757
|
+
clearTimeout(timer);
|
|
758
|
+
reconnectTimerByBot.delete(botId);
|
|
759
|
+
}
|
|
760
|
+
function scheduleReconnect(id, delay) {
|
|
761
|
+
if (!currentIdentity(id))
|
|
762
|
+
return;
|
|
763
|
+
cancelReconnect(id.botId);
|
|
764
|
+
const timer = setTimeout(() => {
|
|
765
|
+
if (reconnectTimerByBot.get(id.botId) !== timer)
|
|
766
|
+
return;
|
|
767
|
+
reconnectTimerByBot.delete(id.botId);
|
|
768
|
+
if (currentIdentity(id))
|
|
769
|
+
connectIdentity(id);
|
|
770
|
+
}, delay);
|
|
771
|
+
reconnectTimerByBot.set(id.botId, timer);
|
|
772
|
+
}
|
|
540
773
|
var backoffByBot = new Map;
|
|
541
774
|
var backfillTimerByBot = new Map;
|
|
775
|
+
var heartbeatsByBot = new Map;
|
|
542
776
|
var dedup = new MessageDedup;
|
|
777
|
+
var HB_PING_MS = Math.max(5000, Number(process.env.AGENTSCHAT_CONNECTOR_PING_MS || 15000));
|
|
778
|
+
var HB_PONG_MS = Math.max(HB_PING_MS + 5000, Number(process.env.AGENTSCHAT_CONNECTOR_PONG_TIMEOUT_MS || 45000));
|
|
779
|
+
var HB_CONNECT_MS = Math.max(5000, Number(process.env.AGENTSCHAT_CONNECTOR_CONNECT_TIMEOUT_MS || 30000));
|
|
780
|
+
function stopHeartbeat(botId) {
|
|
781
|
+
const hb = heartbeatsByBot.get(botId);
|
|
782
|
+
if (!hb)
|
|
783
|
+
return;
|
|
784
|
+
hb.stop();
|
|
785
|
+
heartbeatsByBot.delete(botId);
|
|
786
|
+
}
|
|
543
787
|
var CURSOR_DIR = process.env.AGENTCHAT_CURSOR_DIR || process.cwd();
|
|
544
788
|
var cursorFlushMs = Math.max(500, Number(process.env.AGENTSCHAT_MCP_CURSOR_FLUSH_MS || 5000));
|
|
545
789
|
var cursors = new Map;
|
|
@@ -597,6 +841,8 @@ async function joinMemberships(ws, id) {
|
|
|
597
841
|
return;
|
|
598
842
|
}
|
|
599
843
|
const body = await r.json();
|
|
844
|
+
if (!currentIdentity(id) || socketsByBot.get(id.botId) !== ws)
|
|
845
|
+
return;
|
|
600
846
|
const channels = Array.isArray(body) ? body : body.channels || [];
|
|
601
847
|
const joined = [];
|
|
602
848
|
for (const ch of channels) {
|
|
@@ -611,7 +857,7 @@ async function joinMemberships(ws, id) {
|
|
|
611
857
|
clearTimeout(prev);
|
|
612
858
|
const t = setTimeout(() => {
|
|
613
859
|
backfillTimerByBot.delete(id.botId);
|
|
614
|
-
if (
|
|
860
|
+
if (!currentIdentity(id) || socketsByBot.get(id.botId) !== ws)
|
|
615
861
|
return;
|
|
616
862
|
backfillIdentity(id, joined);
|
|
617
863
|
}, 2000);
|
|
@@ -624,6 +870,8 @@ async function joinMemberships(ws, id) {
|
|
|
624
870
|
async function backfillIdentity(id, channelIds) {
|
|
625
871
|
const s = cursorFor(id);
|
|
626
872
|
for (const channelId of channelIds) {
|
|
873
|
+
if (!currentIdentity(id))
|
|
874
|
+
return;
|
|
627
875
|
try {
|
|
628
876
|
const after = s.map.get(channelId);
|
|
629
877
|
const params = after ? `?after=${encodeURIComponent(after)}&limit=50` : `?limit=1`;
|
|
@@ -633,6 +881,8 @@ async function backfillIdentity(id, channelIds) {
|
|
|
633
881
|
if (!r.ok)
|
|
634
882
|
continue;
|
|
635
883
|
const msgs = (await r.json())?.messages ?? [];
|
|
884
|
+
if (!currentIdentity(id))
|
|
885
|
+
return;
|
|
636
886
|
const plan = planBackfill(after, msgs, id.agentId);
|
|
637
887
|
if (plan.seed) {
|
|
638
888
|
s.map.set(channelId, plan.seed);
|
|
@@ -644,11 +894,9 @@ async function backfillIdentity(id, channelIds) {
|
|
|
644
894
|
log(`backfill ${id.botId} ${channelId}: ${plan.replay.length} missed msg(s)`);
|
|
645
895
|
for (const m of plan.replay) {
|
|
646
896
|
const frame = { ...m, type: "message", channel_id: m.channel_id ?? channelId, __botId: id.botId, __source: "backfill" };
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
advanceCursor(id, frame.channel_id, frame.timestamp);
|
|
651
|
-
broadcast?.(frame);
|
|
897
|
+
if (ingestAgentsChatFrame(id, frame, { advanceCursor, dedup })) {
|
|
898
|
+
broadcast?.(frame);
|
|
899
|
+
}
|
|
652
900
|
}
|
|
653
901
|
} catch (e) {
|
|
654
902
|
log(`backfill failed for ${id.botId} ${channelId}: ${e?.message ?? e}`);
|
|
@@ -656,49 +904,101 @@ async function backfillIdentity(id, channelIds) {
|
|
|
656
904
|
}
|
|
657
905
|
}
|
|
658
906
|
function connectIdentity(id) {
|
|
907
|
+
if (!currentIdentity(id))
|
|
908
|
+
return;
|
|
909
|
+
cancelReconnect(id.botId);
|
|
910
|
+
stopHeartbeat(id.botId);
|
|
911
|
+
const prev = socketsByBot.get(id.botId);
|
|
912
|
+
if (prev) {
|
|
913
|
+
try {
|
|
914
|
+
prev.removeAllListeners("close");
|
|
915
|
+
prev.close();
|
|
916
|
+
} catch {}
|
|
917
|
+
socketsByBot.delete(id.botId);
|
|
918
|
+
}
|
|
919
|
+
let heartbeatForced = false;
|
|
659
920
|
const ws = new WS(WS_URL);
|
|
660
921
|
socketsByBot.set(id.botId, ws);
|
|
922
|
+
const hb = new HeartbeatMonitor({
|
|
923
|
+
sendPing: () => {
|
|
924
|
+
try {
|
|
925
|
+
if (ws.readyState === WS.OPEN) {
|
|
926
|
+
ws.send(JSON.stringify({ type: "ping", timestamp: new Date().toISOString() }));
|
|
927
|
+
}
|
|
928
|
+
} catch {}
|
|
929
|
+
},
|
|
930
|
+
reconnect: () => {
|
|
931
|
+
if (!currentIdentity(id) || socketsByBot.get(id.botId) !== ws || heartbeatForced)
|
|
932
|
+
return;
|
|
933
|
+
heartbeatForced = true;
|
|
934
|
+
log(`agentschat WS heartbeat timeout for ${id.botId}; forcing reconnect`);
|
|
935
|
+
backoffByBot.set(id.botId, 1000);
|
|
936
|
+
try {
|
|
937
|
+
ws.removeAllListeners("close");
|
|
938
|
+
ws.close();
|
|
939
|
+
} catch {}
|
|
940
|
+
socketsByBot.delete(id.botId);
|
|
941
|
+
stopHeartbeat(id.botId);
|
|
942
|
+
scheduleReconnect(id, 500);
|
|
943
|
+
},
|
|
944
|
+
getReadyState: () => ws.readyState ?? WS_CLOSED
|
|
945
|
+
}, HB_PING_MS, HB_PONG_MS, HB_CONNECT_MS);
|
|
946
|
+
heartbeatsByBot.set(id.botId, hb);
|
|
947
|
+
hb.start();
|
|
661
948
|
ws.on("open", () => {
|
|
949
|
+
if (!currentIdentity(id) || socketsByBot.get(id.botId) !== ws) {
|
|
950
|
+
ws.close();
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
662
953
|
backoffByBot.set(id.botId, 1000);
|
|
663
954
|
try {
|
|
664
955
|
ws.send(JSON.stringify({ type: "auth", agent_id: id.agentId, token: id.token, capabilities: ["chat"] }));
|
|
665
956
|
} catch {}
|
|
666
957
|
});
|
|
667
958
|
ws.on("message", (raw) => {
|
|
959
|
+
if (!currentIdentity(id) || socketsByBot.get(id.botId) !== ws)
|
|
960
|
+
return;
|
|
668
961
|
let data;
|
|
669
962
|
try {
|
|
670
963
|
data = JSON.parse(String(raw));
|
|
671
964
|
} catch {
|
|
672
965
|
return;
|
|
673
966
|
}
|
|
967
|
+
if (data.type === "pong") {
|
|
968
|
+
hb.receivedPong();
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
674
971
|
if (data.type === "auth_ok") {
|
|
972
|
+
hb.receivedPong();
|
|
675
973
|
log(`connected to agentschat as ${id.agentId}`);
|
|
676
974
|
joinMemberships(ws, id);
|
|
677
975
|
return;
|
|
678
976
|
}
|
|
679
977
|
if (data.type === "channel_created" && data.channel_id) {
|
|
978
|
+
hb.receivedPong();
|
|
680
979
|
joinChannel(ws, id, data.channel_id, data.name);
|
|
681
980
|
return;
|
|
682
981
|
}
|
|
683
982
|
if (data.type === "message") {
|
|
983
|
+
hb.receivedPong();
|
|
684
984
|
if (!data.__source)
|
|
685
985
|
data.__source = "live";
|
|
686
|
-
|
|
687
|
-
if (key && dedup.recordOrSkip(key))
|
|
688
|
-
return;
|
|
689
|
-
if (data.content !== "__typing__")
|
|
690
|
-
advanceCursor(id, data.channel_id, data.timestamp);
|
|
691
|
-
if (data.sender_id !== id.agentId && data.content !== "__typing__") {
|
|
986
|
+
if (ingestAgentsChatFrame(id, data, { advanceCursor, dedup })) {
|
|
692
987
|
broadcast?.({ ...data, __botId: id.botId });
|
|
693
988
|
}
|
|
694
989
|
}
|
|
695
990
|
});
|
|
696
991
|
ws.on("close", () => {
|
|
992
|
+
if (!currentIdentity(id) || socketsByBot.get(id.botId) !== ws)
|
|
993
|
+
return;
|
|
994
|
+
stopHeartbeat(id.botId);
|
|
697
995
|
if (process.__shutdown)
|
|
698
996
|
return;
|
|
997
|
+
if (heartbeatForced)
|
|
998
|
+
return;
|
|
699
999
|
const delay = backoffByBot.get(id.botId) ?? 1000;
|
|
700
1000
|
log(`agentschat WS closed for ${id.botId}; reconnecting in ${delay}ms`);
|
|
701
|
-
|
|
1001
|
+
scheduleReconnect(id, delay);
|
|
702
1002
|
backoffByBot.set(id.botId, Math.min(delay * 2, 30000));
|
|
703
1003
|
});
|
|
704
1004
|
ws.on("error", (e) => log(`agentschat WS error (${id.botId}): ${e?.message ?? e}`));
|
|
@@ -710,19 +1010,21 @@ var connector = startConnector({
|
|
|
710
1010
|
identities,
|
|
711
1011
|
agentschat: {
|
|
712
1012
|
async sendMessage(botId, chatId, content, replyTo) {
|
|
713
|
-
const id = identities
|
|
1013
|
+
const id = requireIdentity(identities, botId);
|
|
714
1014
|
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}/messages`, {
|
|
715
1015
|
method: "POST",
|
|
716
1016
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${id.token}` },
|
|
717
1017
|
body: JSON.stringify({ sender_id: id.agentId, content_type: "text", content, ...replyTo ? { parent_id: replyTo } : {} })
|
|
1018
|
+
}).catch(() => {
|
|
1019
|
+
throw new PlatformNetworkError;
|
|
718
1020
|
});
|
|
719
1021
|
if (!res.ok)
|
|
720
|
-
throw new
|
|
1022
|
+
throw new PlatformHttpError(res.status);
|
|
721
1023
|
const data = await res.json();
|
|
722
1024
|
return { id: data?.id };
|
|
723
1025
|
},
|
|
724
1026
|
async getChatInfo(botId, chatId) {
|
|
725
|
-
const id = identities
|
|
1027
|
+
const id = requireIdentity(identities, botId);
|
|
726
1028
|
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}`, {
|
|
727
1029
|
headers: { Authorization: `Bearer ${id.token}` }
|
|
728
1030
|
});
|
|
@@ -732,7 +1034,7 @@ var connector = startConnector({
|
|
|
732
1034
|
return { name: data?.name ?? chatId, type: chatId.startsWith("dm-") ? "dm" : "group" };
|
|
733
1035
|
},
|
|
734
1036
|
async sendTyping(botId, chatId) {
|
|
735
|
-
const id = identities
|
|
1037
|
+
const id = requireIdentity(identities, botId);
|
|
736
1038
|
const ws = socketsByBot.get(id.botId);
|
|
737
1039
|
if (ws && ws.readyState === WS.OPEN) {
|
|
738
1040
|
try {
|
|
@@ -740,9 +1042,10 @@ var connector = startConnector({
|
|
|
740
1042
|
} catch {}
|
|
741
1043
|
}
|
|
742
1044
|
},
|
|
743
|
-
async getChannelContext(botId, chatId, sinceTs, excludeId) {
|
|
744
|
-
const id = identities
|
|
1045
|
+
async getChannelContext(botId, chatId, sinceTs, excludeId, signal) {
|
|
1046
|
+
const id = requireIdentity(identities, botId);
|
|
745
1047
|
const res = await fetch(`${API}/api/channels/${encodeURIComponent(chatId)}/messages?limit=50`, {
|
|
1048
|
+
signal,
|
|
746
1049
|
headers: { Authorization: `Bearer ${id.token}` }
|
|
747
1050
|
});
|
|
748
1051
|
if (!res.ok)
|
|
@@ -762,12 +1065,70 @@ var connector = startConnector({
|
|
|
762
1065
|
logger: log
|
|
763
1066
|
});
|
|
764
1067
|
broadcast = (msg) => connector.injectAgentsChatMessage(msg);
|
|
1068
|
+
await connector.ready;
|
|
765
1069
|
log(`listening on ${HOST}:${connector.port} (contract v1, ${identities.length} identit${identities.length === 1 ? "y" : "ies"})`);
|
|
766
1070
|
for (const id of identities)
|
|
767
1071
|
connectIdentity(id);
|
|
1072
|
+
function reloadIdentitiesFromConfig() {
|
|
1073
|
+
const next = loadIdentities().map((id) => identities.find((prev) => prev.botId === id.botId && prev.agentId === id.agentId && prev.token === id.token && prev.gatewayId === id.gatewayId && prev.secret === id.secret && prev.profile === id.profile) ?? id);
|
|
1074
|
+
const prevByBot = new Map(identities.map((i) => [i.botId, i]));
|
|
1075
|
+
const nextByBot = new Map(next.map((i) => [i.botId, i]));
|
|
1076
|
+
const added = next.filter((i) => !prevByBot.has(i.botId));
|
|
1077
|
+
const removed = identities.filter((i) => !nextByBot.has(i.botId));
|
|
1078
|
+
const kept = next.filter((i) => prevByBot.has(i.botId));
|
|
1079
|
+
connector.reloadIdentities(next);
|
|
1080
|
+
identities = next;
|
|
1081
|
+
for (const id of removed) {
|
|
1082
|
+
cancelReconnect(id.botId);
|
|
1083
|
+
log(`hot-reload: disconnecting removed identity ${id.botId}`);
|
|
1084
|
+
stopHeartbeat(id.botId);
|
|
1085
|
+
const t = backfillTimerByBot.get(id.botId);
|
|
1086
|
+
if (t) {
|
|
1087
|
+
clearTimeout(t);
|
|
1088
|
+
backfillTimerByBot.delete(id.botId);
|
|
1089
|
+
}
|
|
1090
|
+
const ws = socketsByBot.get(id.botId);
|
|
1091
|
+
if (ws) {
|
|
1092
|
+
try {
|
|
1093
|
+
ws.removeAllListeners("close");
|
|
1094
|
+
ws.close();
|
|
1095
|
+
} catch {}
|
|
1096
|
+
socketsByBot.delete(id.botId);
|
|
1097
|
+
}
|
|
1098
|
+
backoffByBot.delete(id.botId);
|
|
1099
|
+
}
|
|
1100
|
+
for (const id of kept) {
|
|
1101
|
+
const prev = prevByBot.get(id.botId);
|
|
1102
|
+
if (prev !== id) {
|
|
1103
|
+
cancelReconnect(id.botId);
|
|
1104
|
+
const timer = backfillTimerByBot.get(id.botId);
|
|
1105
|
+
if (timer)
|
|
1106
|
+
clearTimeout(timer);
|
|
1107
|
+
backfillTimerByBot.delete(id.botId);
|
|
1108
|
+
log(`hot-reload: reconnecting ${id.botId} (credentials changed)`);
|
|
1109
|
+
connectIdentity(id);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
for (const id of added) {
|
|
1113
|
+
log(`hot-reload: connecting new identity ${id.botId}`);
|
|
1114
|
+
connectIdentity(id);
|
|
1115
|
+
}
|
|
1116
|
+
log(`hot-reload complete: ${identities.length} identit${identities.length === 1 ? "y" : "ies"} (${added.length} added, ${removed.length} removed)`);
|
|
1117
|
+
}
|
|
1118
|
+
process.on("SIGHUP", () => {
|
|
1119
|
+
try {
|
|
1120
|
+
reloadIdentitiesFromConfig();
|
|
1121
|
+
} catch (e) {
|
|
1122
|
+
log(`hot-reload FAILED (keeping previous identities): ${e?.message ?? e}`);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
768
1125
|
for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
769
1126
|
process.on(sig, () => {
|
|
770
1127
|
process.__shutdown = true;
|
|
1128
|
+
for (const botId of reconnectTimerByBot.keys())
|
|
1129
|
+
cancelReconnect(botId);
|
|
1130
|
+
for (const botId of [...heartbeatsByBot.keys()])
|
|
1131
|
+
stopHeartbeat(botId);
|
|
771
1132
|
for (const ws of socketsByBot.values()) {
|
|
772
1133
|
try {
|
|
773
1134
|
ws.close();
|