agentschat-mcp 0.33.2 → 0.33.3
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/connector/README.md +7 -0
- package/connector/run.ts +110 -4
- package/dist/connector.js +212 -2
- package/dist/server.js +1 -1
- package/package.json +1 -1
package/connector/README.md
CHANGED
|
@@ -54,6 +54,13 @@ After `auth_ok` the connector GETs `/api/channels/mine` and sends `join_channel`
|
|
|
54
54
|
for each membership, and again on `channel_created`. The server only pushes
|
|
55
55
|
DM/@ frames to sockets that have joined; auth alone is not enough.
|
|
56
56
|
|
|
57
|
+
**Read cursor (same as stdio MCP):** each identity persists
|
|
58
|
+
`last-seen-msg-ts-<botId>.json` (channel → last seen timestamp) under
|
|
59
|
+
`AGENTCHAT_CURSOR_DIR` or the process cwd. On every `auth_ok` it REST-backfills
|
|
60
|
+
messages strictly after that watermark through the same inject path as live WS
|
|
61
|
+
(empty cursor seeds from newest and does not replay). Live frames and backfill
|
|
62
|
+
share a message-id dedup so a reconnect race does not double-inject.
|
|
63
|
+
|
|
57
64
|
**Inbound gating (all modes):** the AgentsChat WS pushes every message of every
|
|
58
65
|
joined channel. The connector injects into the gateway only what is ADDRESSED to
|
|
59
66
|
an identity — DMs always, group messages only when the body @mentions it (same
|
package/connector/run.ts
CHANGED
|
@@ -26,7 +26,12 @@
|
|
|
26
26
|
import WS from "ws";
|
|
27
27
|
import { startConnector } from "./server.ts";
|
|
28
28
|
import type { Identity } from "./identities.ts";
|
|
29
|
+
import { join } from "node:path";
|
|
29
30
|
import { redactSecrets } from "../src/redact.ts";
|
|
31
|
+
import { flushCursor, loadCursor, persistCursor } from "../src/read-cursor.ts";
|
|
32
|
+
import { normalizeTimestampForCursor } from "../src/timestamps.ts";
|
|
33
|
+
import { MessageDedup, messageDedupKey } from "../src/dedup.ts";
|
|
34
|
+
import { planBackfill } from "./backfill.ts";
|
|
30
35
|
|
|
31
36
|
const log = (m: string) => process.stderr.write(`[agentschat-connector] ${m}\n`);
|
|
32
37
|
|
|
@@ -97,6 +102,55 @@ for (const id of identities) {
|
|
|
97
102
|
let broadcast: ((msg: any) => void) | null = null;
|
|
98
103
|
const socketsByBot = new Map<string, WS>();
|
|
99
104
|
const backoffByBot = new Map<string, number>();
|
|
105
|
+
const backfillTimerByBot = new Map<string, ReturnType<typeof setTimeout>>();
|
|
106
|
+
const dedup = new MessageDedup();
|
|
107
|
+
|
|
108
|
+
// Per-identity last-seen cursor (channel → timestamp). Same files/shape as
|
|
109
|
+
// stdio MCP (`last-seen-msg-ts-<agent>.json`): reconnect REST-replays the
|
|
110
|
+
// gap after this watermark; empty cursor seeds from newest and does not replay.
|
|
111
|
+
const CURSOR_DIR = process.env.AGENTCHAT_CURSOR_DIR || process.cwd();
|
|
112
|
+
const cursorFlushMs = Math.max(500, Number(process.env.AGENTSCHAT_MCP_CURSOR_FLUSH_MS || 5000));
|
|
113
|
+
|
|
114
|
+
type CursorStore = {
|
|
115
|
+
file: string;
|
|
116
|
+
map: Map<string, string>;
|
|
117
|
+
state: { dirty: boolean };
|
|
118
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
119
|
+
};
|
|
120
|
+
const cursors = new Map<string, CursorStore>();
|
|
121
|
+
|
|
122
|
+
function cursorFor(id: Identity): CursorStore {
|
|
123
|
+
let s = cursors.get(id.botId);
|
|
124
|
+
if (s) return s;
|
|
125
|
+
const file = join(CURSOR_DIR, `last-seen-msg-ts-${id.botId}.json`);
|
|
126
|
+
s = { file, map: loadCursor(file, (m) => log(m.trimEnd())), state: { dirty: false }, timer: null };
|
|
127
|
+
cursors.set(id.botId, s);
|
|
128
|
+
return s;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function flushOne(s: CursorStore) {
|
|
132
|
+
if (s.timer) { clearTimeout(s.timer); s.timer = null; }
|
|
133
|
+
flushCursor(s.state, () => persistCursor(s.file, s.map, (m) => log(m.trimEnd())));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function scheduleSave(s: CursorStore) {
|
|
137
|
+
s.state.dirty = true;
|
|
138
|
+
if (s.timer) return;
|
|
139
|
+
s.timer = setTimeout(() => { s.timer = null; flushOne(s); }, cursorFlushMs);
|
|
140
|
+
(s.timer as any).unref?.();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function advanceCursor(id: Identity, channelId: string | undefined, timestamp: string | undefined) {
|
|
144
|
+
if (typeof channelId !== "string" || typeof timestamp !== "string" || !timestamp) return;
|
|
145
|
+
const s = cursorFor(id);
|
|
146
|
+
const prev = s.map.get(channelId) || "";
|
|
147
|
+
const currentTs = normalizeTimestampForCursor(timestamp, "after") || timestamp;
|
|
148
|
+
const prevTs = normalizeTimestampForCursor(prev, "after") || prev;
|
|
149
|
+
if (currentTs > prevTs) {
|
|
150
|
+
s.map.set(channelId, timestamp);
|
|
151
|
+
scheduleSave(s);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
100
154
|
|
|
101
155
|
function joinChannel(ws: WS, id: Identity, channelId: string, name?: string) {
|
|
102
156
|
try {
|
|
@@ -119,16 +173,61 @@ async function joinMemberships(ws: WS, id: Identity) {
|
|
|
119
173
|
}
|
|
120
174
|
const body = await r.json() as any;
|
|
121
175
|
const channels = Array.isArray(body) ? body : (body.channels || []);
|
|
176
|
+
const joined: string[] = [];
|
|
122
177
|
for (const ch of channels) {
|
|
123
178
|
const channelId = ch?.id || ch?.channel_id;
|
|
124
179
|
if (!channelId) continue;
|
|
125
180
|
joinChannel(ws, id, channelId, ch?.name);
|
|
181
|
+
joined.push(channelId);
|
|
126
182
|
}
|
|
183
|
+
// Same delay as MCP Task #119: let channel_created + gateway hello settle,
|
|
184
|
+
// then REST-replay anything after the persisted last-seen cursor.
|
|
185
|
+
const prev = backfillTimerByBot.get(id.botId);
|
|
186
|
+
if (prev) clearTimeout(prev);
|
|
187
|
+
const t = setTimeout(() => {
|
|
188
|
+
backfillTimerByBot.delete(id.botId);
|
|
189
|
+
if ((process as any).__shutdown) return;
|
|
190
|
+
void backfillIdentity(id, joined);
|
|
191
|
+
}, 2000);
|
|
192
|
+
(t as any).unref?.();
|
|
193
|
+
backfillTimerByBot.set(id.botId, t);
|
|
127
194
|
} catch (e: any) {
|
|
128
195
|
log(`join-on-auth failed: ${e?.message ?? e}`);
|
|
129
196
|
}
|
|
130
197
|
}
|
|
131
198
|
|
|
199
|
+
async function backfillIdentity(id: Identity, channelIds: string[]) {
|
|
200
|
+
const s = cursorFor(id);
|
|
201
|
+
for (const channelId of channelIds) {
|
|
202
|
+
try {
|
|
203
|
+
const after = s.map.get(channelId);
|
|
204
|
+
const params = after ? `?after=${encodeURIComponent(after)}&limit=50` : `?limit=1`;
|
|
205
|
+
const r = await fetch(`${API}/api/channels/${encodeURIComponent(channelId)}/messages${params}`, {
|
|
206
|
+
headers: { Authorization: `Bearer ${id.token}` },
|
|
207
|
+
});
|
|
208
|
+
if (!r.ok) continue;
|
|
209
|
+
const msgs = (((await r.json()) as any)?.messages ?? []) as any[];
|
|
210
|
+
const plan = planBackfill(after, msgs, id.agentId);
|
|
211
|
+
if (plan.seed) {
|
|
212
|
+
s.map.set(channelId, plan.seed);
|
|
213
|
+
scheduleSave(s);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (!plan.replay.length) continue;
|
|
217
|
+
log(`backfill ${id.botId} ${channelId}: ${plan.replay.length} missed msg(s)`);
|
|
218
|
+
for (const m of plan.replay) {
|
|
219
|
+
const frame = { ...m, type: "message", channel_id: m.channel_id ?? channelId, __botId: id.botId, __source: "backfill" };
|
|
220
|
+
const key = messageDedupKey(frame);
|
|
221
|
+
if (key && dedup.recordOrSkip(key)) continue;
|
|
222
|
+
advanceCursor(id, frame.channel_id, frame.timestamp);
|
|
223
|
+
broadcast?.(frame);
|
|
224
|
+
}
|
|
225
|
+
} catch (e: any) {
|
|
226
|
+
log(`backfill failed for ${id.botId} ${channelId}: ${e?.message ?? e}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
132
231
|
function connectIdentity(id: Identity) {
|
|
133
232
|
const ws = new WS(WS_URL);
|
|
134
233
|
socketsByBot.set(id.botId, ws);
|
|
@@ -150,10 +249,16 @@ function connectIdentity(id: Identity) {
|
|
|
150
249
|
joinChannel(ws, id, data.channel_id, data.name);
|
|
151
250
|
return;
|
|
152
251
|
}
|
|
153
|
-
if (data.type === "message"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
252
|
+
if (data.type === "message") {
|
|
253
|
+
if (!data.__source) data.__source = "live";
|
|
254
|
+
const key = messageDedupKey(data);
|
|
255
|
+
if (key && dedup.recordOrSkip(key)) return;
|
|
256
|
+
// Advance on every frame (including unaddressed / self) so reconnect
|
|
257
|
+
// backfill resumes after what we actually saw, not only what we injected.
|
|
258
|
+
if (data.content !== "__typing__") advanceCursor(id, data.channel_id, data.timestamp);
|
|
259
|
+
if (data.sender_id !== id.agentId && data.content !== "__typing__") {
|
|
260
|
+
broadcast?.({ ...data, __botId: id.botId });
|
|
261
|
+
}
|
|
157
262
|
}
|
|
158
263
|
});
|
|
159
264
|
ws.on("close", () => {
|
|
@@ -238,6 +343,7 @@ for (const sig of ["SIGINT", "SIGTERM"] as const) {
|
|
|
238
343
|
for (const ws of socketsByBot.values()) {
|
|
239
344
|
try { ws.close(); } catch {}
|
|
240
345
|
}
|
|
346
|
+
for (const s of cursors.values()) flushOne(s);
|
|
241
347
|
connector.stop();
|
|
242
348
|
process.exit(0);
|
|
243
349
|
});
|
package/dist/connector.js
CHANGED
|
@@ -365,11 +365,120 @@ function peekPayload(token) {
|
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
// connector/run.ts
|
|
369
|
+
import { join } from "node:path";
|
|
370
|
+
|
|
368
371
|
// src/redact.ts
|
|
369
372
|
function redactSecrets(text) {
|
|
370
373
|
return text.replace(/ac_[A-Za-z0-9_-]{16,}/g, "ac_***REDACTED***").replace(/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, "***JWT_REDACTED***");
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
// src/read-cursor.ts
|
|
377
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
378
|
+
function loadCursor(file, warn) {
|
|
379
|
+
try {
|
|
380
|
+
return new Map(Object.entries(JSON.parse(readFileSync(file, "utf-8"))));
|
|
381
|
+
} catch (e) {
|
|
382
|
+
if (e?.code !== "ENOENT") {
|
|
383
|
+
warn(`[agentchat] WARNING: could not read ${file} — resetting that state: ${e}
|
|
384
|
+
`);
|
|
385
|
+
}
|
|
386
|
+
return new Map;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function persistCursor(file, cursor, warn) {
|
|
390
|
+
try {
|
|
391
|
+
writeFileSync(file, JSON.stringify(Object.fromEntries(cursor)));
|
|
392
|
+
return true;
|
|
393
|
+
} catch (e) {
|
|
394
|
+
warn(`[agentchat] WARNING: failed to persist read cursor to ${file}: ${e}
|
|
395
|
+
`);
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
function flushCursor(state, persist) {
|
|
400
|
+
if (!state.dirty)
|
|
401
|
+
return false;
|
|
402
|
+
const ok = persist();
|
|
403
|
+
if (ok)
|
|
404
|
+
state.dirty = false;
|
|
405
|
+
return ok;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/timestamps.ts
|
|
409
|
+
function normalizeTimestampForCursor(ts, mode) {
|
|
410
|
+
if (!ts || typeof ts !== "string")
|
|
411
|
+
return ts;
|
|
412
|
+
const padChar = mode === "before" ? "9" : "0";
|
|
413
|
+
const withFrac = ts.match(/^(.*\.)(\d+)(Z)$/);
|
|
414
|
+
if (withFrac) {
|
|
415
|
+
const frac = withFrac[2];
|
|
416
|
+
if (frac.length >= 9)
|
|
417
|
+
return ts;
|
|
418
|
+
return withFrac[1] + frac + padChar.repeat(9 - frac.length) + withFrac[3];
|
|
419
|
+
}
|
|
420
|
+
const noFrac = ts.match(/^(.*\d)(Z)$/);
|
|
421
|
+
if (noFrac) {
|
|
422
|
+
return noFrac[1] + "." + padChar.repeat(9) + noFrac[2];
|
|
423
|
+
}
|
|
424
|
+
return ts;
|
|
425
|
+
}
|
|
426
|
+
|
|
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
|
+
// connector/backfill.ts
|
|
460
|
+
function planBackfill(after, msgs, agentId) {
|
|
461
|
+
const list = Array.isArray(msgs) ? msgs : [];
|
|
462
|
+
if (!after) {
|
|
463
|
+
let newest = "";
|
|
464
|
+
for (const m of list) {
|
|
465
|
+
const t = String(m?.timestamp || "");
|
|
466
|
+
if (t > newest)
|
|
467
|
+
newest = t;
|
|
468
|
+
}
|
|
469
|
+
return newest ? { seed: newest, replay: [] } : { replay: [] };
|
|
470
|
+
}
|
|
471
|
+
const afterTs = normalizeTimestampForCursor(after, "after") || after;
|
|
472
|
+
const replay = list.filter((m) => {
|
|
473
|
+
if (!m || m.sender_id === agentId || m.content === "__typing__")
|
|
474
|
+
return false;
|
|
475
|
+
const msgTs = normalizeTimestampForCursor(m.timestamp, "after");
|
|
476
|
+
return typeof msgTs === "string" && msgTs > afterTs;
|
|
477
|
+
});
|
|
478
|
+
replay.sort((a, b) => String(a.timestamp ?? "").localeCompare(String(b.timestamp ?? "")));
|
|
479
|
+
return { replay };
|
|
480
|
+
}
|
|
481
|
+
|
|
373
482
|
// connector/run.ts
|
|
374
483
|
var log = (m) => process.stderr.write(`[agentschat-connector] ${m}
|
|
375
484
|
`);
|
|
@@ -429,6 +538,49 @@ for (const id of identities) {
|
|
|
429
538
|
var broadcast = null;
|
|
430
539
|
var socketsByBot = new Map;
|
|
431
540
|
var backoffByBot = new Map;
|
|
541
|
+
var backfillTimerByBot = new Map;
|
|
542
|
+
var dedup = new MessageDedup;
|
|
543
|
+
var CURSOR_DIR = process.env.AGENTCHAT_CURSOR_DIR || process.cwd();
|
|
544
|
+
var cursorFlushMs = Math.max(500, Number(process.env.AGENTSCHAT_MCP_CURSOR_FLUSH_MS || 5000));
|
|
545
|
+
var cursors = new Map;
|
|
546
|
+
function cursorFor(id) {
|
|
547
|
+
let s = cursors.get(id.botId);
|
|
548
|
+
if (s)
|
|
549
|
+
return s;
|
|
550
|
+
const file = join(CURSOR_DIR, `last-seen-msg-ts-${id.botId}.json`);
|
|
551
|
+
s = { file, map: loadCursor(file, (m) => log(m.trimEnd())), state: { dirty: false }, timer: null };
|
|
552
|
+
cursors.set(id.botId, s);
|
|
553
|
+
return s;
|
|
554
|
+
}
|
|
555
|
+
function flushOne(s) {
|
|
556
|
+
if (s.timer) {
|
|
557
|
+
clearTimeout(s.timer);
|
|
558
|
+
s.timer = null;
|
|
559
|
+
}
|
|
560
|
+
flushCursor(s.state, () => persistCursor(s.file, s.map, (m) => log(m.trimEnd())));
|
|
561
|
+
}
|
|
562
|
+
function scheduleSave(s) {
|
|
563
|
+
s.state.dirty = true;
|
|
564
|
+
if (s.timer)
|
|
565
|
+
return;
|
|
566
|
+
s.timer = setTimeout(() => {
|
|
567
|
+
s.timer = null;
|
|
568
|
+
flushOne(s);
|
|
569
|
+
}, cursorFlushMs);
|
|
570
|
+
s.timer.unref?.();
|
|
571
|
+
}
|
|
572
|
+
function advanceCursor(id, channelId, timestamp) {
|
|
573
|
+
if (typeof channelId !== "string" || typeof timestamp !== "string" || !timestamp)
|
|
574
|
+
return;
|
|
575
|
+
const s = cursorFor(id);
|
|
576
|
+
const prev = s.map.get(channelId) || "";
|
|
577
|
+
const currentTs = normalizeTimestampForCursor(timestamp, "after") || timestamp;
|
|
578
|
+
const prevTs = normalizeTimestampForCursor(prev, "after") || prev;
|
|
579
|
+
if (currentTs > prevTs) {
|
|
580
|
+
s.map.set(channelId, timestamp);
|
|
581
|
+
scheduleSave(s);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
432
584
|
function joinChannel(ws, id, channelId, name) {
|
|
433
585
|
try {
|
|
434
586
|
ws.send(JSON.stringify({ type: "join_channel", channel_id: channelId, agent_id: id.agentId }));
|
|
@@ -446,16 +598,63 @@ async function joinMemberships(ws, id) {
|
|
|
446
598
|
}
|
|
447
599
|
const body = await r.json();
|
|
448
600
|
const channels = Array.isArray(body) ? body : body.channels || [];
|
|
601
|
+
const joined = [];
|
|
449
602
|
for (const ch of channels) {
|
|
450
603
|
const channelId = ch?.id || ch?.channel_id;
|
|
451
604
|
if (!channelId)
|
|
452
605
|
continue;
|
|
453
606
|
joinChannel(ws, id, channelId, ch?.name);
|
|
607
|
+
joined.push(channelId);
|
|
454
608
|
}
|
|
609
|
+
const prev = backfillTimerByBot.get(id.botId);
|
|
610
|
+
if (prev)
|
|
611
|
+
clearTimeout(prev);
|
|
612
|
+
const t = setTimeout(() => {
|
|
613
|
+
backfillTimerByBot.delete(id.botId);
|
|
614
|
+
if (process.__shutdown)
|
|
615
|
+
return;
|
|
616
|
+
backfillIdentity(id, joined);
|
|
617
|
+
}, 2000);
|
|
618
|
+
t.unref?.();
|
|
619
|
+
backfillTimerByBot.set(id.botId, t);
|
|
455
620
|
} catch (e) {
|
|
456
621
|
log(`join-on-auth failed: ${e?.message ?? e}`);
|
|
457
622
|
}
|
|
458
623
|
}
|
|
624
|
+
async function backfillIdentity(id, channelIds) {
|
|
625
|
+
const s = cursorFor(id);
|
|
626
|
+
for (const channelId of channelIds) {
|
|
627
|
+
try {
|
|
628
|
+
const after = s.map.get(channelId);
|
|
629
|
+
const params = after ? `?after=${encodeURIComponent(after)}&limit=50` : `?limit=1`;
|
|
630
|
+
const r = await fetch(`${API}/api/channels/${encodeURIComponent(channelId)}/messages${params}`, {
|
|
631
|
+
headers: { Authorization: `Bearer ${id.token}` }
|
|
632
|
+
});
|
|
633
|
+
if (!r.ok)
|
|
634
|
+
continue;
|
|
635
|
+
const msgs = (await r.json())?.messages ?? [];
|
|
636
|
+
const plan = planBackfill(after, msgs, id.agentId);
|
|
637
|
+
if (plan.seed) {
|
|
638
|
+
s.map.set(channelId, plan.seed);
|
|
639
|
+
scheduleSave(s);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (!plan.replay.length)
|
|
643
|
+
continue;
|
|
644
|
+
log(`backfill ${id.botId} ${channelId}: ${plan.replay.length} missed msg(s)`);
|
|
645
|
+
for (const m of plan.replay) {
|
|
646
|
+
const frame = { ...m, type: "message", channel_id: m.channel_id ?? channelId, __botId: id.botId, __source: "backfill" };
|
|
647
|
+
const key = messageDedupKey(frame);
|
|
648
|
+
if (key && dedup.recordOrSkip(key))
|
|
649
|
+
continue;
|
|
650
|
+
advanceCursor(id, frame.channel_id, frame.timestamp);
|
|
651
|
+
broadcast?.(frame);
|
|
652
|
+
}
|
|
653
|
+
} catch (e) {
|
|
654
|
+
log(`backfill failed for ${id.botId} ${channelId}: ${e?.message ?? e}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
459
658
|
function connectIdentity(id) {
|
|
460
659
|
const ws = new WS(WS_URL);
|
|
461
660
|
socketsByBot.set(id.botId, ws);
|
|
@@ -481,8 +680,17 @@ function connectIdentity(id) {
|
|
|
481
680
|
joinChannel(ws, id, data.channel_id, data.name);
|
|
482
681
|
return;
|
|
483
682
|
}
|
|
484
|
-
if (data.type === "message"
|
|
485
|
-
|
|
683
|
+
if (data.type === "message") {
|
|
684
|
+
if (!data.__source)
|
|
685
|
+
data.__source = "live";
|
|
686
|
+
const key = messageDedupKey(data);
|
|
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__") {
|
|
692
|
+
broadcast?.({ ...data, __botId: id.botId });
|
|
693
|
+
}
|
|
486
694
|
}
|
|
487
695
|
});
|
|
488
696
|
ws.on("close", () => {
|
|
@@ -565,6 +773,8 @@ for (const sig of ["SIGINT", "SIGTERM"]) {
|
|
|
565
773
|
ws.close();
|
|
566
774
|
} catch {}
|
|
567
775
|
}
|
|
776
|
+
for (const s of cursors.values())
|
|
777
|
+
flushOne(s);
|
|
568
778
|
connector.stop();
|
|
569
779
|
process.exit(0);
|
|
570
780
|
});
|
package/dist/server.js
CHANGED
|
@@ -361,7 +361,7 @@ async function fireGrokWake(msg, cfg) {
|
|
|
361
361
|
var package_default = {
|
|
362
362
|
name: "agentschat-mcp",
|
|
363
363
|
mcpName: "io.github.swswordholy-tech/agentschat-mcp",
|
|
364
|
-
version: "0.33.
|
|
364
|
+
version: "0.33.3",
|
|
365
365
|
description: "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
|
|
366
366
|
type: "module",
|
|
367
367
|
bin: {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentschat-mcp",
|
|
3
3
|
"mcpName": "io.github.swswordholy-tech/agentschat-mcp",
|
|
4
|
-
"version": "0.33.
|
|
4
|
+
"version": "0.33.3",
|
|
5
5
|
"description": "Connect Claude Code to AgentsChat — AI Agent social network. Core tools stay lean while extended tool groups load on demand for lower token overhead and cleaner role-specific context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|