@diegoaltoworks/chatter 0.60.0 → 0.61.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/README.md +2 -1
- package/dist/channels/matrix/api.d.ts +133 -0
- package/dist/channels/matrix/api.d.ts.map +1 -0
- package/dist/channels/matrix/channel.d.ts +98 -0
- package/dist/channels/matrix/channel.d.ts.map +1 -0
- package/dist/channels/matrix/handler.d.ts +51 -0
- package/dist/channels/matrix/handler.d.ts.map +1 -0
- package/dist/channels/matrix/index.d.ts +37 -0
- package/dist/channels/matrix/index.d.ts.map +1 -0
- package/dist/channels/matrix/index.js +972 -0
- package/dist/channels/matrix/index.mjs +929 -0
- package/dist/channels/matrix/sync.d.ts +50 -0
- package/dist/channels/matrix/sync.d.ts.map +1 -0
- package/dist/channels/matrix/updates.d.ts +75 -0
- package/dist/channels/matrix/updates.d.ts.map +1 -0
- package/package.json +8 -2
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
// src/channels/matrix/api.ts
|
|
2
|
+
var MatrixApiError = class extends Error {
|
|
3
|
+
errcode;
|
|
4
|
+
status;
|
|
5
|
+
retryAfterMs;
|
|
6
|
+
constructor(message, options) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "MatrixApiError";
|
|
9
|
+
this.errcode = options?.errcode;
|
|
10
|
+
this.status = options?.status;
|
|
11
|
+
this.retryAfterMs = options?.retryAfterMs;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
function redactToken(text, token) {
|
|
15
|
+
if (!token) return text;
|
|
16
|
+
return text.split(token).join("***");
|
|
17
|
+
}
|
|
18
|
+
var MEDIA_MSGTYPE = {
|
|
19
|
+
image: "m.image",
|
|
20
|
+
file: "m.file",
|
|
21
|
+
video: "m.video",
|
|
22
|
+
audio: "m.audio"
|
|
23
|
+
};
|
|
24
|
+
function errorText(error) {
|
|
25
|
+
return error instanceof Error ? error.message : String(error);
|
|
26
|
+
}
|
|
27
|
+
var txnCounter = 0;
|
|
28
|
+
function nextTxnId() {
|
|
29
|
+
txnCounter += 1;
|
|
30
|
+
return `chatter-${Date.now()}-${txnCounter}`;
|
|
31
|
+
}
|
|
32
|
+
function toMediaRequest(payload) {
|
|
33
|
+
const media = typeof payload === "string" ? { url: payload } : payload;
|
|
34
|
+
if (!media || typeof media !== "object" || typeof media.url !== "string" || !media.url) {
|
|
35
|
+
throw new TypeError(
|
|
36
|
+
`Matrix sendMedia payload must be a URL/mxc string or { url, kind?, caption?, filename? } \u2014 got: ${typeof payload}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
const kind = media.kind ?? "image";
|
|
40
|
+
if (!MEDIA_MSGTYPE[kind]) {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
`Matrix sendMedia kind must be one of ${Object.keys(MEDIA_MSGTYPE).join(", ")} \u2014 got "${kind}"`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return { kind, url: media.url, caption: media.caption, filename: media.filename };
|
|
46
|
+
}
|
|
47
|
+
function createMatrixApi(config) {
|
|
48
|
+
const token = config.accessToken;
|
|
49
|
+
if (!token.trim()) throw new Error("Matrix accessToken is required (e.g. MATRIX_ACCESS_TOKEN)");
|
|
50
|
+
const homeserverUrl = config.homeserverUrl.trim().replace(/\/+$/, "");
|
|
51
|
+
if (!homeserverUrl)
|
|
52
|
+
throw new Error("Matrix homeserverUrl is required (e.g. https://matrix.example.org)");
|
|
53
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
54
|
+
async function call(method, path, options) {
|
|
55
|
+
const search = new URLSearchParams();
|
|
56
|
+
for (const [key, value] of Object.entries(options?.query ?? {})) {
|
|
57
|
+
if (value !== void 0) search.set(key, String(value));
|
|
58
|
+
}
|
|
59
|
+
const qs = search.toString();
|
|
60
|
+
const url = `${homeserverUrl}${path}${qs ? `?${qs}` : ""}`;
|
|
61
|
+
let response;
|
|
62
|
+
try {
|
|
63
|
+
response = await doFetch(url, {
|
|
64
|
+
method,
|
|
65
|
+
headers: {
|
|
66
|
+
authorization: `Bearer ${token}`,
|
|
67
|
+
...options?.body !== void 0 ? { "content-type": "application/json" } : {}
|
|
68
|
+
},
|
|
69
|
+
body: options?.body !== void 0 ? JSON.stringify(options.body) : void 0,
|
|
70
|
+
signal: options?.signal
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new MatrixApiError(
|
|
74
|
+
`${method} ${path} request failed: ${redactToken(errorText(error), token)}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (response.ok) {
|
|
78
|
+
if (response.status === 204) return void 0;
|
|
79
|
+
try {
|
|
80
|
+
return await response.json();
|
|
81
|
+
} catch {
|
|
82
|
+
throw new MatrixApiError(
|
|
83
|
+
`${method} ${path} returned a non-JSON response (HTTP ${response.status})`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
body = await response.json();
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
throw new MatrixApiError(
|
|
93
|
+
`${method} ${path} failed (HTTP ${response.status}): ${redactToken(body?.error ?? "unknown error", token)}`,
|
|
94
|
+
{ errcode: body?.errcode, status: response.status, retryAfterMs: body?.retry_after_ms }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
async function sendEvent(roomId, content, options) {
|
|
98
|
+
const eventType = options?.eventType ?? "m.room.message";
|
|
99
|
+
const result = await call(
|
|
100
|
+
"PUT",
|
|
101
|
+
`/_matrix/client/v3/rooms/${encodeURIComponent(roomId)}/send/${encodeURIComponent(eventType)}/${nextTxnId()}`,
|
|
102
|
+
{ body: content }
|
|
103
|
+
);
|
|
104
|
+
return { eventId: result.event_id };
|
|
105
|
+
}
|
|
106
|
+
async function uploadMedia(bytes, contentType, filename) {
|
|
107
|
+
const search = new URLSearchParams();
|
|
108
|
+
if (filename) search.set("filename", filename);
|
|
109
|
+
const qs = search.toString();
|
|
110
|
+
let response;
|
|
111
|
+
try {
|
|
112
|
+
response = await doFetch(`${homeserverUrl}/_matrix/media/v3/upload${qs ? `?${qs}` : ""}`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { authorization: `Bearer ${token}`, "content-type": contentType },
|
|
115
|
+
// Uint8Array is a valid fetch BodyInit at runtime (Bun/Node both
|
|
116
|
+
// accept it); only the DOM lib's stricter overload set rejects it.
|
|
117
|
+
body: bytes
|
|
118
|
+
});
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw new MatrixApiError(`media upload failed: ${redactToken(errorText(error), token)}`);
|
|
121
|
+
}
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
let body;
|
|
124
|
+
try {
|
|
125
|
+
body = await response.json();
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
throw new MatrixApiError(
|
|
129
|
+
`media upload failed (HTTP ${response.status}): ${redactToken(body?.error ?? "unknown error", token)}`,
|
|
130
|
+
{ errcode: body?.errcode, status: response.status }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
let result;
|
|
134
|
+
try {
|
|
135
|
+
result = await response.json();
|
|
136
|
+
} catch {
|
|
137
|
+
throw new MatrixApiError(
|
|
138
|
+
`media upload returned a non-JSON response (HTTP ${response.status})`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
return { contentUri: result.content_uri };
|
|
142
|
+
}
|
|
143
|
+
async function sendMedia(roomId, payload) {
|
|
144
|
+
const media = toMediaRequest(payload);
|
|
145
|
+
let contentUri = media.url;
|
|
146
|
+
if (!contentUri.startsWith("mxc://")) {
|
|
147
|
+
let bytes;
|
|
148
|
+
let contentType = "application/octet-stream";
|
|
149
|
+
try {
|
|
150
|
+
const fetched = await doFetch(media.url);
|
|
151
|
+
if (!fetched.ok) throw new Error(`fetching media source returned HTTP ${fetched.status}`);
|
|
152
|
+
contentType = fetched.headers.get("content-type") ?? contentType;
|
|
153
|
+
bytes = new Uint8Array(await fetched.arrayBuffer());
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw new MatrixApiError(`could not fetch media source for upload: ${errorText(error)}`);
|
|
156
|
+
}
|
|
157
|
+
const filename = media.filename ?? media.url.split("/").pop() ?? media.kind;
|
|
158
|
+
const uploaded = await uploadMedia(bytes, contentType, filename);
|
|
159
|
+
contentUri = uploaded.contentUri;
|
|
160
|
+
}
|
|
161
|
+
return sendEvent(roomId, {
|
|
162
|
+
msgtype: MEDIA_MSGTYPE[media.kind],
|
|
163
|
+
body: media.caption ?? media.filename ?? media.kind,
|
|
164
|
+
url: contentUri
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
call,
|
|
169
|
+
async whoami() {
|
|
170
|
+
const result = await call("GET", "/_matrix/client/v3/account/whoami");
|
|
171
|
+
return { userId: result.user_id };
|
|
172
|
+
},
|
|
173
|
+
sync: ({ since, timeoutMs, signal }) => call("GET", "/_matrix/client/v3/sync", {
|
|
174
|
+
query: {
|
|
175
|
+
since,
|
|
176
|
+
timeout: timeoutMs,
|
|
177
|
+
// An initial sync (no `since`) returns each joined room's recent
|
|
178
|
+
// timeline by default — replaying it into the pipeline would
|
|
179
|
+
// re-answer messages that arrived before this process ever
|
|
180
|
+
// started. A bot only wants live traffic, so it asks for none:
|
|
181
|
+
// room state (needed for auto-join / m.direct bookkeeping) still
|
|
182
|
+
// comes through unaffected, only `timeline.events` is suppressed.
|
|
183
|
+
...since ? {} : { filter: JSON.stringify({ room: { timeline: { limit: 0 } } }) }
|
|
184
|
+
},
|
|
185
|
+
signal
|
|
186
|
+
}),
|
|
187
|
+
sendEvent,
|
|
188
|
+
uploadMedia,
|
|
189
|
+
sendMedia,
|
|
190
|
+
async joinRoom(roomIdOrAlias) {
|
|
191
|
+
await call("POST", `/_matrix/client/v3/join/${encodeURIComponent(roomIdOrAlias)}`, {
|
|
192
|
+
body: {}
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
async getAccountData(userId, type) {
|
|
196
|
+
try {
|
|
197
|
+
return await call(
|
|
198
|
+
"GET",
|
|
199
|
+
`/_matrix/client/v3/user/${encodeURIComponent(userId)}/account_data/${encodeURIComponent(type)}`
|
|
200
|
+
);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (error instanceof MatrixApiError && error.errcode === "M_NOT_FOUND") return void 0;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/core/logger.ts
|
|
210
|
+
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
211
|
+
function createConsoleLogger(level = "info") {
|
|
212
|
+
const threshold = LEVEL_ORDER[level];
|
|
213
|
+
const write = (at) => (...args) => {
|
|
214
|
+
if (LEVEL_ORDER[at] < threshold) return;
|
|
215
|
+
console.error(`[${at}]`, ...args);
|
|
216
|
+
};
|
|
217
|
+
return {
|
|
218
|
+
debug: write("debug"),
|
|
219
|
+
info: write("info"),
|
|
220
|
+
warn: write("warn"),
|
|
221
|
+
error: write("error")
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/core/guardrails.ts
|
|
226
|
+
function detectLeakage(text) {
|
|
227
|
+
const flags = [
|
|
228
|
+
/system prompt/i,
|
|
229
|
+
/hidden instruction/i,
|
|
230
|
+
/here (are|is) (my|the) rules/i,
|
|
231
|
+
/BEGIN SYSTEM PROMPT/i,
|
|
232
|
+
/tool instructions/i
|
|
233
|
+
];
|
|
234
|
+
return flags.some((rx) => rx.test(text));
|
|
235
|
+
}
|
|
236
|
+
function scrubOutput(text) {
|
|
237
|
+
return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
|
|
238
|
+
}
|
|
239
|
+
function guardOutput(text) {
|
|
240
|
+
if (detectLeakage(text)) {
|
|
241
|
+
return "Sorry, I can't share internal instructions. How else can I help?";
|
|
242
|
+
}
|
|
243
|
+
return scrubOutput(text);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/core/llm.ts
|
|
247
|
+
var DEFAULT_MODEL = "gpt-4o";
|
|
248
|
+
async function completeOnce({
|
|
249
|
+
client,
|
|
250
|
+
system,
|
|
251
|
+
messages,
|
|
252
|
+
temperature = 0.2,
|
|
253
|
+
model = DEFAULT_MODEL
|
|
254
|
+
}) {
|
|
255
|
+
const res = await client.chat.completions.create({
|
|
256
|
+
model,
|
|
257
|
+
temperature,
|
|
258
|
+
messages: [{ role: "system", content: system }, ...messages]
|
|
259
|
+
});
|
|
260
|
+
const text = res.choices[0]?.message?.content ?? "";
|
|
261
|
+
return {
|
|
262
|
+
content: guardOutput(text),
|
|
263
|
+
usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/core/answer.ts
|
|
268
|
+
var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
|
|
269
|
+
async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
|
|
270
|
+
const result = await answerFn({ system, messages, mode, sender, conversationId });
|
|
271
|
+
const raw = typeof result === "string" ? result : result?.content;
|
|
272
|
+
const usage = typeof result === "string" ? void 0 : result?.usage;
|
|
273
|
+
return {
|
|
274
|
+
content: guardOutput(typeof raw === "string" ? raw : ""),
|
|
275
|
+
usage: usage ?? noUsage()
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
async function answerOnce({
|
|
279
|
+
answerFn,
|
|
280
|
+
client,
|
|
281
|
+
system,
|
|
282
|
+
messages,
|
|
283
|
+
mode,
|
|
284
|
+
sender,
|
|
285
|
+
conversationId,
|
|
286
|
+
temperature,
|
|
287
|
+
model
|
|
288
|
+
}) {
|
|
289
|
+
if (answerFn) {
|
|
290
|
+
return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
|
|
291
|
+
}
|
|
292
|
+
return completeOnce({ client, system, messages, temperature, model });
|
|
293
|
+
}
|
|
294
|
+
async function applyTransformReply(transformReply, input, logger) {
|
|
295
|
+
if (!transformReply) return input.text;
|
|
296
|
+
try {
|
|
297
|
+
return await transformReply(input);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
logger?.error(
|
|
300
|
+
`transformReply threw for channel "${input.channel}"; sending the original reply`,
|
|
301
|
+
error
|
|
302
|
+
);
|
|
303
|
+
return input.text;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/core/buckets.ts
|
|
308
|
+
function defaultBuckets(mode) {
|
|
309
|
+
return ["base", mode];
|
|
310
|
+
}
|
|
311
|
+
function isIdentified(sender) {
|
|
312
|
+
return typeof sender === "string" && sender.trim().length > 0;
|
|
313
|
+
}
|
|
314
|
+
function normalize(buckets) {
|
|
315
|
+
const out = [];
|
|
316
|
+
for (const raw of buckets) {
|
|
317
|
+
if (typeof raw !== "string") continue;
|
|
318
|
+
const bucket = raw.trim();
|
|
319
|
+
if (bucket && !out.includes(bucket)) out.push(bucket);
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
async function resolveBuckets({
|
|
324
|
+
mode,
|
|
325
|
+
sender,
|
|
326
|
+
bucketsFor
|
|
327
|
+
}) {
|
|
328
|
+
if (!bucketsFor) return void 0;
|
|
329
|
+
const identified = isIdentified(sender);
|
|
330
|
+
const requested = await bucketsFor(identified ? { mode, sender } : { mode });
|
|
331
|
+
if (!Array.isArray(requested)) return void 0;
|
|
332
|
+
const cleaned = normalize(requested);
|
|
333
|
+
if (identified) return cleaned;
|
|
334
|
+
const ceiling = new Set(defaultBuckets(mode));
|
|
335
|
+
return cleaned.filter((bucket) => ceiling.has(bucket));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/core/messages.ts
|
|
339
|
+
function lastUserMessage(messages) {
|
|
340
|
+
return [...messages].reverse().find((m) => m.role === "user");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/core/pipeline.ts
|
|
344
|
+
var MODE_SETTINGS = {
|
|
345
|
+
public: { topK: 6, label: "Context" },
|
|
346
|
+
private: { topK: 8, label: "Internal Context" }
|
|
347
|
+
};
|
|
348
|
+
async function prepareChat({
|
|
349
|
+
store,
|
|
350
|
+
prompts,
|
|
351
|
+
mode,
|
|
352
|
+
messages,
|
|
353
|
+
personaLayer,
|
|
354
|
+
channelHint,
|
|
355
|
+
buckets,
|
|
356
|
+
sender,
|
|
357
|
+
rewriteQuery,
|
|
358
|
+
rerankContext,
|
|
359
|
+
logger
|
|
360
|
+
}) {
|
|
361
|
+
const lastUserMsg = lastUserMessage(messages);
|
|
362
|
+
if (!lastUserMsg) {
|
|
363
|
+
throw new Error("no user message found in conversation");
|
|
364
|
+
}
|
|
365
|
+
const { topK, label } = MODE_SETTINGS[mode];
|
|
366
|
+
let query = lastUserMsg.content;
|
|
367
|
+
if (rewriteQuery) {
|
|
368
|
+
try {
|
|
369
|
+
const rewritten = await rewriteQuery({ query, mode, sender });
|
|
370
|
+
if (typeof rewritten === "string" && rewritten.trim()) query = rewritten;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
logger?.error("rewriteQuery threw; falling back to the original query", error);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
|
|
376
|
+
if (rerankContext) {
|
|
377
|
+
try {
|
|
378
|
+
const reranked = await rerankContext({ query, chunks: ctx });
|
|
379
|
+
if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
|
|
380
|
+
} catch (error) {
|
|
381
|
+
logger?.error("rerankContext threw; falling back to the original chunks", error);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
|
|
385
|
+
const hint = channelHint?.trim();
|
|
386
|
+
const system = [
|
|
387
|
+
prompts.baseSystemRules,
|
|
388
|
+
persona,
|
|
389
|
+
...hint ? [hint] : [],
|
|
390
|
+
`${label}:
|
|
391
|
+
${ctx.join("\n\n")}`
|
|
392
|
+
].join("\n\n");
|
|
393
|
+
return { system, messages, context: ctx };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// src/history/compaction.ts
|
|
397
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
398
|
+
var LOAD_ALL = Number.MAX_SAFE_INTEGER;
|
|
399
|
+
var SUMMARY_PREFIX = "Summary of earlier conversation: ";
|
|
400
|
+
function defaultSummarize(client, model) {
|
|
401
|
+
return async ({ turns }) => {
|
|
402
|
+
const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
|
|
403
|
+
const { content } = await answerOnce({
|
|
404
|
+
client,
|
|
405
|
+
system: "Summarize the conversation turns below into a short paragraph a reader would need to continue the conversation. Preserve names, facts, and decisions; drop pleasantries. Reply with the summary only, no preamble.",
|
|
406
|
+
messages: [{ role: "user", content: transcript }],
|
|
407
|
+
// No `answerFn`: compaction is an internal chatter operation, not a
|
|
408
|
+
// chat surface, so it must not conscript a caller's brain hook, which
|
|
409
|
+
// is shaped to answer the user, not to summarize on the engine's behalf.
|
|
410
|
+
mode: "public",
|
|
411
|
+
model
|
|
412
|
+
});
|
|
413
|
+
return content;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function createHistoryCompactor(deps, options) {
|
|
417
|
+
const { threshold, keep, logger } = options;
|
|
418
|
+
if (!Number.isInteger(threshold) || threshold < 1) {
|
|
419
|
+
throw new Error(`Invalid history compaction threshold: ${threshold}`);
|
|
420
|
+
}
|
|
421
|
+
if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
|
|
422
|
+
throw new Error(`Invalid history compaction keep: ${keep}`);
|
|
423
|
+
}
|
|
424
|
+
const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
|
|
425
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
426
|
+
return {
|
|
427
|
+
async maybeCompact(store, conversationId) {
|
|
428
|
+
const turns = await store.load(conversationId, LOAD_ALL);
|
|
429
|
+
if (turns.length < threshold) return;
|
|
430
|
+
const older = turns.slice(0, turns.length - keep);
|
|
431
|
+
let summary;
|
|
432
|
+
let timer;
|
|
433
|
+
try {
|
|
434
|
+
summary = await Promise.race([
|
|
435
|
+
summarize({ conversationId, turns: older }),
|
|
436
|
+
new Promise((_, reject) => {
|
|
437
|
+
timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
|
|
438
|
+
})
|
|
439
|
+
]);
|
|
440
|
+
} catch (error) {
|
|
441
|
+
logger?.error(
|
|
442
|
+
`history compaction summarize failed for "${conversationId}"; history left untouched`,
|
|
443
|
+
error
|
|
444
|
+
);
|
|
445
|
+
return;
|
|
446
|
+
} finally {
|
|
447
|
+
clearTimeout(timer);
|
|
448
|
+
}
|
|
449
|
+
const trimmed = summary.trim();
|
|
450
|
+
if (!trimmed) return;
|
|
451
|
+
const fresh = await store.load(conversationId, LOAD_ALL);
|
|
452
|
+
const recent = fresh.slice(older.length);
|
|
453
|
+
await store.clear(conversationId);
|
|
454
|
+
await store.append(conversationId, {
|
|
455
|
+
role: "assistant",
|
|
456
|
+
content: `${SUMMARY_PREFIX}${trimmed}`
|
|
457
|
+
});
|
|
458
|
+
for (const turn of recent) {
|
|
459
|
+
await store.append(conversationId, turn);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/channels/gates.ts
|
|
466
|
+
function matches(re, text) {
|
|
467
|
+
return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
|
|
468
|
+
}
|
|
469
|
+
function isBlockedByAllowlist(msg, config) {
|
|
470
|
+
return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
|
|
471
|
+
}
|
|
472
|
+
function decideChannelAction(msg, config) {
|
|
473
|
+
if (msg.fromBot || !msg.text.trim()) return "ignore";
|
|
474
|
+
if (!msg.isDirectMessage) {
|
|
475
|
+
if (isBlockedByAllowlist(msg, config)) return "ignore";
|
|
476
|
+
if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
|
|
477
|
+
if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
|
|
478
|
+
if (config.mutedChats.has(msg.chatId)) return "ignore";
|
|
479
|
+
return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
|
|
480
|
+
}
|
|
481
|
+
return "reply";
|
|
482
|
+
}
|
|
483
|
+
function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
|
|
484
|
+
const log = /* @__PURE__ */ new Map();
|
|
485
|
+
let lastSweep = now();
|
|
486
|
+
function sweep(current) {
|
|
487
|
+
if (current - lastSweep < windowMs) return;
|
|
488
|
+
lastSweep = current;
|
|
489
|
+
const cutoff = current - windowMs;
|
|
490
|
+
for (const [key, entries] of log) {
|
|
491
|
+
if (!entries.some((t) => t > cutoff)) log.delete(key);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
function allow(key) {
|
|
495
|
+
const current = now();
|
|
496
|
+
sweep(current);
|
|
497
|
+
const cutoff = current - windowMs;
|
|
498
|
+
const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
|
|
499
|
+
if (entries.length >= maxPerWindow) {
|
|
500
|
+
if (entries.length > 0) log.set(key, entries);
|
|
501
|
+
else log.delete(key);
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
entries.push(current);
|
|
505
|
+
log.set(key, entries);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
allow.size = () => log.size;
|
|
509
|
+
return allow;
|
|
510
|
+
}
|
|
511
|
+
function underReplyRateLimit(msg, limiters) {
|
|
512
|
+
return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/channels/pipeline.ts
|
|
516
|
+
var DEFAULT_HISTORY_LIMIT = 20;
|
|
517
|
+
var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
|
|
518
|
+
var DEFAULT_DM_RATE_LIMIT = { max: 20, windowMs: 60 * 60 * 1e3 };
|
|
519
|
+
async function resolvePersonaLayer(config, ctx) {
|
|
520
|
+
if (!config.personaResolver) return void 0;
|
|
521
|
+
try {
|
|
522
|
+
return await config.personaResolver(ctx);
|
|
523
|
+
} catch {
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async function isHistoryEnabled(config, sender) {
|
|
528
|
+
const predicate = config.history?.historyEnabledFor;
|
|
529
|
+
if (!predicate) return true;
|
|
530
|
+
try {
|
|
531
|
+
return await predicate(sender);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
config.logger?.error("historyEnabledFor threw; disabling history for this turn", error);
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function createInboundPipeline(deps, config) {
|
|
538
|
+
const mutedChats = /* @__PURE__ */ new Set();
|
|
539
|
+
const now = config.now ?? Date.now;
|
|
540
|
+
const group = config.groupRateLimit ?? DEFAULT_GROUP_RATE_LIMIT;
|
|
541
|
+
const dm = config.dmRateLimit ?? DEFAULT_DM_RATE_LIMIT;
|
|
542
|
+
const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
|
|
543
|
+
const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
|
|
544
|
+
const mode = "public";
|
|
545
|
+
const compactor = config.history?.compaction ? createHistoryCompactor(
|
|
546
|
+
{ client: deps.client },
|
|
547
|
+
{ model: config.model, ...config.history.compaction }
|
|
548
|
+
) : void 0;
|
|
549
|
+
return async function handleInboundMessage(msg, turn) {
|
|
550
|
+
const allowedChats = config.allowedChats ?? [];
|
|
551
|
+
const action = decideChannelAction(msg, {
|
|
552
|
+
allowedChats,
|
|
553
|
+
mutedChats,
|
|
554
|
+
muteRegex: config.muteRegex,
|
|
555
|
+
unmuteRegex: config.unmuteRegex
|
|
556
|
+
});
|
|
557
|
+
if (action === "mute") {
|
|
558
|
+
mutedChats.add(msg.chatId);
|
|
559
|
+
if (config.muteReply) await turn.reply.sendGateReply(msg.chatId, config.muteReply);
|
|
560
|
+
return { action: "mute" };
|
|
561
|
+
}
|
|
562
|
+
if (action === "unmute") {
|
|
563
|
+
mutedChats.delete(msg.chatId);
|
|
564
|
+
if (config.unmuteReply) await turn.reply.sendGateReply(msg.chatId, config.unmuteReply);
|
|
565
|
+
return { action: "unmute" };
|
|
566
|
+
}
|
|
567
|
+
if (action === "ignore") {
|
|
568
|
+
return { action: "ignore" };
|
|
569
|
+
}
|
|
570
|
+
if (!underReplyRateLimit(msg, { dm: underDmRateLimit, group: underGroupRateLimit })) {
|
|
571
|
+
return { action: "rate-limited" };
|
|
572
|
+
}
|
|
573
|
+
const sender = typeof turn.sender === "function" ? await turn.sender() : turn.sender ?? msg.senderId;
|
|
574
|
+
const conversationId = turn.conversationId ?? msg.chatId;
|
|
575
|
+
if (turn.intercept && await turn.intercept(sender)) {
|
|
576
|
+
return { action: "intercepted" };
|
|
577
|
+
}
|
|
578
|
+
const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
|
|
579
|
+
const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
|
|
580
|
+
const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
|
|
581
|
+
const priorTurns = config.history && historyEnabled ? await config.history.store.load(
|
|
582
|
+
conversationId,
|
|
583
|
+
config.history.limit ?? DEFAULT_HISTORY_LIMIT
|
|
584
|
+
) : [];
|
|
585
|
+
const messages = [...priorTurns, { role: "user", content: msg.text }];
|
|
586
|
+
if (historyEnabled) {
|
|
587
|
+
await config.history?.store.append(conversationId, { role: "user", content: msg.text });
|
|
588
|
+
}
|
|
589
|
+
const { system } = await prepareChat({
|
|
590
|
+
store: deps.store,
|
|
591
|
+
prompts: deps.prompts,
|
|
592
|
+
mode,
|
|
593
|
+
messages,
|
|
594
|
+
personaLayer,
|
|
595
|
+
channelHint: config.channelHint,
|
|
596
|
+
buckets,
|
|
597
|
+
sender,
|
|
598
|
+
rewriteQuery: config.rewriteQuery,
|
|
599
|
+
rerankContext: config.rerankContext,
|
|
600
|
+
logger: config.logger
|
|
601
|
+
});
|
|
602
|
+
const { content: produced } = await answerOnce({
|
|
603
|
+
answerFn: config.answerFn,
|
|
604
|
+
client: deps.client,
|
|
605
|
+
system,
|
|
606
|
+
messages,
|
|
607
|
+
mode,
|
|
608
|
+
sender,
|
|
609
|
+
conversationId,
|
|
610
|
+
model: config.model
|
|
611
|
+
});
|
|
612
|
+
const content = await applyTransformReply(
|
|
613
|
+
config.transformReply,
|
|
614
|
+
{ channel: config.channel ?? "channel", sender, conversationId, text: produced },
|
|
615
|
+
config.logger
|
|
616
|
+
);
|
|
617
|
+
if (!content) {
|
|
618
|
+
return { action: "ignore" };
|
|
619
|
+
}
|
|
620
|
+
if (historyEnabled) {
|
|
621
|
+
await config.history?.store.append(conversationId, { role: "assistant", content });
|
|
622
|
+
}
|
|
623
|
+
await turn.reply.sendAnswer(msg.chatId, content);
|
|
624
|
+
const history = config.history;
|
|
625
|
+
if (compactor && historyEnabled && history) {
|
|
626
|
+
try {
|
|
627
|
+
await compactor.maybeCompact(history.store, conversationId);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
config.logger?.error(`history compaction failed for "${conversationId}"`, error);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return { action: "reply", content };
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/channels/matrix/updates.ts
|
|
637
|
+
function messageText(content) {
|
|
638
|
+
return content.body ?? "";
|
|
639
|
+
}
|
|
640
|
+
var PILL_HREF_PATTERN = /href=["']([^"']+)["']/g;
|
|
641
|
+
function pillUserId(href) {
|
|
642
|
+
const fragment = href.split("https://matrix.to/#/")[1];
|
|
643
|
+
if (!fragment) return void 0;
|
|
644
|
+
try {
|
|
645
|
+
return decodeURIComponent(fragment.split("?")[0] ?? "");
|
|
646
|
+
} catch {
|
|
647
|
+
return void 0;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function mentionsBot(content, me) {
|
|
651
|
+
if (content["m.mentions"]?.user_ids?.includes(me.userId)) return true;
|
|
652
|
+
const formatted = content.formatted_body;
|
|
653
|
+
if (!formatted) return false;
|
|
654
|
+
for (const match of formatted.matchAll(PILL_HREF_PATTERN)) {
|
|
655
|
+
if (pillUserId(match[1] ?? "") === me.userId) return true;
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
function isReplyToBot(content, sentEventIds) {
|
|
660
|
+
const replyToId = content["m.relates_to"]?.["m.in_reply_to"]?.event_id;
|
|
661
|
+
return replyToId ? sentEventIds.has(replyToId) : false;
|
|
662
|
+
}
|
|
663
|
+
var MAX_TRACKED_SENT_EVENTS = 500;
|
|
664
|
+
function recordSentEventId(sentEventIds, eventId) {
|
|
665
|
+
sentEventIds.add(eventId);
|
|
666
|
+
if (sentEventIds.size > MAX_TRACKED_SENT_EVENTS) {
|
|
667
|
+
const oldest = sentEventIds.values().next().value;
|
|
668
|
+
if (oldest !== void 0) sentEventIds.delete(oldest);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function directRoomIds(accountDataEvents) {
|
|
672
|
+
const direct = accountDataEvents.find((event) => event.type === "m.direct");
|
|
673
|
+
const mapping = direct?.content ?? {};
|
|
674
|
+
const rooms = /* @__PURE__ */ new Set();
|
|
675
|
+
for (const value of Object.values(mapping)) {
|
|
676
|
+
if (Array.isArray(value)) {
|
|
677
|
+
for (const roomId of value) {
|
|
678
|
+
if (typeof roomId === "string") rooms.add(roomId);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return rooms;
|
|
683
|
+
}
|
|
684
|
+
function toChannelMessage(roomId, event, me, directRooms, sentEventIds) {
|
|
685
|
+
if (event.type !== "m.room.message") return void 0;
|
|
686
|
+
const content = event.content;
|
|
687
|
+
if (!content.msgtype) return void 0;
|
|
688
|
+
return {
|
|
689
|
+
chatId: roomId,
|
|
690
|
+
senderId: event.sender,
|
|
691
|
+
text: messageText(content),
|
|
692
|
+
isDirectMessage: directRooms.has(roomId),
|
|
693
|
+
mentionsBot: mentionsBot(content, me),
|
|
694
|
+
isReplyToBot: isReplyToBot(content, sentEventIds),
|
|
695
|
+
fromBot: event.sender === me.userId,
|
|
696
|
+
messageRef: event.event_id
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
function matrixSenderKey(userId) {
|
|
700
|
+
return `mx:${userId}`;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// src/channels/matrix/handler.ts
|
|
704
|
+
async function sendText(api, roomId, text, sentEventIds, replyToEventId) {
|
|
705
|
+
const { eventId } = await api.sendEvent(roomId, {
|
|
706
|
+
msgtype: "m.text",
|
|
707
|
+
body: text,
|
|
708
|
+
...replyToEventId ? { "m.relates_to": { "m.in_reply_to": { event_id: replyToEventId } } } : {}
|
|
709
|
+
});
|
|
710
|
+
recordSentEventId(sentEventIds, eventId);
|
|
711
|
+
}
|
|
712
|
+
function createMatrixSyncHandler(deps) {
|
|
713
|
+
const { api, me, pipeline, allowedChats, sentEventIds, logger, label } = deps;
|
|
714
|
+
const autoJoin = deps.autoJoin ?? true;
|
|
715
|
+
const loggedUnallowedChats = /* @__PURE__ */ new Set();
|
|
716
|
+
let directRooms = new Set(deps.initialDirectRooms ?? []);
|
|
717
|
+
return async function handleSync(response) {
|
|
718
|
+
if (response.account_data?.events?.some((event) => event.type === "m.direct")) {
|
|
719
|
+
directRooms = directRoomIds(response.account_data.events);
|
|
720
|
+
}
|
|
721
|
+
if (autoJoin) {
|
|
722
|
+
for (const roomId of Object.keys(response.rooms?.invite ?? {})) {
|
|
723
|
+
try {
|
|
724
|
+
await api.joinRoom(roomId);
|
|
725
|
+
logger.info(`${label}: joined ${roomId} (accepted invite)`);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
logger.warn(`${label}: failed to join ${roomId}:`, error);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
for (const [roomId, room] of Object.entries(response.rooms?.join ?? {})) {
|
|
732
|
+
for (const event of room.timeline.events) {
|
|
733
|
+
const msg = toChannelMessage(roomId, event, me, directRooms, sentEventIds);
|
|
734
|
+
if (!msg) continue;
|
|
735
|
+
if (isBlockedByAllowlist(msg, { allowedChats })) {
|
|
736
|
+
if (!loggedUnallowedChats.has(msg.chatId)) {
|
|
737
|
+
loggedUnallowedChats.add(msg.chatId);
|
|
738
|
+
logger.warn(`${label}: skipped room ${msg.chatId} - not in allowedChats`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const reply = {
|
|
742
|
+
// Threaded onto the incoming event: in a busy room an untethered
|
|
743
|
+
// answer reads as a non-sequitur.
|
|
744
|
+
sendAnswer: (chatId, text) => sendText(api, chatId, text, sentEventIds, event.event_id),
|
|
745
|
+
sendGateReply: (chatId, text) => sendText(api, chatId, text, sentEventIds)
|
|
746
|
+
};
|
|
747
|
+
try {
|
|
748
|
+
await pipeline(msg, {
|
|
749
|
+
reply,
|
|
750
|
+
conversationId: msg.chatId,
|
|
751
|
+
sender: matrixSenderKey(msg.senderId)
|
|
752
|
+
});
|
|
753
|
+
} catch (error) {
|
|
754
|
+
logger.warn(`${label}: message ${event.event_id} in ${roomId} handling failed:`, error);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function createMatrixSender(api, sentEventIds) {
|
|
761
|
+
return {
|
|
762
|
+
sendText: (chatId, text) => sendText(api, chatId, text, sentEventIds),
|
|
763
|
+
sendMedia: async (chatId, payload) => {
|
|
764
|
+
const { eventId } = await api.sendMedia(chatId, payload);
|
|
765
|
+
recordSentEventId(sentEventIds, eventId);
|
|
766
|
+
},
|
|
767
|
+
sendReaction: (chatId, messageRef, emoji) => api.sendEvent(
|
|
768
|
+
chatId,
|
|
769
|
+
{
|
|
770
|
+
"m.relates_to": { rel_type: "m.annotation", event_id: String(messageRef), key: emoji }
|
|
771
|
+
},
|
|
772
|
+
{ eventType: "m.reaction" }
|
|
773
|
+
).then(() => void 0)
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/channels/matrix/sync.ts
|
|
778
|
+
var BASE_DELAY_MS = 1e3;
|
|
779
|
+
var MAX_DELAY_MS = 6e4;
|
|
780
|
+
function syncBackoffMs(failures) {
|
|
781
|
+
return Math.min(BASE_DELAY_MS * 2 ** Math.min(failures, 20), MAX_DELAY_MS);
|
|
782
|
+
}
|
|
783
|
+
function retryDelayMs(error, failures) {
|
|
784
|
+
const retryAfterMs = error instanceof MatrixApiError ? error.retryAfterMs : void 0;
|
|
785
|
+
return retryAfterMs ?? syncBackoffMs(failures);
|
|
786
|
+
}
|
|
787
|
+
async function runMatrixSyncLoop(deps) {
|
|
788
|
+
const logger = deps.logger ?? createConsoleLogger();
|
|
789
|
+
const label = deps.label ?? "Matrix";
|
|
790
|
+
let since = deps.initialSince;
|
|
791
|
+
let failures = 0;
|
|
792
|
+
while (!deps.isStopped()) {
|
|
793
|
+
let response;
|
|
794
|
+
try {
|
|
795
|
+
response = await deps.sync(since);
|
|
796
|
+
} catch (error) {
|
|
797
|
+
if (deps.isStopped()) break;
|
|
798
|
+
failures += 1;
|
|
799
|
+
const delay = retryDelayMs(error, failures);
|
|
800
|
+
logger.warn(`${label}: sync failed, retrying in ${Math.round(delay / 1e3)}s:`, error);
|
|
801
|
+
await deps.sleep(delay);
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
failures = 0;
|
|
805
|
+
since = response.next_batch;
|
|
806
|
+
deps.onSince?.(since);
|
|
807
|
+
if (deps.isStopped()) break;
|
|
808
|
+
try {
|
|
809
|
+
await deps.handleSync(response);
|
|
810
|
+
} catch (error) {
|
|
811
|
+
logger.warn(`${label}: batch ending at ${since} handling failed:`, error);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
return since;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/channels/matrix/channel.ts
|
|
818
|
+
var DEFAULT_SYNC_TIMEOUT_MS = 3e4;
|
|
819
|
+
function defaultSleep(ms) {
|
|
820
|
+
return new Promise((resolve) => {
|
|
821
|
+
const timer = setTimeout(resolve, ms);
|
|
822
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
function createMatrixChannel(config) {
|
|
826
|
+
const channelName = config.name ?? "matrix";
|
|
827
|
+
const allowedChats = config.allowedChats ?? [];
|
|
828
|
+
const sleep = config.sleep ?? defaultSleep;
|
|
829
|
+
const syncTimeoutMs = config.syncTimeoutMs ?? DEFAULT_SYNC_TIMEOUT_MS;
|
|
830
|
+
let stopped = false;
|
|
831
|
+
let registered = false;
|
|
832
|
+
let senders;
|
|
833
|
+
const sentEventIds = /* @__PURE__ */ new Set();
|
|
834
|
+
return {
|
|
835
|
+
name: channelName,
|
|
836
|
+
async start(deps) {
|
|
837
|
+
const log = config.logger ?? deps.logger ?? createConsoleLogger();
|
|
838
|
+
const api = config.api ?? createMatrixApi({
|
|
839
|
+
homeserverUrl: config.homeserverUrl,
|
|
840
|
+
accessToken: config.accessToken,
|
|
841
|
+
fetch: config.fetch
|
|
842
|
+
});
|
|
843
|
+
const me = await api.whoami();
|
|
844
|
+
const label = `Matrix[${me.userId}]`;
|
|
845
|
+
let initialDirectRooms;
|
|
846
|
+
try {
|
|
847
|
+
const direct = await api.getAccountData(me.userId, "m.direct");
|
|
848
|
+
if (direct) initialDirectRooms = directRoomIds([{ type: "m.direct", content: direct }]);
|
|
849
|
+
} catch (error) {
|
|
850
|
+
log.warn(`${label}: could not load m.direct account data at start:`, error);
|
|
851
|
+
}
|
|
852
|
+
deps.senders.register(channelName, createMatrixSender(api, sentEventIds));
|
|
853
|
+
senders = deps.senders;
|
|
854
|
+
registered = true;
|
|
855
|
+
const pipeline = createInboundPipeline(
|
|
856
|
+
{ client: deps.client, store: deps.store, prompts: deps.prompts },
|
|
857
|
+
{
|
|
858
|
+
channel: channelName,
|
|
859
|
+
answerFn: config.answerFn ?? deps.config.answerFn,
|
|
860
|
+
bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
|
|
861
|
+
rewriteQuery: config.rewriteQuery ?? deps.config.rewriteQuery,
|
|
862
|
+
rerankContext: config.rerankContext ?? deps.config.rerankContext,
|
|
863
|
+
transformReply: config.transformReply ?? deps.config.transformReply,
|
|
864
|
+
model: config.model,
|
|
865
|
+
channelHint: config.channelHint ?? "Channel: Matrix.",
|
|
866
|
+
personaResolver: config.personaResolver,
|
|
867
|
+
history: config.history,
|
|
868
|
+
allowedChats,
|
|
869
|
+
muteRegex: config.muteRegex,
|
|
870
|
+
unmuteRegex: config.unmuteRegex,
|
|
871
|
+
muteReply: config.muteReply,
|
|
872
|
+
unmuteReply: config.unmuteReply,
|
|
873
|
+
dmRateLimit: config.dmRateLimit,
|
|
874
|
+
groupRateLimit: config.groupRateLimit,
|
|
875
|
+
now: config.now,
|
|
876
|
+
logger: log
|
|
877
|
+
}
|
|
878
|
+
);
|
|
879
|
+
const handleSync = createMatrixSyncHandler({
|
|
880
|
+
api,
|
|
881
|
+
me,
|
|
882
|
+
pipeline,
|
|
883
|
+
allowedChats,
|
|
884
|
+
sentEventIds,
|
|
885
|
+
autoJoin: config.autoJoin,
|
|
886
|
+
initialDirectRooms,
|
|
887
|
+
logger: log,
|
|
888
|
+
label
|
|
889
|
+
});
|
|
890
|
+
void runMatrixSyncLoop({
|
|
891
|
+
sync: (since) => api.sync({ since, timeoutMs: syncTimeoutMs }),
|
|
892
|
+
handleSync,
|
|
893
|
+
isStopped: () => stopped,
|
|
894
|
+
sleep,
|
|
895
|
+
initialSince: config.initialSince,
|
|
896
|
+
onSince: config.onSince,
|
|
897
|
+
logger: log,
|
|
898
|
+
label
|
|
899
|
+
}).catch((error) => log.error(`${label}: sync loop ended unexpectedly:`, error));
|
|
900
|
+
log.info(`${label}: syncing for events`);
|
|
901
|
+
},
|
|
902
|
+
stop() {
|
|
903
|
+
stopped = true;
|
|
904
|
+
if (registered) {
|
|
905
|
+
senders?.unregister(channelName);
|
|
906
|
+
registered = false;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
export {
|
|
912
|
+
MAX_TRACKED_SENT_EVENTS,
|
|
913
|
+
MatrixApiError,
|
|
914
|
+
createMatrixApi,
|
|
915
|
+
createMatrixChannel,
|
|
916
|
+
createMatrixSender,
|
|
917
|
+
createMatrixSyncHandler,
|
|
918
|
+
directRoomIds,
|
|
919
|
+
isReplyToBot,
|
|
920
|
+
matrixSenderKey,
|
|
921
|
+
mentionsBot,
|
|
922
|
+
messageText,
|
|
923
|
+
recordSentEventId,
|
|
924
|
+
redactToken,
|
|
925
|
+
retryDelayMs,
|
|
926
|
+
runMatrixSyncLoop,
|
|
927
|
+
syncBackoffMs,
|
|
928
|
+
toChannelMessage
|
|
929
|
+
};
|