@diegoaltoworks/chatter 0.58.0 → 0.60.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.
@@ -146,6 +146,17 @@ function createTelegramApi(config) {
146
146
  message_id: messageId,
147
147
  reaction: [{ type: "emoji", emoji }]
148
148
  });
149
+ },
150
+ async setWebhook(url, options) {
151
+ await call("setWebhook", {
152
+ url,
153
+ // Only what this channel interprets — see the matching comment on getUpdates.
154
+ allowed_updates: ["message"],
155
+ ...options?.secretToken ? { secret_token: options.secretToken } : {}
156
+ });
157
+ },
158
+ async deleteWebhook() {
159
+ await call("deleteWebhook");
149
160
  }
150
161
  };
151
162
  }
@@ -166,56 +177,6 @@ function createConsoleLogger(level = "info") {
166
177
  };
167
178
  }
168
179
 
169
- // src/channels/gates.ts
170
- function matches(re, text) {
171
- return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
172
- }
173
- function isBlockedByAllowlist(msg, config) {
174
- return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
175
- }
176
- function decideChannelAction(msg, config) {
177
- if (msg.fromBot || !msg.text.trim()) return "ignore";
178
- if (!msg.isDirectMessage) {
179
- if (isBlockedByAllowlist(msg, config)) return "ignore";
180
- if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
181
- if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
182
- if (config.mutedChats.has(msg.chatId)) return "ignore";
183
- return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
184
- }
185
- return "reply";
186
- }
187
- function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
188
- const log = /* @__PURE__ */ new Map();
189
- let lastSweep = now();
190
- function sweep(current) {
191
- if (current - lastSweep < windowMs) return;
192
- lastSweep = current;
193
- const cutoff = current - windowMs;
194
- for (const [key, entries] of log) {
195
- if (!entries.some((t) => t > cutoff)) log.delete(key);
196
- }
197
- }
198
- function allow(key) {
199
- const current = now();
200
- sweep(current);
201
- const cutoff = current - windowMs;
202
- const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
203
- if (entries.length >= maxPerWindow) {
204
- if (entries.length > 0) log.set(key, entries);
205
- else log.delete(key);
206
- return false;
207
- }
208
- entries.push(current);
209
- log.set(key, entries);
210
- return true;
211
- }
212
- allow.size = () => log.size;
213
- return allow;
214
- }
215
- function underReplyRateLimit(msg, limiters) {
216
- return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
217
- }
218
-
219
180
  // src/core/guardrails.ts
220
181
  function detectLeakage(text) {
221
182
  const flags = [
@@ -387,6 +348,125 @@ ${ctx.join("\n\n")}`
387
348
  return { system, messages, context: ctx };
388
349
  }
389
350
 
351
+ // src/history/compaction.ts
352
+ var DEFAULT_TIMEOUT_MS = 8e3;
353
+ var LOAD_ALL = Number.MAX_SAFE_INTEGER;
354
+ var SUMMARY_PREFIX = "Summary of earlier conversation: ";
355
+ function defaultSummarize(client, model) {
356
+ return async ({ turns }) => {
357
+ const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
358
+ const { content } = await answerOnce({
359
+ client,
360
+ 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.",
361
+ messages: [{ role: "user", content: transcript }],
362
+ // No `answerFn`: compaction is an internal chatter operation, not a
363
+ // chat surface, so it must not conscript a caller's brain hook, which
364
+ // is shaped to answer the user, not to summarize on the engine's behalf.
365
+ mode: "public",
366
+ model
367
+ });
368
+ return content;
369
+ };
370
+ }
371
+ function createHistoryCompactor(deps, options) {
372
+ const { threshold, keep, logger } = options;
373
+ if (!Number.isInteger(threshold) || threshold < 1) {
374
+ throw new Error(`Invalid history compaction threshold: ${threshold}`);
375
+ }
376
+ if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
377
+ throw new Error(`Invalid history compaction keep: ${keep}`);
378
+ }
379
+ const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
380
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
381
+ return {
382
+ async maybeCompact(store, conversationId) {
383
+ const turns = await store.load(conversationId, LOAD_ALL);
384
+ if (turns.length < threshold) return;
385
+ const older = turns.slice(0, turns.length - keep);
386
+ let summary;
387
+ let timer;
388
+ try {
389
+ summary = await Promise.race([
390
+ summarize({ conversationId, turns: older }),
391
+ new Promise((_, reject) => {
392
+ timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
393
+ })
394
+ ]);
395
+ } catch (error) {
396
+ logger?.error(
397
+ `history compaction summarize failed for "${conversationId}"; history left untouched`,
398
+ error
399
+ );
400
+ return;
401
+ } finally {
402
+ clearTimeout(timer);
403
+ }
404
+ const trimmed = summary.trim();
405
+ if (!trimmed) return;
406
+ const fresh = await store.load(conversationId, LOAD_ALL);
407
+ const recent = fresh.slice(older.length);
408
+ await store.clear(conversationId);
409
+ await store.append(conversationId, {
410
+ role: "assistant",
411
+ content: `${SUMMARY_PREFIX}${trimmed}`
412
+ });
413
+ for (const turn of recent) {
414
+ await store.append(conversationId, turn);
415
+ }
416
+ }
417
+ };
418
+ }
419
+
420
+ // src/channels/gates.ts
421
+ function matches(re, text) {
422
+ return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
423
+ }
424
+ function isBlockedByAllowlist(msg, config) {
425
+ return !msg.fromBot && msg.text.trim().length > 0 && !msg.isDirectMessage && config.allowedChats.length > 0 && !config.allowedChats.includes(msg.chatId);
426
+ }
427
+ function decideChannelAction(msg, config) {
428
+ if (msg.fromBot || !msg.text.trim()) return "ignore";
429
+ if (!msg.isDirectMessage) {
430
+ if (isBlockedByAllowlist(msg, config)) return "ignore";
431
+ if (config.muteRegex && matches(config.muteRegex, msg.text)) return "mute";
432
+ if (config.unmuteRegex && matches(config.unmuteRegex, msg.text)) return "unmute";
433
+ if (config.mutedChats.has(msg.chatId)) return "ignore";
434
+ return msg.mentionsBot || msg.isReplyToBot ? "reply" : "ignore";
435
+ }
436
+ return "reply";
437
+ }
438
+ function createSlidingWindowRateLimiter(maxPerWindow, windowMs, now = Date.now) {
439
+ const log = /* @__PURE__ */ new Map();
440
+ let lastSweep = now();
441
+ function sweep(current) {
442
+ if (current - lastSweep < windowMs) return;
443
+ lastSweep = current;
444
+ const cutoff = current - windowMs;
445
+ for (const [key, entries] of log) {
446
+ if (!entries.some((t) => t > cutoff)) log.delete(key);
447
+ }
448
+ }
449
+ function allow(key) {
450
+ const current = now();
451
+ sweep(current);
452
+ const cutoff = current - windowMs;
453
+ const entries = (log.get(key) ?? []).filter((t) => t > cutoff);
454
+ if (entries.length >= maxPerWindow) {
455
+ if (entries.length > 0) log.set(key, entries);
456
+ else log.delete(key);
457
+ return false;
458
+ }
459
+ entries.push(current);
460
+ log.set(key, entries);
461
+ return true;
462
+ }
463
+ allow.size = () => log.size;
464
+ return allow;
465
+ }
466
+ function underReplyRateLimit(msg, limiters) {
467
+ return msg.isDirectMessage ? limiters.dm(msg.chatId) : limiters.group(msg.chatId);
468
+ }
469
+
390
470
  // src/channels/pipeline.ts
391
471
  var DEFAULT_HISTORY_LIMIT = 20;
392
472
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
@@ -417,6 +497,10 @@ function createInboundPipeline(deps, config) {
417
497
  const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
418
498
  const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
419
499
  const mode = "public";
500
+ const compactor = config.history?.compaction ? createHistoryCompactor(
501
+ { client: deps.client },
502
+ { model: config.model, ...config.history.compaction }
503
+ ) : void 0;
420
504
  return async function handleInboundMessage(msg, turn) {
421
505
  const allowedChats = config.allowedChats ?? [];
422
506
  const action = decideChannelAction(msg, {
@@ -492,6 +576,14 @@ function createInboundPipeline(deps, config) {
492
576
  await config.history?.store.append(conversationId, { role: "assistant", content });
493
577
  }
494
578
  await turn.reply.sendAnswer(msg.chatId, content);
579
+ const history = config.history;
580
+ if (compactor && historyEnabled && history) {
581
+ try {
582
+ await compactor.maybeCompact(history.store, conversationId);
583
+ } catch (error) {
584
+ config.logger?.error(`history compaction failed for "${conversationId}"`, error);
585
+ }
586
+ }
495
587
  return { action: "reply", content };
496
588
  };
497
589
  }
@@ -539,6 +631,41 @@ function telegramSenderKey(senderId) {
539
631
  return `tg:${senderId}`;
540
632
  }
541
633
 
634
+ // src/channels/telegram/handler.ts
635
+ function createTelegramUpdateHandler(deps) {
636
+ const { api, me, pipeline, allowedChats, logger, label } = deps;
637
+ const loggedUnallowedChats = /* @__PURE__ */ new Set();
638
+ return async function handleUpdate(update) {
639
+ const message = update.message;
640
+ const msg = toChannelMessage(update, me);
641
+ if (!msg || !message) return;
642
+ if (isBlockedByAllowlist(msg, { allowedChats })) {
643
+ if (!loggedUnallowedChats.has(msg.chatId)) {
644
+ loggedUnallowedChats.add(msg.chatId);
645
+ logger.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
646
+ }
647
+ }
648
+ const reply = {
649
+ // Threaded onto the incoming message: in a busy group an untethered
650
+ // answer reads as a non-sequitur.
651
+ sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
652
+ sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
653
+ };
654
+ await pipeline(msg, {
655
+ reply,
656
+ conversationId: msg.chatId,
657
+ sender: telegramSenderKey(msg.senderId)
658
+ });
659
+ };
660
+ }
661
+ function createTelegramSender(api) {
662
+ return {
663
+ sendText: (chatId, text) => api.sendMessage(chatId, text),
664
+ sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
665
+ sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
666
+ };
667
+ }
668
+
542
669
  // src/channels/telegram/poll.ts
543
670
  var BASE_DELAY_MS = 1e3;
544
671
  var MAX_DELAY_MS = 6e4;
@@ -609,12 +736,7 @@ function createTelegramChannel(config) {
609
736
  });
610
737
  const me = await api.getMe();
611
738
  const label = `Telegram[${me.username ?? me.id}]`;
612
- const sender = {
613
- sendText: (chatId, text) => api.sendMessage(chatId, text),
614
- sendMedia: (chatId, payload) => api.sendMedia(chatId, payload),
615
- sendReaction: (chatId, messageRef, emoji) => api.setMessageReaction(chatId, Number(messageRef), emoji)
616
- };
617
- deps.senders.register(channelName, sender);
739
+ deps.senders.register(channelName, createTelegramSender(api));
618
740
  senders = deps.senders;
619
741
  registered = true;
620
742
  const pipeline = createInboundPipeline(
@@ -641,29 +763,14 @@ function createTelegramChannel(config) {
641
763
  logger: log
642
764
  }
643
765
  );
644
- const loggedUnallowedChats = /* @__PURE__ */ new Set();
645
- async function handleUpdate(update) {
646
- const message = update.message;
647
- const msg = toChannelMessage(update, me);
648
- if (!msg || !message) return;
649
- if (isBlockedByAllowlist(msg, { allowedChats })) {
650
- if (!loggedUnallowedChats.has(msg.chatId)) {
651
- loggedUnallowedChats.add(msg.chatId);
652
- log.warn(`${label}: skipped chat ${msg.chatId} - not in allowedChats`);
653
- }
654
- }
655
- const reply = {
656
- // Threaded onto the incoming message: in a busy group an untethered
657
- // answer reads as a non-sequitur.
658
- sendAnswer: (chatId, text) => api.sendMessage(chatId, text, { replyToMessageId: message.message_id }),
659
- sendGateReply: (chatId, text) => api.sendMessage(chatId, text)
660
- };
661
- await pipeline(msg, {
662
- reply,
663
- conversationId: msg.chatId,
664
- sender: telegramSenderKey(msg.senderId)
665
- });
666
- }
766
+ const handleUpdate = createTelegramUpdateHandler({
767
+ api,
768
+ me,
769
+ pipeline,
770
+ allowedChats,
771
+ logger: log,
772
+ label
773
+ });
667
774
  abort = new AbortController();
668
775
  const signal = abort.signal;
669
776
  void runLongPoll({
@@ -688,12 +795,205 @@ function createTelegramChannel(config) {
688
795
  }
689
796
  };
690
797
  }
798
+
799
+ // src/channels/telegram/webhook.ts
800
+ import { timingSafeEqual } from "node:crypto";
801
+
802
+ // node_modules/hono/dist/http-exception.js
803
+ var HTTPException = class extends Error {
804
+ res;
805
+ status;
806
+ /**
807
+ * Creates an instance of `HTTPException`.
808
+ * @param status - HTTP status code for the exception. Defaults to 500.
809
+ * @param options - Additional options for the exception.
810
+ */
811
+ constructor(status = 500, options) {
812
+ super(options?.message, { cause: options?.cause });
813
+ this.res = options?.res;
814
+ this.status = status;
815
+ }
816
+ /**
817
+ * Returns the response object associated with the exception.
818
+ * If a response object is not provided, a new response is created with the error message and status code.
819
+ * @returns The response object.
820
+ */
821
+ getResponse() {
822
+ if (this.res) {
823
+ const newResponse = new Response(this.res.body, {
824
+ status: this.status,
825
+ headers: this.res.headers
826
+ });
827
+ return newResponse;
828
+ }
829
+ return new Response(this.message, {
830
+ status: this.status
831
+ });
832
+ }
833
+ };
834
+
835
+ // node_modules/hono/dist/middleware/body-limit/index.js
836
+ var ERROR_MESSAGE = "Payload Too Large";
837
+ var bodyLimit = (options) => {
838
+ const onError = options.onError || (() => {
839
+ const res = new Response(ERROR_MESSAGE, {
840
+ status: 413
841
+ });
842
+ throw new HTTPException(413, { res });
843
+ });
844
+ const maxSize = options.maxSize;
845
+ return async function bodyLimit2(c, next) {
846
+ if (!c.req.raw.body) {
847
+ return next();
848
+ }
849
+ const hasTransferEncoding = c.req.raw.headers.has("transfer-encoding");
850
+ const hasContentLength = c.req.raw.headers.has("content-length");
851
+ if (hasContentLength && !hasTransferEncoding) {
852
+ const contentLength = parseInt(c.req.raw.headers.get("content-length") || "0", 10);
853
+ return contentLength > maxSize ? onError(c) : next();
854
+ }
855
+ let size = 0;
856
+ const chunks = [];
857
+ const rawReader = c.req.raw.body.getReader();
858
+ for (; ; ) {
859
+ const { done, value } = await rawReader.read();
860
+ if (done) {
861
+ break;
862
+ }
863
+ size += value.length;
864
+ if (size > maxSize) {
865
+ return onError(c);
866
+ }
867
+ chunks.push(value);
868
+ }
869
+ const requestInit = {
870
+ body: new ReadableStream({
871
+ start(controller) {
872
+ for (const chunk of chunks) {
873
+ controller.enqueue(chunk);
874
+ }
875
+ controller.close();
876
+ }
877
+ }),
878
+ duplex: "half"
879
+ };
880
+ c.req.raw = new Request(c.req.raw, requestInit);
881
+ return next();
882
+ };
883
+ };
884
+
885
+ // src/middleware/bodyLimit.ts
886
+ var DEFAULT_MAX_REQUEST_BYTES = 262144;
887
+ function chatBodyLimit(maxSize = DEFAULT_MAX_REQUEST_BYTES, toError = (message) => ({ error: message })) {
888
+ return bodyLimit({
889
+ maxSize,
890
+ onError: (c) => c.json(toError("Request body too large"), 413)
891
+ });
892
+ }
893
+
894
+ // src/channels/telegram/webhook.ts
895
+ var SECRET_TOKEN_HEADER = "x-telegram-bot-api-secret-token";
896
+ function secureCompare(a, b) {
897
+ const bufA = Buffer.from(a);
898
+ const bufB = Buffer.from(b);
899
+ return bufA.length === bufB.length && timingSafeEqual(bufA, bufB);
900
+ }
901
+ var SECRET_TOKEN_PATTERN = /^[A-Za-z0-9_-]{1,256}$/;
902
+ function createTelegramWebhookRoute(config) {
903
+ const webhookSecret = config.webhookSecret;
904
+ if (!webhookSecret?.trim()) {
905
+ throw new Error("Telegram webhook requires webhookSecret (it fails closed without one)");
906
+ }
907
+ if (!SECRET_TOKEN_PATTERN.test(webhookSecret)) {
908
+ throw new Error(
909
+ "Telegram webhookSecret must match /^[A-Za-z0-9_-]{1,256}$/ (Telegram's own secret_token charset)"
910
+ );
911
+ }
912
+ const channelName = config.name ?? "telegram";
913
+ const allowedChats = config.allowedChats ?? [];
914
+ const path = config.path ?? "/webhooks/telegram";
915
+ return async (app, deps) => {
916
+ const log = config.logger ?? deps.logger ?? createConsoleLogger();
917
+ const api = config.api ?? createTelegramApi({
918
+ botToken: config.botToken,
919
+ fetch: config.fetch,
920
+ baseUrl: config.apiBaseUrl
921
+ });
922
+ const me = await api.getMe();
923
+ const label = `Telegram[${me.username ?? me.id}] (webhook)`;
924
+ deps.senders.register(channelName, createTelegramSender(api));
925
+ const pipeline = createInboundPipeline(
926
+ { client: deps.client, store: deps.store, prompts: deps.prompts },
927
+ {
928
+ channel: channelName,
929
+ answerFn: config.answerFn ?? deps.config.answerFn,
930
+ bucketsFor: config.bucketsFor ?? deps.config.bucketsFor,
931
+ rewriteQuery: config.rewriteQuery ?? deps.config.rewriteQuery,
932
+ rerankContext: config.rerankContext ?? deps.config.rerankContext,
933
+ transformReply: config.transformReply ?? deps.config.transformReply,
934
+ model: config.model,
935
+ channelHint: config.channelHint ?? "Channel: Telegram.",
936
+ personaResolver: config.personaResolver,
937
+ history: config.history,
938
+ allowedChats,
939
+ muteRegex: config.muteRegex,
940
+ unmuteRegex: config.unmuteRegex,
941
+ muteReply: config.muteReply,
942
+ unmuteReply: config.unmuteReply,
943
+ dmRateLimit: config.dmRateLimit,
944
+ groupRateLimit: config.groupRateLimit,
945
+ now: config.now,
946
+ logger: log
947
+ }
948
+ );
949
+ const handleUpdate = createTelegramUpdateHandler({
950
+ api,
951
+ me,
952
+ pipeline,
953
+ allowedChats,
954
+ logger: log,
955
+ label
956
+ });
957
+ app.post(
958
+ path,
959
+ chatBodyLimit(deps.config.server?.maxRequestBytes),
960
+ // Reject oversized bodies first
961
+ async (c) => {
962
+ const provided = c.req.header(SECRET_TOKEN_HEADER);
963
+ if (!provided || !secureCompare(provided, webhookSecret)) {
964
+ log.warn(`${label}: rejected webhook POST - missing or invalid secret token`);
965
+ return c.text("", 403);
966
+ }
967
+ let body;
968
+ try {
969
+ body = await c.req.json();
970
+ } catch {
971
+ return c.text("", 400);
972
+ }
973
+ if (typeof body !== "object" || body === null) {
974
+ return c.text("", 400);
975
+ }
976
+ const update = body;
977
+ try {
978
+ await handleUpdate(update);
979
+ } catch (error) {
980
+ log.warn(`${label}: update handling failed:`, error);
981
+ }
982
+ return c.text("", 200);
983
+ }
984
+ );
985
+ log.info(`${label}: webhook mounted at ${path} as @${me.username ?? me.id}`);
986
+ };
987
+ }
691
988
  export {
692
989
  TELEGRAM_API_BASE,
693
990
  TELEGRAM_TEXT_LIMIT,
694
991
  TelegramApiError,
695
992
  createTelegramApi,
696
993
  createTelegramChannel,
994
+ createTelegramSender,
995
+ createTelegramUpdateHandler,
996
+ createTelegramWebhookRoute,
697
997
  mentionsBot,
698
998
  messageEntities,
699
999
  messageText,
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Telegram Bot API transport: a `CustomRoutes` mount that receives updates
3
+ * over HTTPS instead of long-polling `getUpdates` (see `./channel` for that
4
+ * transport). Update handling is identical between the two — both build on
5
+ * `./handler`'s shared `createTelegramUpdateHandler`/`createTelegramSender` —
6
+ * so choosing one over the other is purely an infrastructure decision: a
7
+ * webhook needs a publicly reachable HTTPS endpoint and no open connection,
8
+ * long-polling needs neither but holds one request open at a time. See
9
+ * docs/telegram.md for the full comparison and `TelegramApi.setWebhook` for
10
+ * registering the URL with Telegram once this route is mounted.
11
+ *
12
+ * ```ts
13
+ * await createServer({
14
+ * ...,
15
+ * customRoutes: createTelegramWebhookRoute({
16
+ * botToken: process.env.TELEGRAM_BOT_TOKEN as string,
17
+ * webhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET as string,
18
+ * }),
19
+ * });
20
+ * ```
21
+ */
22
+ import type { AnswerFn, TransformReply } from "../../core/answer";
23
+ import type { BucketsFor } from "../../core/buckets";
24
+ import { type Logger } from "../../core/logger";
25
+ import type { RerankContext, RewriteQuery } from "../../core/pipeline";
26
+ import type { HistoryCompactionOptions } from "../../history/compaction";
27
+ import type { HistoryStore } from "../../history/types";
28
+ import type { CustomRoutes } from "../../types";
29
+ import { type TelegramApi } from "./api";
30
+ export interface TelegramWebhookConfig {
31
+ /** From @BotFather. A credential — pass it from the environment, never commit it. */
32
+ botToken: string;
33
+ /**
34
+ * Shared secret Telegram is told about via `setWebhook({ secretToken })`
35
+ * and then echoes back on every POST as `X-Telegram-Bot-Api-Secret-Token`.
36
+ * Required: without one there is no way to tell a genuine Telegram POST
37
+ * from a forged one, so this fails closed by construction — a missing or
38
+ * blank value throws rather than falling back to accepting every request.
39
+ */
40
+ webhookSecret: string;
41
+ /** Where the route is mounted. @default "/webhooks/telegram" */
42
+ path?: string;
43
+ /** Channel and sender-registry name. Override to run more than one bot in one process. @default "telegram" */
44
+ name?: string;
45
+ /** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply. */
46
+ allowedChats?: string[];
47
+ answerFn?: AnswerFn;
48
+ bucketsFor?: BucketsFor;
49
+ rewriteQuery?: RewriteQuery;
50
+ rerankContext?: RerankContext;
51
+ transformReply?: TransformReply;
52
+ model?: string;
53
+ /** Extra system-prompt section describing the delivery channel; passed through to `prepareChat`. @default "Channel: Telegram." */
54
+ channelHint?: string;
55
+ /** A throw/rejection is treated as "no persona" for that turn. `sender` is the namespaced `tg:<id>` key. */
56
+ personaResolver?: (ctx: {
57
+ sender: string;
58
+ text: string;
59
+ }) => string | undefined | Promise<string | undefined>;
60
+ /** Off by default — the channel stays single-turn until a store is configured. */
61
+ history?: {
62
+ store: HistoryStore;
63
+ /** Most recent turns to load per reply. @default 20 */
64
+ limit?: number;
65
+ historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
66
+ /**
67
+ * Summarize-then-truncate compaction — see
68
+ * `InboundPipelineConfig.history.compaction` in `./channels`.
69
+ * @default off
70
+ */
71
+ compaction?: HistoryCompactionOptions;
72
+ };
73
+ muteRegex?: RegExp;
74
+ unmuteRegex?: RegExp;
75
+ muteReply?: string;
76
+ unmuteReply?: string;
77
+ dmRateLimit?: {
78
+ max: number;
79
+ windowMs: number;
80
+ };
81
+ groupRateLimit?: {
82
+ max: number;
83
+ windowMs: number;
84
+ };
85
+ now?: () => number;
86
+ /** Overridable for tests and for hosts routing through a proxy; defaults to `globalThis.fetch`. */
87
+ fetch?: typeof fetch;
88
+ /** Self-hosted Bot API server origin. */
89
+ apiBaseUrl?: string;
90
+ /** Overridable for tests, which fake the Bot API instead of calling it; defaults to a `fetch` client over {@link TelegramWebhookConfig.botToken}. */
91
+ api?: TelegramApi;
92
+ logger?: Logger;
93
+ }
94
+ /**
95
+ * Builds a `CustomRoutes` mount that receives Telegram updates over HTTPS.
96
+ * Throws synchronously if `webhookSecret` is missing, blank, or outside the
97
+ * charset Telegram accepts for it — before the route ever exists, rather
98
+ * than mounting an endpoint that would silently accept everything or reject
99
+ * every request.
100
+ */
101
+ export declare function createTelegramWebhookRoute(config: TelegramWebhookConfig): CustomRoutes;
102
+ //# sourceMappingURL=webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/webhook.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,OAAO,EAAqB,KAAK,WAAW,EAAuB,MAAM,OAAO,CAAC;AAMjF,MAAM,WAAW,qBAAqB;IACpC,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;OAMG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8GAA8G;IAC9G,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iHAAiH;IACjH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kIAAkI;IAClI,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4GAA4G;IAC5G,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD,kFAAkF;IAClF,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QACnE;;;;WAIG;QACH,UAAU,CAAC,EAAE,wBAAwB,CAAC;KACvC,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mGAAmG;IACnG,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qJAAqJ;IACrJ,GAAG,CAAC,EAAE,WAAW,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,qBAAqB,GAAG,YAAY,CAkHtF"}
@@ -54,6 +54,7 @@ import { type Logger } from "../../core/logger";
54
54
  import type { RerankContext, RewriteQuery } from "../../core/pipeline";
55
55
  import type { PromptLoader } from "../../core/prompts";
56
56
  import type { VectorStore } from "../../core/retrieval";
57
+ import type { HistoryCompactionOptions } from "../../history/compaction";
57
58
  import type { HistoryStore } from "../../history/types";
58
59
  import { type ChannelMessage, type SessionIdentityRegistry } from "../gates";
59
60
  import type { WhatsAppMessageEvent } from "./channel";
@@ -199,6 +200,12 @@ export interface WhatsAppInboundConfig {
199
200
  * @default every sender is enabled
200
201
  */
201
202
  historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
203
+ /**
204
+ * Summarize-then-truncate compaction — see
205
+ * `InboundPipelineConfig.history.compaction` in `./channels`.
206
+ * @default off
207
+ */
208
+ compaction?: HistoryCompactionOptions;
202
209
  };
203
210
  now?: () => number;
204
211
  /** Logger for gate/dispatch diagnostics. Default: a console logger writing to stderr. */
@@ -1 +1 @@
1
- {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/inbound.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AACjC,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,uBAAuB,EAC7B,MAAM,UAAU,CAAC;AAElB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAC;AACtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAIrD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE/C;AAOD,kGAAkG;AAClG,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAG/E;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,SAAS,EAClB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,CAAC,CA+BjB;AAID,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,SAAS,GAAG,gBAAgB,CAWnE;AAED,mHAAmH;AACnH,wBAAgB,WAAW,CAAC,OAAO,EAAE,SAAS,GAAG,MAAM,CAWtD;AAeD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAiBvE;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,cAAc,CAAC;IACpB,8FAA8F;IAC9F,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,oBAAoB,EAC3B,QAAQ,EAAE,uBAAuB,GAChC,iBAAiB,CA+CnB;AAID,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,0GAA0G;IAC1G,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iHAAiH;IACjH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,kGAAkG;IAClG,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;OAOG;IACH,QAAQ,EAAE,uBAAuB,CAAC;IAClC,4HAA4H;IAC5H,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gHAAgH;IAChH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,cAAc,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACnD,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE;QACtB,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IACvD;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B;;;;;OAKG;IACH,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,YAAY,CAAC;QACpB,uDAAuD;QACvD,KAAK,CAAC,EAAE,MAAM,CAAC;QACf;;;;WAIG;QACH,iBAAiB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;QACnE;;;;WAIG;QACH,UAAU,CAAC,EAAE,wBAAwB,CAAC;KACvC,CAAC;IACF,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,yFAAyF;IACzF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,uTAAuT;AACvT,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,qBAAqB,GAC5B,CAAC,KAAK,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CAmGhD"}