@diegoaltoworks/chatter 0.59.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.
Files changed (36) hide show
  1. package/README.md +2 -1
  2. package/dist/channels/index.js +81 -0
  3. package/dist/channels/index.mjs +81 -0
  4. package/dist/channels/matrix/api.d.ts +133 -0
  5. package/dist/channels/matrix/api.d.ts.map +1 -0
  6. package/dist/channels/matrix/channel.d.ts +98 -0
  7. package/dist/channels/matrix/channel.d.ts.map +1 -0
  8. package/dist/channels/matrix/handler.d.ts +51 -0
  9. package/dist/channels/matrix/handler.d.ts.map +1 -0
  10. package/dist/channels/matrix/index.d.ts +37 -0
  11. package/dist/channels/matrix/index.d.ts.map +1 -0
  12. package/dist/channels/matrix/index.js +972 -0
  13. package/dist/channels/matrix/index.mjs +929 -0
  14. package/dist/channels/matrix/sync.d.ts +50 -0
  15. package/dist/channels/matrix/sync.d.ts.map +1 -0
  16. package/dist/channels/matrix/updates.d.ts +75 -0
  17. package/dist/channels/matrix/updates.d.ts.map +1 -0
  18. package/dist/channels/pipeline.d.ts +8 -0
  19. package/dist/channels/pipeline.d.ts.map +1 -1
  20. package/dist/channels/telegram/channel.d.ts +7 -0
  21. package/dist/channels/telegram/channel.d.ts.map +1 -1
  22. package/dist/channels/telegram/index.js +81 -0
  23. package/dist/channels/telegram/index.mjs +81 -0
  24. package/dist/channels/telegram/webhook.d.ts +7 -0
  25. package/dist/channels/telegram/webhook.d.ts.map +1 -1
  26. package/dist/channels/whatsapp/inbound.d.ts +7 -0
  27. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  28. package/dist/channels/whatsapp/index.js +81 -0
  29. package/dist/channels/whatsapp/index.mjs +81 -0
  30. package/dist/history/compaction.d.ts +61 -0
  31. package/dist/history/compaction.d.ts.map +1 -0
  32. package/dist/history/index.d.ts +7 -0
  33. package/dist/history/index.d.ts.map +1 -1
  34. package/dist/history/index.js +143 -0
  35. package/dist/history/index.mjs +141 -0
  36. package/package.json +8 -2
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The `/sync` long-poll loop, isolated from both the HTTP client and the
3
+ * channel so its failure behaviour is testable without either — the same
4
+ * shape as `../telegram/poll.ts`'s `runLongPoll`, adapted to Matrix's
5
+ * `since`-token pagination instead of an acknowledged offset.
6
+ *
7
+ * The property that matters: a homeserver that is down, rate-limiting, or
8
+ * rejecting the token must never turn into a tight retry loop. Every
9
+ * failure backs off exponentially, and `M_LIMIT_EXCEEDED`'s own
10
+ * `retry_after_ms` wins over that backoff when the homeserver sends one.
11
+ */
12
+ import { type Logger } from "../../core/logger";
13
+ import { type MatrixSyncResponse } from "./api";
14
+ /** Exponential backoff: 2s, 4s, 8s ... capped at a minute. `failures` is the count including the one just seen. */
15
+ export declare function syncBackoffMs(failures: number): number;
16
+ /**
17
+ * How long to wait after a failed sync. `M_LIMIT_EXCEEDED`'s
18
+ * `retry_after_ms` is an instruction, not a hint — honour it verbatim even
19
+ * when it exceeds the backoff cap, because syncing again sooner just
20
+ * extends the rate-limit window.
21
+ */
22
+ export declare function retryDelayMs(error: unknown, failures: number): number;
23
+ export interface MatrixSyncLoopDeps {
24
+ /** One `/sync` call. Rejections are backed off; they never end the loop. */
25
+ sync(since: string | undefined): Promise<MatrixSyncResponse>;
26
+ /** Per-batch work. A throw is logged and skipped — one bad batch never stops the loop. */
27
+ handleSync(response: MatrixSyncResponse): Promise<void>;
28
+ /** Checked before every sync, and again after a failure, so `stop()` cuts the loop instead of sleeping out a backoff first. */
29
+ isStopped(): boolean;
30
+ /** Overridable for tests; a real channel passes a `setTimeout`-based sleep. */
31
+ sleep(ms: number): Promise<void>;
32
+ /** Where this loop's `since` token resumes from. Omitted = an initial full sync. */
33
+ initialSince?: string;
34
+ /** Called with each newly acknowledged `since` token, for a host that persists it. */
35
+ onSince?(since: string): void;
36
+ logger?: Logger;
37
+ /** Prefix for log lines, e.g. `Matrix[@bot:example.org]`. */
38
+ label?: string;
39
+ }
40
+ /**
41
+ * Syncs until `isStopped()`, resolving with the last `since` token reached
42
+ * (what a restart would resume from).
43
+ *
44
+ * The token advances BEFORE the batch is handled, matching the Telegram
45
+ * poll loop's offset-first ordering: a handler that throws on one bad
46
+ * batch must not make the homeserver redeliver it forever, which would
47
+ * wedge the loop on a single poison event and starve every later one.
48
+ */
49
+ export declare function runMatrixSyncLoop(deps: MatrixSyncLoopDeps): Promise<string | undefined>;
50
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/sync.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAkB,KAAK,kBAAkB,EAAE,MAAM,OAAO,CAAC;AAKhE,mHAAmH;AACnH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGrE;AAED,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC7D,0FAA0F;IAC1F,UAAU,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,+HAA+H;IAC/H,SAAS,IAAI,OAAO,CAAC;IACrB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,oFAAoF;IACpF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sFAAsF;IACtF,OAAO,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAiC7F"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Matrix room event -> {@link ChannelMessage}: the transport's own half of
3
+ * the channel contract (see docs/build-a-channel.md), kept pure so mention,
4
+ * reply-to and DM detection are testable without a network or a running
5
+ * channel.
6
+ *
7
+ * Addressing parity with the other built-in channels is the point of this
8
+ * module. WhatsApp and Telegram both resolve "was the bot addressed?" from
9
+ * transport-native structures; Matrix resolves it from the stable
10
+ * `m.mentions` field (MSC3952, in the spec since v1.7) with a pill-in-HTML
11
+ * fallback for clients that only set that. The gate policy that consumes
12
+ * those booleans (`decideChannelAction`) is identical for every channel.
13
+ */
14
+ import type { ChannelMessage } from "../gates";
15
+ import type { MatrixAccountDataEvent, MatrixEvent } from "./api";
16
+ /** The bot's own identity, as `whoami` reports it. */
17
+ export interface MatrixIdentity {
18
+ userId: string;
19
+ }
20
+ interface MatrixMentions {
21
+ user_ids?: string[];
22
+ room?: boolean;
23
+ }
24
+ interface MatrixInReplyTo {
25
+ event_id?: string;
26
+ }
27
+ interface MatrixMessageContent {
28
+ msgtype?: string;
29
+ body?: string;
30
+ format?: string;
31
+ formatted_body?: string;
32
+ "m.mentions"?: MatrixMentions;
33
+ "m.relates_to"?: {
34
+ "m.in_reply_to"?: MatrixInReplyTo;
35
+ };
36
+ }
37
+ /** The message text, or "" for an event this channel doesn't render as text (a redaction, a non-text msgtype with no body). */
38
+ export declare function messageText(content: MatrixMessageContent): string;
39
+ export declare function mentionsBot(content: MatrixMessageContent, me: MatrixIdentity): boolean;
40
+ /**
41
+ * Was this message a reply to one the bot sent? `sentEventIds` is the
42
+ * session's own record of event ids it has sent (see `./channel`) — Matrix
43
+ * has no cheap "who sent event X" lookup without fetching that event, and a
44
+ * bot only ever needs to recognise its OWN messages, which it already knows
45
+ * without asking the server.
46
+ */
47
+ export declare function isReplyToBot(content: MatrixMessageContent, sentEventIds: ReadonlySet<string>): boolean;
48
+ /** How many of the session's own sent event ids {@link isReplyToBot} remembers — bounded so a long-running bot's memory doesn't grow with every message it ever sent. */
49
+ export declare const MAX_TRACKED_SENT_EVENTS = 500;
50
+ /** Records `eventId` as one of ours, evicting the oldest entry first once the set is at capacity (`Set` iterates in insertion order, so the first value is the oldest). */
51
+ export declare function recordSentEventId(sentEventIds: Set<string>, eventId: string): void;
52
+ /**
53
+ * Room ids the homeserver's `m.direct` account data lists as direct
54
+ * messages, across every peer — the canonical way a Matrix client (and so a
55
+ * bot) tells a DM from a group: there is no per-room "this is a DM" flag.
56
+ * A room absent from this set is treated as a group and needs addressing,
57
+ * the safer default when a client never populated `m.direct` for it.
58
+ */
59
+ export declare function directRoomIds(accountDataEvents: MatrixAccountDataEvent[]): Set<string>;
60
+ /**
61
+ * The {@link ChannelMessage} for a timeline event, or `undefined` when
62
+ * there is nothing for a chat pipeline to look at: not an `m.room.message`
63
+ * (a reaction, a membership change, an encrypted event this channel can't
64
+ * decrypt), or one with no `msgtype` at all (a redacted message).
65
+ */
66
+ export declare function toChannelMessage(roomId: string, event: MatrixEvent, me: MatrixIdentity, directRooms: ReadonlySet<string>, sentEventIds: ReadonlySet<string>): ChannelMessage | undefined;
67
+ /**
68
+ * A stable identity key for `bucketsFor`/`personaResolver`/`answerFn`.
69
+ * Namespaced so a bare Matrix user id (which is itself already `@user:
70
+ * server.tld`, unlike Telegram's bare numeral) never collides with another
71
+ * channel's key when a host shares persona/spend state across channels.
72
+ */
73
+ export declare function matrixSenderKey(userId: string): string;
74
+ export {};
75
+ //# sourceMappingURL=updates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"updates.d.ts","sourceRoot":"","sources":["../../../src/channels/matrix/updates.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AAEjE,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,cAAc;IACtB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,oBAAoB;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,cAAc,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,eAAe,CAAA;KAAE,CAAC;CACxD;AAED,+HAA+H;AAC/H,wBAAgB,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAEjE;AAiCD,wBAAgB,WAAW,CAAC,OAAO,EAAE,oBAAoB,EAAE,EAAE,EAAE,cAAc,GAAG,OAAO,CAQtF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,oBAAoB,EAC7B,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,OAAO,CAGT;AAED,yKAAyK;AACzK,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,2KAA2K;AAC3K,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAMlF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,iBAAiB,EAAE,sBAAsB,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAYtF;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,WAAW,EAClB,EAAE,EAAE,cAAc,EAClB,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,EAChC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAChC,cAAc,GAAG,SAAS,CAe5B;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD"}
@@ -20,6 +20,7 @@ import type { Logger } from "../core/logger";
20
20
  import { type RerankContext, type RewriteQuery } from "../core/pipeline";
21
21
  import type { PromptLoader } from "../core/prompts";
22
22
  import type { VectorStore } from "../core/retrieval";
23
+ import type { HistoryCompactionOptions } from "../history/compaction";
23
24
  import type { HistoryStore } from "../history/types";
24
25
  import { type ChannelMessage } from "./gates";
25
26
  /** Where a pipeline turn's output goes. Distinct from `ChannelSender` (`./senders`) — a transport implementing this may thread/quote the chat answer without doing the same for a plain gate acknowledgement. */
@@ -69,6 +70,13 @@ export interface InboundPipelineConfig {
69
70
  * opted out. @default every sender is enabled
70
71
  */
71
72
  historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
73
+ /**
74
+ * Summarize-then-truncate compaction, checked right after each reply is
75
+ * appended. Off by default — a conversation's window grows unbounded
76
+ * (aside from the store's own `maxPerConversation`) until this is set.
77
+ * See `../history/compaction`.
78
+ */
79
+ compaction?: HistoryCompactionOptions;
72
80
  };
73
81
  /** Group chats eligible for a reply. Empty (default) = every group. Has no effect on DMs, which always reply. */
74
82
  allowedChats?: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/channels/pipeline.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/channels/pipeline.ts"],"names":[],"mappings":"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"}
@@ -24,6 +24,7 @@ import type { AnswerFn, TransformReply } from "../../core/answer";
24
24
  import type { BucketsFor } from "../../core/buckets";
25
25
  import { type Logger } from "../../core/logger";
26
26
  import type { RerankContext, RewriteQuery } from "../../core/pipeline";
27
+ import type { HistoryCompactionOptions } from "../../history/compaction";
27
28
  import type { HistoryStore } from "../../history/types";
28
29
  import type { Channel } from "../index";
29
30
  import { type TelegramApi } from "./api";
@@ -61,6 +62,12 @@ export interface TelegramChannelConfig {
61
62
  * @default every sender is enabled
62
63
  */
63
64
  historyEnabledFor?: (sender: string) => boolean | Promise<boolean>;
65
+ /**
66
+ * Summarize-then-truncate compaction — see
67
+ * `InboundPipelineConfig.history.compaction` in `./channels`.
68
+ * @default off
69
+ */
70
+ compaction?: HistoryCompactionOptions;
64
71
  };
65
72
  muteRegex?: RegExp;
66
73
  unmuteRegex?: RegExp;
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/telegram/channel.ts"],"names":[],"mappings":"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"}
@@ -393,6 +393,75 @@ ${ctx.join("\n\n")}`
393
393
  return { system, messages, context: ctx };
394
394
  }
395
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
+
396
465
  // src/channels/gates.ts
397
466
  function matches(re, text) {
398
467
  return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
@@ -473,6 +542,10 @@ function createInboundPipeline(deps, config) {
473
542
  const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
474
543
  const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
475
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;
476
549
  return async function handleInboundMessage(msg, turn) {
477
550
  const allowedChats = config.allowedChats ?? [];
478
551
  const action = decideChannelAction(msg, {
@@ -548,6 +621,14 @@ function createInboundPipeline(deps, config) {
548
621
  await config.history?.store.append(conversationId, { role: "assistant", content });
549
622
  }
550
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
+ }
551
632
  return { action: "reply", content };
552
633
  };
553
634
  }
@@ -348,6 +348,75 @@ ${ctx.join("\n\n")}`
348
348
  return { system, messages, context: ctx };
349
349
  }
350
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
+
351
420
  // src/channels/gates.ts
352
421
  function matches(re, text) {
353
422
  return new RegExp(re.source, re.flags.replace(/[gy]/g, "")).test(text);
@@ -428,6 +497,10 @@ function createInboundPipeline(deps, config) {
428
497
  const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
429
498
  const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
430
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;
431
504
  return async function handleInboundMessage(msg, turn) {
432
505
  const allowedChats = config.allowedChats ?? [];
433
506
  const action = decideChannelAction(msg, {
@@ -503,6 +576,14 @@ function createInboundPipeline(deps, config) {
503
576
  await config.history?.store.append(conversationId, { role: "assistant", content });
504
577
  }
505
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
+ }
506
587
  return { action: "reply", content };
507
588
  };
508
589
  }
@@ -23,6 +23,7 @@ import type { AnswerFn, TransformReply } from "../../core/answer";
23
23
  import type { BucketsFor } from "../../core/buckets";
24
24
  import { type Logger } from "../../core/logger";
25
25
  import type { RerankContext, RewriteQuery } from "../../core/pipeline";
26
+ import type { HistoryCompactionOptions } from "../../history/compaction";
26
27
  import type { HistoryStore } from "../../history/types";
27
28
  import type { CustomRoutes } from "../../types";
28
29
  import { type TelegramApi } from "./api";
@@ -62,6 +63,12 @@ export interface TelegramWebhookConfig {
62
63
  /** Most recent turns to load per reply. @default 20 */
63
64
  limit?: number;
64
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;
65
72
  };
66
73
  muteRegex?: RegExp;
67
74
  unmuteRegex?: RegExp;
@@ -1 +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,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;KACpE,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"}
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":"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"}
@@ -840,6 +840,75 @@ ${ctx.join("\n\n")}`
840
840
  return { system, messages, context: ctx };
841
841
  }
842
842
 
843
+ // src/history/compaction.ts
844
+ var DEFAULT_TIMEOUT_MS = 8e3;
845
+ var LOAD_ALL = Number.MAX_SAFE_INTEGER;
846
+ var SUMMARY_PREFIX = "Summary of earlier conversation: ";
847
+ function defaultSummarize(client, model) {
848
+ return async ({ turns }) => {
849
+ const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
850
+ const { content } = await answerOnce({
851
+ client,
852
+ 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.",
853
+ messages: [{ role: "user", content: transcript }],
854
+ // No `answerFn`: compaction is an internal chatter operation, not a
855
+ // chat surface, so it must not conscript a caller's brain hook, which
856
+ // is shaped to answer the user, not to summarize on the engine's behalf.
857
+ mode: "public",
858
+ model
859
+ });
860
+ return content;
861
+ };
862
+ }
863
+ function createHistoryCompactor(deps, options) {
864
+ const { threshold, keep, logger } = options;
865
+ if (!Number.isInteger(threshold) || threshold < 1) {
866
+ throw new Error(`Invalid history compaction threshold: ${threshold}`);
867
+ }
868
+ if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
869
+ throw new Error(`Invalid history compaction keep: ${keep}`);
870
+ }
871
+ const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
872
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
873
+ return {
874
+ async maybeCompact(store, conversationId) {
875
+ const turns = await store.load(conversationId, LOAD_ALL);
876
+ if (turns.length < threshold) return;
877
+ const older = turns.slice(0, turns.length - keep);
878
+ let summary;
879
+ let timer;
880
+ try {
881
+ summary = await Promise.race([
882
+ summarize({ conversationId, turns: older }),
883
+ new Promise((_, reject) => {
884
+ timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
885
+ })
886
+ ]);
887
+ } catch (error) {
888
+ logger?.error(
889
+ `history compaction summarize failed for "${conversationId}"; history left untouched`,
890
+ error
891
+ );
892
+ return;
893
+ } finally {
894
+ clearTimeout(timer);
895
+ }
896
+ const trimmed = summary.trim();
897
+ if (!trimmed) return;
898
+ const fresh = await store.load(conversationId, LOAD_ALL);
899
+ const recent = fresh.slice(older.length);
900
+ await store.clear(conversationId);
901
+ await store.append(conversationId, {
902
+ role: "assistant",
903
+ content: `${SUMMARY_PREFIX}${trimmed}`
904
+ });
905
+ for (const turn of recent) {
906
+ await store.append(conversationId, turn);
907
+ }
908
+ }
909
+ };
910
+ }
911
+
843
912
  // src/channels/pipeline.ts
844
913
  var DEFAULT_HISTORY_LIMIT = 20;
845
914
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
@@ -870,6 +939,10 @@ function createInboundPipeline(deps, config) {
870
939
  const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
871
940
  const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
872
941
  const mode = "public";
942
+ const compactor = config.history?.compaction ? createHistoryCompactor(
943
+ { client: deps.client },
944
+ { model: config.model, ...config.history.compaction }
945
+ ) : void 0;
873
946
  return async function handleInboundMessage(msg, turn) {
874
947
  const allowedChats = config.allowedChats ?? [];
875
948
  const action = decideChannelAction(msg, {
@@ -945,6 +1018,14 @@ function createInboundPipeline(deps, config) {
945
1018
  await config.history?.store.append(conversationId, { role: "assistant", content });
946
1019
  }
947
1020
  await turn.reply.sendAnswer(msg.chatId, content);
1021
+ const history = config.history;
1022
+ if (compactor && historyEnabled && history) {
1023
+ try {
1024
+ await compactor.maybeCompact(history.store, conversationId);
1025
+ } catch (error) {
1026
+ config.logger?.error(`history compaction failed for "${conversationId}"`, error);
1027
+ }
1028
+ }
948
1029
  return { action: "reply", content };
949
1030
  };
950
1031
  }