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