@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
@@ -778,6 +778,75 @@ ${ctx.join("\n\n")}`
778
778
  return { system, messages, context: ctx };
779
779
  }
780
780
 
781
+ // src/history/compaction.ts
782
+ var DEFAULT_TIMEOUT_MS = 8e3;
783
+ var LOAD_ALL = Number.MAX_SAFE_INTEGER;
784
+ var SUMMARY_PREFIX = "Summary of earlier conversation: ";
785
+ function defaultSummarize(client, model) {
786
+ return async ({ turns }) => {
787
+ const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
788
+ const { content } = await answerOnce({
789
+ client,
790
+ 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.",
791
+ messages: [{ role: "user", content: transcript }],
792
+ // No `answerFn`: compaction is an internal chatter operation, not a
793
+ // chat surface, so it must not conscript a caller's brain hook, which
794
+ // is shaped to answer the user, not to summarize on the engine's behalf.
795
+ mode: "public",
796
+ model
797
+ });
798
+ return content;
799
+ };
800
+ }
801
+ function createHistoryCompactor(deps, options) {
802
+ const { threshold, keep, logger } = options;
803
+ if (!Number.isInteger(threshold) || threshold < 1) {
804
+ throw new Error(`Invalid history compaction threshold: ${threshold}`);
805
+ }
806
+ if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
807
+ throw new Error(`Invalid history compaction keep: ${keep}`);
808
+ }
809
+ const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
810
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
811
+ return {
812
+ async maybeCompact(store, conversationId) {
813
+ const turns = await store.load(conversationId, LOAD_ALL);
814
+ if (turns.length < threshold) return;
815
+ const older = turns.slice(0, turns.length - keep);
816
+ let summary;
817
+ let timer;
818
+ try {
819
+ summary = await Promise.race([
820
+ summarize({ conversationId, turns: older }),
821
+ new Promise((_, reject) => {
822
+ timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
823
+ })
824
+ ]);
825
+ } catch (error) {
826
+ logger?.error(
827
+ `history compaction summarize failed for "${conversationId}"; history left untouched`,
828
+ error
829
+ );
830
+ return;
831
+ } finally {
832
+ clearTimeout(timer);
833
+ }
834
+ const trimmed = summary.trim();
835
+ if (!trimmed) return;
836
+ const fresh = await store.load(conversationId, LOAD_ALL);
837
+ const recent = fresh.slice(older.length);
838
+ await store.clear(conversationId);
839
+ await store.append(conversationId, {
840
+ role: "assistant",
841
+ content: `${SUMMARY_PREFIX}${trimmed}`
842
+ });
843
+ for (const turn of recent) {
844
+ await store.append(conversationId, turn);
845
+ }
846
+ }
847
+ };
848
+ }
849
+
781
850
  // src/channels/pipeline.ts
782
851
  var DEFAULT_HISTORY_LIMIT = 20;
783
852
  var DEFAULT_GROUP_RATE_LIMIT = { max: 30, windowMs: 60 * 60 * 1e3 };
@@ -808,6 +877,10 @@ function createInboundPipeline(deps, config) {
808
877
  const underGroupRateLimit = createSlidingWindowRateLimiter(group.max, group.windowMs, now);
809
878
  const underDmRateLimit = createSlidingWindowRateLimiter(dm.max, dm.windowMs, now);
810
879
  const mode = "public";
880
+ const compactor = config.history?.compaction ? createHistoryCompactor(
881
+ { client: deps.client },
882
+ { model: config.model, ...config.history.compaction }
883
+ ) : void 0;
811
884
  return async function handleInboundMessage(msg, turn) {
812
885
  const allowedChats = config.allowedChats ?? [];
813
886
  const action = decideChannelAction(msg, {
@@ -883,6 +956,14 @@ function createInboundPipeline(deps, config) {
883
956
  await config.history?.store.append(conversationId, { role: "assistant", content });
884
957
  }
885
958
  await turn.reply.sendAnswer(msg.chatId, content);
959
+ const history = config.history;
960
+ if (compactor && historyEnabled && history) {
961
+ try {
962
+ await compactor.maybeCompact(history.store, conversationId);
963
+ } catch (error) {
964
+ config.logger?.error(`history compaction failed for "${conversationId}"`, error);
965
+ }
966
+ }
886
967
  return { action: "reply", content };
887
968
  };
888
969
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Summarize-then-truncate compaction for `HistoryStore` — folds older turns
3
+ * into a single stored summary row once a conversation's window grows past a
4
+ * threshold, so a long-running conversation's prompt doesn't grow without
5
+ * bound. Off by default; a caller wires `maybeCompact` in right after its own
6
+ * `store.append` call, the same way `./index`'s docs describe wiring the
7
+ * store itself.
8
+ *
9
+ * The summarize step races a timeout the same way `../images/captionComposer`
10
+ * races its compose step, except the safe fallback here is "leave history
11
+ * untouched" rather than a template pool — a stuck or failing summarize call
12
+ * must never lose turns nobody got to read back.
13
+ */
14
+ import type OpenAI from "openai";
15
+ import type { Logger } from "../core/logger";
16
+ import type { HistoryMessage, HistoryStore } from "./types";
17
+ /** What a summarize step receives: the turns being folded away, oldest first. Never includes the turns `keep` retains verbatim. */
18
+ export interface SummarizeRequest {
19
+ conversationId: string;
20
+ turns: HistoryMessage[];
21
+ }
22
+ /** Best-effort compaction step, typically an LLM call. */
23
+ export type SummarizeFn = (request: SummarizeRequest) => Promise<string>;
24
+ export interface HistoryCompactionOptions {
25
+ /** Turn count that triggers compaction. */
26
+ threshold: number;
27
+ /** Most recent turns kept verbatim after compaction; everything older is folded into the summary. Must be less than `threshold`. */
28
+ keep: number;
29
+ /**
30
+ * Best-effort summarize step. Defaults to `answerOnce` with a neutral
31
+ * prompt and no `answerFn` (so it always uses the built-in completion,
32
+ * never a caller's brain hook) — set this to route summarization through a
33
+ * caller's own `answerFn`, a cheaper model, or a non-LLM summarizer.
34
+ */
35
+ summarize?: SummarizeFn;
36
+ /** Model for the default `summarize` step. Ignored when `summarize` is supplied. Falls back to `answerOnce`'s own default model when unset. */
37
+ model?: string;
38
+ /** Milliseconds before `summarize` is abandoned in favour of leaving history untouched for this turn. @default 8000 */
39
+ timeoutMs?: number;
40
+ logger?: Pick<Logger, "error">;
41
+ }
42
+ export interface HistoryCompactor {
43
+ /**
44
+ * Compacts `conversationId` if its stored turn count has reached
45
+ * `threshold`. A no-op below threshold. A failed or timed-out `summarize`
46
+ * leaves the store exactly as it was and does not throw; a failure in the
47
+ * store itself (`load`/`clear`/`append`) propagates like any other store
48
+ * failure.
49
+ */
50
+ maybeCompact(store: HistoryStore, conversationId: string): Promise<void>;
51
+ }
52
+ /** Marks a stored row as a compaction summary rather than a real turn — informational only, nothing parses it back out. */
53
+ export declare const SUMMARY_PREFIX = "Summary of earlier conversation: ";
54
+ /**
55
+ * `deps.client` builds the default `summarize` step; unused when a caller
56
+ * supplies its own.
57
+ */
58
+ export declare function createHistoryCompactor(deps: {
59
+ client: OpenAI;
60
+ }, options: HistoryCompactionOptions): HistoryCompactor;
61
+ //# sourceMappingURL=compaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compaction.d.ts","sourceRoot":"","sources":["../../src/history/compaction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,MAAM,MAAM,QAAQ,CAAC;AAEjC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5D,mIAAmI;AACnI,MAAM,WAAW,gBAAgB;IAC/B,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,0DAA0D;AAC1D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAEzE,MAAM,WAAW,wBAAwB;IACvC,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,oIAAoI;IACpI,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,+IAA+I;IAC/I,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uHAAuH;IACvH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAWD,2HAA2H;AAC3H,eAAO,MAAM,cAAc,sCAAsC,CAAC;AAoBlE;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACxB,OAAO,EAAE,wBAAwB,GAChC,gBAAgB,CAgElB"}
@@ -10,8 +10,15 @@
10
10
  * implement it. The shipped Turso implementation needs `@libsql/client`,
11
11
  * which chatter already expects for retrieval.
12
12
  *
13
+ * `./compaction` is an optional layer on top: summarize-then-truncate for a
14
+ * conversation whose window has grown past a threshold, built entirely on
15
+ * the store's own `load`/`clear`/`append` so it works with any
16
+ * `HistoryStore`, not just the Turso one.
17
+ *
13
18
  * @packageDocumentation
14
19
  */
20
+ export type { HistoryCompactionOptions, HistoryCompactor, SummarizeFn, SummarizeRequest, } from "./compaction";
21
+ export { createHistoryCompactor, SUMMARY_PREFIX } from "./compaction";
15
22
  export type { TursoHistoryStoreOptions } from "./tursoStore";
16
23
  export { createTursoHistoryStore } from "./tursoStore";
17
24
  export type { HistoryMessage, HistoryStore } from "./types";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,YAAY,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,YAAY,EACV,wBAAwB,EACxB,gBAAgB,EAChB,WAAW,EACX,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACtE,YAAY,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACvD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC"}
@@ -20,10 +20,151 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/history/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ SUMMARY_PREFIX: () => SUMMARY_PREFIX,
24
+ createHistoryCompactor: () => createHistoryCompactor,
23
25
  createTursoHistoryStore: () => createTursoHistoryStore
24
26
  });
25
27
  module.exports = __toCommonJS(index_exports);
26
28
 
29
+ // src/core/guardrails.ts
30
+ function detectLeakage(text) {
31
+ const flags = [
32
+ /system prompt/i,
33
+ /hidden instruction/i,
34
+ /here (are|is) (my|the) rules/i,
35
+ /BEGIN SYSTEM PROMPT/i,
36
+ /tool instructions/i
37
+ ];
38
+ return flags.some((rx) => rx.test(text));
39
+ }
40
+ function scrubOutput(text) {
41
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
42
+ }
43
+ function guardOutput(text) {
44
+ if (detectLeakage(text)) {
45
+ return "Sorry, I can't share internal instructions. How else can I help?";
46
+ }
47
+ return scrubOutput(text);
48
+ }
49
+
50
+ // src/core/llm.ts
51
+ var DEFAULT_MODEL = "gpt-4o";
52
+ async function completeOnce({
53
+ client,
54
+ system,
55
+ messages,
56
+ temperature = 0.2,
57
+ model = DEFAULT_MODEL
58
+ }) {
59
+ const res = await client.chat.completions.create({
60
+ model,
61
+ temperature,
62
+ messages: [{ role: "system", content: system }, ...messages]
63
+ });
64
+ const text = res.choices[0]?.message?.content ?? "";
65
+ return {
66
+ content: guardOutput(text),
67
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
68
+ };
69
+ }
70
+
71
+ // src/core/answer.ts
72
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
73
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
74
+ const result = await answerFn({ system, messages, mode, sender, conversationId });
75
+ const raw = typeof result === "string" ? result : result?.content;
76
+ const usage = typeof result === "string" ? void 0 : result?.usage;
77
+ return {
78
+ content: guardOutput(typeof raw === "string" ? raw : ""),
79
+ usage: usage ?? noUsage()
80
+ };
81
+ }
82
+ async function answerOnce({
83
+ answerFn,
84
+ client,
85
+ system,
86
+ messages,
87
+ mode,
88
+ sender,
89
+ conversationId,
90
+ temperature,
91
+ model
92
+ }) {
93
+ if (answerFn) {
94
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
95
+ }
96
+ return completeOnce({ client, system, messages, temperature, model });
97
+ }
98
+
99
+ // src/history/compaction.ts
100
+ var DEFAULT_TIMEOUT_MS = 8e3;
101
+ var LOAD_ALL = Number.MAX_SAFE_INTEGER;
102
+ var SUMMARY_PREFIX = "Summary of earlier conversation: ";
103
+ function defaultSummarize(client, model) {
104
+ return async ({ turns }) => {
105
+ const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
106
+ const { content } = await answerOnce({
107
+ client,
108
+ 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.",
109
+ messages: [{ role: "user", content: transcript }],
110
+ // No `answerFn`: compaction is an internal chatter operation, not a
111
+ // chat surface, so it must not conscript a caller's brain hook, which
112
+ // is shaped to answer the user, not to summarize on the engine's behalf.
113
+ mode: "public",
114
+ model
115
+ });
116
+ return content;
117
+ };
118
+ }
119
+ function createHistoryCompactor(deps, options) {
120
+ const { threshold, keep, logger } = options;
121
+ if (!Number.isInteger(threshold) || threshold < 1) {
122
+ throw new Error(`Invalid history compaction threshold: ${threshold}`);
123
+ }
124
+ if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
125
+ throw new Error(`Invalid history compaction keep: ${keep}`);
126
+ }
127
+ const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
128
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
129
+ return {
130
+ async maybeCompact(store, conversationId) {
131
+ const turns = await store.load(conversationId, LOAD_ALL);
132
+ if (turns.length < threshold) return;
133
+ const older = turns.slice(0, turns.length - keep);
134
+ let summary;
135
+ let timer;
136
+ try {
137
+ summary = await Promise.race([
138
+ summarize({ conversationId, turns: older }),
139
+ new Promise((_, reject) => {
140
+ timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
141
+ })
142
+ ]);
143
+ } catch (error) {
144
+ logger?.error(
145
+ `history compaction summarize failed for "${conversationId}"; history left untouched`,
146
+ error
147
+ );
148
+ return;
149
+ } finally {
150
+ clearTimeout(timer);
151
+ }
152
+ const trimmed = summary.trim();
153
+ if (!trimmed) return;
154
+ const fresh = await store.load(conversationId, LOAD_ALL);
155
+ const recent = fresh.slice(older.length);
156
+ await store.clear(conversationId);
157
+ await store.append(conversationId, {
158
+ role: "assistant",
159
+ content: `${SUMMARY_PREFIX}${trimmed}`
160
+ });
161
+ for (const turn of recent) {
162
+ await store.append(conversationId, turn);
163
+ }
164
+ }
165
+ };
166
+ }
167
+
27
168
  // src/history/tursoStore.ts
28
169
  var schemaReady = /* @__PURE__ */ new WeakMap();
29
170
  function ensureHistorySchema(client, tableName) {
@@ -117,5 +258,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
117
258
  }
118
259
  // Annotate the CommonJS export names for ESM import in node:
119
260
  0 && (module.exports = {
261
+ SUMMARY_PREFIX,
262
+ createHistoryCompactor,
120
263
  createTursoHistoryStore
121
264
  });
@@ -1,3 +1,142 @@
1
+ // src/core/guardrails.ts
2
+ function detectLeakage(text) {
3
+ const flags = [
4
+ /system prompt/i,
5
+ /hidden instruction/i,
6
+ /here (are|is) (my|the) rules/i,
7
+ /BEGIN SYSTEM PROMPT/i,
8
+ /tool instructions/i
9
+ ];
10
+ return flags.some((rx) => rx.test(text));
11
+ }
12
+ function scrubOutput(text) {
13
+ return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
14
+ }
15
+ function guardOutput(text) {
16
+ if (detectLeakage(text)) {
17
+ return "Sorry, I can't share internal instructions. How else can I help?";
18
+ }
19
+ return scrubOutput(text);
20
+ }
21
+
22
+ // src/core/llm.ts
23
+ var DEFAULT_MODEL = "gpt-4o";
24
+ async function completeOnce({
25
+ client,
26
+ system,
27
+ messages,
28
+ temperature = 0.2,
29
+ model = DEFAULT_MODEL
30
+ }) {
31
+ const res = await client.chat.completions.create({
32
+ model,
33
+ temperature,
34
+ messages: [{ role: "system", content: system }, ...messages]
35
+ });
36
+ const text = res.choices[0]?.message?.content ?? "";
37
+ return {
38
+ content: guardOutput(text),
39
+ usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
40
+ };
41
+ }
42
+
43
+ // src/core/answer.ts
44
+ var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
45
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
46
+ const result = await answerFn({ system, messages, mode, sender, conversationId });
47
+ const raw = typeof result === "string" ? result : result?.content;
48
+ const usage = typeof result === "string" ? void 0 : result?.usage;
49
+ return {
50
+ content: guardOutput(typeof raw === "string" ? raw : ""),
51
+ usage: usage ?? noUsage()
52
+ };
53
+ }
54
+ async function answerOnce({
55
+ answerFn,
56
+ client,
57
+ system,
58
+ messages,
59
+ mode,
60
+ sender,
61
+ conversationId,
62
+ temperature,
63
+ model
64
+ }) {
65
+ if (answerFn) {
66
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
67
+ }
68
+ return completeOnce({ client, system, messages, temperature, model });
69
+ }
70
+
71
+ // src/history/compaction.ts
72
+ var DEFAULT_TIMEOUT_MS = 8e3;
73
+ var LOAD_ALL = Number.MAX_SAFE_INTEGER;
74
+ var SUMMARY_PREFIX = "Summary of earlier conversation: ";
75
+ function defaultSummarize(client, model) {
76
+ return async ({ turns }) => {
77
+ const transcript = turns.map((turn) => `${turn.role}: ${turn.content}`).join("\n");
78
+ const { content } = await answerOnce({
79
+ client,
80
+ 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.",
81
+ messages: [{ role: "user", content: transcript }],
82
+ // No `answerFn`: compaction is an internal chatter operation, not a
83
+ // chat surface, so it must not conscript a caller's brain hook, which
84
+ // is shaped to answer the user, not to summarize on the engine's behalf.
85
+ mode: "public",
86
+ model
87
+ });
88
+ return content;
89
+ };
90
+ }
91
+ function createHistoryCompactor(deps, options) {
92
+ const { threshold, keep, logger } = options;
93
+ if (!Number.isInteger(threshold) || threshold < 1) {
94
+ throw new Error(`Invalid history compaction threshold: ${threshold}`);
95
+ }
96
+ if (!Number.isInteger(keep) || keep < 0 || keep >= threshold) {
97
+ throw new Error(`Invalid history compaction keep: ${keep}`);
98
+ }
99
+ const summarize = options.summarize ?? defaultSummarize(deps.client, options.model);
100
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
101
+ return {
102
+ async maybeCompact(store, conversationId) {
103
+ const turns = await store.load(conversationId, LOAD_ALL);
104
+ if (turns.length < threshold) return;
105
+ const older = turns.slice(0, turns.length - keep);
106
+ let summary;
107
+ let timer;
108
+ try {
109
+ summary = await Promise.race([
110
+ summarize({ conversationId, turns: older }),
111
+ new Promise((_, reject) => {
112
+ timer = setTimeout(() => reject(new Error("history compaction timed out")), timeoutMs);
113
+ })
114
+ ]);
115
+ } catch (error) {
116
+ logger?.error(
117
+ `history compaction summarize failed for "${conversationId}"; history left untouched`,
118
+ error
119
+ );
120
+ return;
121
+ } finally {
122
+ clearTimeout(timer);
123
+ }
124
+ const trimmed = summary.trim();
125
+ if (!trimmed) return;
126
+ const fresh = await store.load(conversationId, LOAD_ALL);
127
+ const recent = fresh.slice(older.length);
128
+ await store.clear(conversationId);
129
+ await store.append(conversationId, {
130
+ role: "assistant",
131
+ content: `${SUMMARY_PREFIX}${trimmed}`
132
+ });
133
+ for (const turn of recent) {
134
+ await store.append(conversationId, turn);
135
+ }
136
+ }
137
+ };
138
+ }
139
+
1
140
  // src/history/tursoStore.ts
2
141
  var schemaReady = /* @__PURE__ */ new WeakMap();
3
142
  function ensureHistorySchema(client, tableName) {
@@ -90,5 +229,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
90
229
  };
91
230
  }
92
231
  export {
232
+ SUMMARY_PREFIX,
233
+ createHistoryCompactor,
93
234
  createTursoHistoryStore
94
235
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegoaltoworks/chatter",
3
- "version": "0.59.0",
3
+ "version": "0.61.0",
4
4
  "description": "Embeddable AI chatbot framework with RAG, authentication, and customizable widgets",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -68,6 +68,11 @@
68
68
  "import": "./dist/channels/telegram/index.mjs",
69
69
  "require": "./dist/channels/telegram/index.js"
70
70
  },
71
+ "./matrix": {
72
+ "types": "./dist/channels/matrix/index.d.ts",
73
+ "import": "./dist/channels/matrix/index.mjs",
74
+ "require": "./dist/channels/matrix/index.js"
75
+ },
71
76
  "./images": {
72
77
  "types": "./dist/images/index.d.ts",
73
78
  "import": "./dist/images/index.mjs",
@@ -88,7 +93,7 @@
88
93
  ],
89
94
  "scripts": {
90
95
  "dev": "bun --watch src/index.ts",
91
- "build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:server && bun run build:mcp && bun run build:usage && bun run build:history && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:telegram && bun run build:flows && bun run build:images && bun run build:scheduler && bun run build:bin && bun run build:client && bun run build:copy-client",
96
+ "build": "bun run build:clean && bun run build:types && bun run build:esm && bun run build:cjs && bun run build:server && bun run build:mcp && bun run build:usage && bun run build:history && bun run build:personas && bun run build:channels && bun run build:whatsapp && bun run build:telegram && bun run build:matrix && bun run build:flows && bun run build:images && bun run build:scheduler && bun run build:bin && bun run build:client && bun run build:copy-client",
92
97
  "build:clean": "rm -rf dist",
93
98
  "build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
94
99
  "build:esm": "esbuild src/index.ts --bundle --format=esm --outfile=dist/index.mjs --external:@hono/node-server --external:hono --external:openai --external:@libsql/client --external:jose --external:@modelcontextprotocol/sdk --external:zod --platform=node --target=node24",
@@ -101,6 +106,7 @@
101
106
  "build:channels": "esbuild src/channels/index.ts --bundle --format=esm --outfile=dist/channels/index.mjs --external:hono --external:openai --external:@libsql/client --platform=node --target=node24 && esbuild src/channels/index.ts --bundle --format=cjs --outfile=dist/channels/index.js --external:hono --external:openai --external:@libsql/client --platform=node --target=node24",
102
107
  "build:whatsapp": "esbuild src/channels/whatsapp/index.ts --bundle --format=esm --outfile=dist/channels/whatsapp/index.mjs --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24 && esbuild src/channels/whatsapp/index.ts --bundle --format=cjs --outfile=dist/channels/whatsapp/index.js --external:@libsql/client --external:@whiskeysockets/baileys --platform=node --target=node24",
103
108
  "build:telegram": "esbuild src/channels/telegram/index.ts --bundle --format=esm --outfile=dist/channels/telegram/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/channels/telegram/index.ts --bundle --format=cjs --outfile=dist/channels/telegram/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
109
+ "build:matrix": "esbuild src/channels/matrix/index.ts --bundle --format=esm --outfile=dist/channels/matrix/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/channels/matrix/index.ts --bundle --format=cjs --outfile=dist/channels/matrix/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
104
110
  "build:flows": "esbuild src/flows/index.ts --bundle --format=esm --outfile=dist/flows/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/flows/index.ts --bundle --format=cjs --outfile=dist/flows/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
105
111
  "build:images": "esbuild src/images/index.ts --bundle --format=esm --outfile=dist/images/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/images/index.ts --bundle --format=cjs --outfile=dist/images/index.js --external:@libsql/client --external:openai --platform=node --target=node24",
106
112
  "build:scheduler": "esbuild src/scheduler/index.ts --bundle --format=esm --outfile=dist/scheduler/index.mjs --external:@libsql/client --external:openai --platform=node --target=node24 && esbuild src/scheduler/index.ts --bundle --format=cjs --outfile=dist/scheduler/index.js --external:@libsql/client --external:openai --platform=node --target=node24",