@diegoaltoworks/chatter 0.56.0 → 0.58.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 (38) hide show
  1. package/dist/channels/index.js +46 -6
  2. package/dist/channels/index.mjs +46 -6
  3. package/dist/channels/pipeline.d.ts +13 -0
  4. package/dist/channels/pipeline.d.ts.map +1 -1
  5. package/dist/channels/telegram/channel.d.ts +12 -0
  6. package/dist/channels/telegram/channel.d.ts.map +1 -1
  7. package/dist/channels/telegram/index.js +48 -6
  8. package/dist/channels/telegram/index.mjs +48 -6
  9. package/dist/channels/whatsapp/inbound.d.ts +13 -0
  10. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  11. package/dist/channels/whatsapp/index.js +48 -6
  12. package/dist/channels/whatsapp/index.mjs +48 -6
  13. package/dist/core/pipeline.d.ts +52 -1
  14. package/dist/core/pipeline.d.ts.map +1 -1
  15. package/dist/history/index.d.ts +1 -0
  16. package/dist/history/index.d.ts.map +1 -1
  17. package/dist/history/index.js +23 -2
  18. package/dist/history/index.mjs +23 -2
  19. package/dist/history/tursoStore.d.ts +13 -1
  20. package/dist/history/tursoStore.d.ts.map +1 -1
  21. package/dist/history/types.d.ts +2 -0
  22. package/dist/history/types.d.ts.map +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +74 -9
  26. package/dist/index.mjs +74 -9
  27. package/dist/mcp-server.d.ts.map +1 -1
  28. package/dist/mcp-server.js +31 -4
  29. package/dist/mcp-server.mjs +31 -4
  30. package/dist/routes/demo.d.ts.map +1 -1
  31. package/dist/routes/openai.d.ts.map +1 -1
  32. package/dist/routes/private.d.ts.map +1 -1
  33. package/dist/routes/public.d.ts.map +1 -1
  34. package/dist/server.js +66 -7
  35. package/dist/server.mjs +66 -7
  36. package/dist/types.d.ts +25 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/package.json +1 -1
@@ -737,14 +737,35 @@ async function prepareChat({
737
737
  messages,
738
738
  personaLayer,
739
739
  channelHint,
740
- buckets
740
+ buckets,
741
+ sender,
742
+ rewriteQuery,
743
+ rerankContext,
744
+ logger
741
745
  }) {
742
746
  const lastUserMsg = lastUserMessage(messages);
743
747
  if (!lastUserMsg) {
744
748
  throw new Error("no user message found in conversation");
745
749
  }
746
750
  const { topK, label } = MODE_SETTINGS[mode];
747
- const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
751
+ let query = lastUserMsg.content;
752
+ if (rewriteQuery) {
753
+ try {
754
+ const rewritten = await rewriteQuery({ query, mode, sender });
755
+ if (typeof rewritten === "string" && rewritten.trim()) query = rewritten;
756
+ } catch (error) {
757
+ logger?.error("rewriteQuery threw; falling back to the original query", error);
758
+ }
759
+ }
760
+ let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
761
+ if (rerankContext) {
762
+ try {
763
+ const reranked = await rerankContext({ query, chunks: ctx });
764
+ if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
765
+ } catch (error) {
766
+ logger?.error("rerankContext threw; falling back to the original chunks", error);
767
+ }
768
+ }
748
769
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
749
770
  const hint = channelHint?.trim();
750
771
  const system = [
@@ -769,6 +790,16 @@ async function resolvePersonaLayer(config, ctx) {
769
790
  return void 0;
770
791
  }
771
792
  }
793
+ async function isHistoryEnabled(config, sender) {
794
+ const predicate = config.history?.historyEnabledFor;
795
+ if (!predicate) return true;
796
+ try {
797
+ return await predicate(sender);
798
+ } catch (error) {
799
+ config.logger?.error("historyEnabledFor threw; disabling history for this turn", error);
800
+ return false;
801
+ }
802
+ }
772
803
  function createInboundPipeline(deps, config) {
773
804
  const mutedChats = /* @__PURE__ */ new Set();
774
805
  const now = config.now ?? Date.now;
@@ -808,12 +839,15 @@ function createInboundPipeline(deps, config) {
808
839
  }
809
840
  const personaLayer = await resolvePersonaLayer(config, { sender, text: msg.text });
810
841
  const buckets = await resolveBuckets({ mode, sender, bucketsFor: config.bucketsFor });
811
- const priorTurns = config.history ? await config.history.store.load(
842
+ const historyEnabled = config.history ? await isHistoryEnabled(config, sender) : false;
843
+ const priorTurns = config.history && historyEnabled ? await config.history.store.load(
812
844
  conversationId,
813
845
  config.history.limit ?? DEFAULT_HISTORY_LIMIT
814
846
  ) : [];
815
847
  const messages = [...priorTurns, { role: "user", content: msg.text }];
816
- await config.history?.store.append(conversationId, { role: "user", content: msg.text });
848
+ if (historyEnabled) {
849
+ await config.history?.store.append(conversationId, { role: "user", content: msg.text });
850
+ }
817
851
  const { system } = await prepareChat({
818
852
  store: deps.store,
819
853
  prompts: deps.prompts,
@@ -821,7 +855,11 @@ function createInboundPipeline(deps, config) {
821
855
  messages,
822
856
  personaLayer,
823
857
  channelHint: config.channelHint,
824
- buckets
858
+ buckets,
859
+ sender,
860
+ rewriteQuery: config.rewriteQuery,
861
+ rerankContext: config.rerankContext,
862
+ logger: config.logger
825
863
  });
826
864
  const { content: produced } = await answerOnce({
827
865
  answerFn: config.answerFn,
@@ -841,7 +879,9 @@ function createInboundPipeline(deps, config) {
841
879
  if (!content) {
842
880
  return { action: "ignore" };
843
881
  }
844
- await config.history?.store.append(conversationId, { role: "assistant", content });
882
+ if (historyEnabled) {
883
+ await config.history?.store.append(conversationId, { role: "assistant", content });
884
+ }
845
885
  await turn.reply.sendAnswer(msg.chatId, content);
846
886
  return { action: "reply", content };
847
887
  };
@@ -948,6 +988,8 @@ function createWhatsAppInboundHandler(config) {
948
988
  channel: "whatsapp",
949
989
  answerFn: config.answerFn,
950
990
  bucketsFor: config.bucketsFor,
991
+ rewriteQuery: config.rewriteQuery,
992
+ rerankContext: config.rerankContext,
951
993
  transformReply: config.transformReply,
952
994
  model: config.model,
953
995
  channelHint: config.channelHint,
@@ -8,6 +8,7 @@
8
8
  * then ask for the answer via `answerOnce`/`answerStream` (see ./answer.ts)
9
9
  * so a caller-supplied brain is honoured.
10
10
  */
11
+ import type { Logger } from "./logger";
11
12
  import type { PromptLoader } from "./prompts";
12
13
  import type { VectorStore } from "./retrieval";
13
14
  export type PipelineMode = "public" | "private";
@@ -23,6 +24,42 @@ export interface PreparedChat {
23
24
  /** Raw retrieved chunks, before being folded into `system` — for callers that log or display RAG context alongside the answer */
24
25
  context: string[];
25
26
  }
27
+ /** What `rewriteQuery` sees for a turn. */
28
+ export interface RewriteQueryArgs {
29
+ /** The latest user message's content — what retrieval would search on unmodified. */
30
+ query: string;
31
+ mode: PipelineMode;
32
+ /** Caller identity, where the surface has one. Absent on anonymous surfaces. */
33
+ sender?: string;
34
+ }
35
+ /**
36
+ * Rewrites the retrieval query before it reaches the vector store — e.g.
37
+ * expanding an ambiguous follow-up ("what about the second one?") into
38
+ * something embeddable on its own. Return the query unchanged to opt out for
39
+ * a given turn. A throw/rejection is caught by `prepareChat`, which falls
40
+ * back to the original query — a broken rewrite must never break retrieval.
41
+ */
42
+ export type RewriteQuery = (ctx: RewriteQueryArgs) => string | Promise<string>;
43
+ /** What `rerankContext` sees for a turn. */
44
+ export interface RerankContextArgs {
45
+ /** The query retrieval actually ran with — post-`rewriteQuery`, if configured. */
46
+ query: string;
47
+ /** Chunks `store.query` returned, in its own order. */
48
+ chunks: string[];
49
+ }
50
+ /**
51
+ * Reorders, filters, or otherwise post-processes the chunks retrieval
52
+ * returned before they're folded into the system prompt. A throw/rejection
53
+ * is caught by `prepareChat`, which falls back to the original chunks in
54
+ * their original order — a broken reranker must never break retrieval.
55
+ *
56
+ * This is NOT the seam for access control. Because a failure falls back to
57
+ * the unfiltered chunks, a hook that drops chunks a sender shouldn't see
58
+ * would silently un-drop them on its own error — the opposite of safe
59
+ * failure. Scope decisions belong in `bucketsFor` (see ./buckets), which
60
+ * fails closed by design; use this hook for relevance/ordering only.
61
+ */
62
+ export type RerankContext = (ctx: RerankContextArgs) => string[] | Promise<string[]>;
26
63
  /**
27
64
  * Run retrieval for the latest user message and assemble the system prompt.
28
65
  *
@@ -40,9 +77,15 @@ export interface PreparedChat {
40
77
  * `resolveBuckets` (see ./buckets) first, which is where the anonymous
41
78
  * ceiling is enforced.
42
79
  *
80
+ * `rewriteQuery` and `rerankContext` shape retrieval itself rather than the
81
+ * prompt around it: the former runs before `store.query`, the latter after.
82
+ * Both are optional and fail open — a throw, rejection, or malformed return
83
+ * value falls back to what retrieval would have done unmodified, so a buggy
84
+ * hook degrades relevance rather than breaking the chat path.
85
+ *
43
86
  * @throws if the conversation contains no user message
44
87
  */
45
- export declare function prepareChat({ store, prompts, mode, messages, personaLayer, channelHint, buckets, }: {
88
+ export declare function prepareChat({ store, prompts, mode, messages, personaLayer, channelHint, buckets, sender, rewriteQuery, rerankContext, logger, }: {
46
89
  store: VectorStore;
47
90
  prompts: PromptLoader;
48
91
  mode: PipelineMode;
@@ -56,5 +99,13 @@ export declare function prepareChat({ store, prompts, mode, messages, personaLay
56
99
  * `["base", mode]`. An empty array retrieves nothing.
57
100
  */
58
101
  buckets?: string[];
102
+ /** Caller identity, passed through to `rewriteQuery`. Absent on anonymous surfaces. */
103
+ sender?: string;
104
+ /** Rewrites the retrieval query before `store.query` runs — see {@link RewriteQuery}. */
105
+ rewriteQuery?: RewriteQuery;
106
+ /** Post-processes retrieved chunks before they're folded into the prompt — see {@link RerankContext}. */
107
+ rerankContext?: RerankContext;
108
+ /** Logs a `rewriteQuery`/`rerankContext` throw before falling back. Unset: the failure is silent. */
109
+ logger?: Pick<Logger, "error">;
59
110
  }): Promise<PreparedChat>;
60
111
  //# sourceMappingURL=pipeline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/core/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,iIAAiI;IACjI,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAOD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,WAAW,CAAC,EAChC,KAAK,EACL,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,OAAO,GACR,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB,GAAG,OAAO,CAAC,YAAY,CAAC,CAmBxB"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/core/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAEvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEhD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,iIAAiI;IACjI,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,2CAA2C;AAC3C,MAAM,WAAW,gBAAgB;IAC/B,qFAAqF;IACrF,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,gBAAgB,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAE/E,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAOrF;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,WAAW,CAAC,EAChC,KAAK,EACL,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,OAAO,EACP,MAAM,EACN,YAAY,EACZ,aAAa,EACb,MAAM,GACP,EAAE;IACD,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,YAAY,CAAC;IACtB,IAAI,EAAE,YAAY,CAAC;IACnB,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kEAAkE;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,uFAAuF;IACvF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yFAAyF;IACzF,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,yGAAyG;IACzG,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,qGAAqG;IACrG,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,GAAG,OAAO,CAAC,YAAY,CAAC,CAuCxB"}
@@ -12,6 +12,7 @@
12
12
  *
13
13
  * @packageDocumentation
14
14
  */
15
+ export type { TursoHistoryStoreOptions } from "./tursoStore";
15
16
  export { createTursoHistoryStore } from "./tursoStore";
16
17
  export type { HistoryMessage, HistoryStore } from "./types";
17
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/history/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,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;;;;;;;;;;;;;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"}
@@ -58,19 +58,32 @@ function ensureHistorySchema(client, tableName) {
58
58
  var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
59
59
  var VALID_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
60
60
  var DEFAULT_MAX_PER_CONVERSATION = 200;
61
- function createTursoHistoryStore(client, tableName = "chat_history", maxPerConversation = DEFAULT_MAX_PER_CONVERSATION) {
61
+ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConversation = DEFAULT_MAX_PER_CONVERSATION, options) {
62
62
  if (!VALID_TABLE_NAME.test(tableName)) {
63
63
  throw new Error(`Invalid history store table name: ${tableName}`);
64
64
  }
65
65
  if (!Number.isInteger(maxPerConversation) || maxPerConversation < 1) {
66
66
  throw new Error(`Invalid history store maxPerConversation: ${maxPerConversation}`);
67
67
  }
68
+ if (options?.ttlMs !== void 0 && (!Number.isInteger(options.ttlMs) || options.ttlMs < 1)) {
69
+ throw new Error(`Invalid history store ttlMs: ${options.ttlMs}`);
70
+ }
71
+ const ttlMs = options?.ttlMs;
72
+ const now = options?.now ?? Date.now;
73
+ async function pruneExpired(conversationId) {
74
+ if (ttlMs === void 0) return;
75
+ await client.execute({
76
+ sql: `DELETE FROM ${tableName} WHERE conversation_id = ? AND created_at < ?`,
77
+ args: [conversationId, now() - ttlMs]
78
+ });
79
+ }
68
80
  return {
69
81
  async append(conversationId, message) {
70
82
  await ensureHistorySchema(client, tableName);
83
+ await pruneExpired(conversationId);
71
84
  await client.execute({
72
85
  sql: `INSERT INTO ${tableName} (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)`,
73
- args: [conversationId, message.role, message.content, Date.now()]
86
+ args: [conversationId, message.role, message.content, now()]
74
87
  });
75
88
  await client.execute({
76
89
  sql: `DELETE FROM ${tableName} WHERE conversation_id = ? AND id NOT IN (
@@ -83,6 +96,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
83
96
  const boundedLimit = Math.max(0, Math.trunc(limit));
84
97
  if (boundedLimit === 0) return [];
85
98
  await ensureHistorySchema(client, tableName);
99
+ await pruneExpired(conversationId);
86
100
  const result = await client.execute({
87
101
  sql: `SELECT role, content FROM ${tableName} WHERE conversation_id = ? ORDER BY id DESC LIMIT ?`,
88
102
  args: [conversationId, boundedLimit]
@@ -91,6 +105,13 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
91
105
  role: String(row.role),
92
106
  content: String(row.content)
93
107
  })).filter((row) => VALID_ROLES.has(row.role)).reverse();
108
+ },
109
+ async clear(conversationId) {
110
+ await ensureHistorySchema(client, tableName);
111
+ await client.execute({
112
+ sql: `DELETE FROM ${tableName} WHERE conversation_id = ?`,
113
+ args: [conversationId]
114
+ });
94
115
  }
95
116
  };
96
117
  }
@@ -32,19 +32,32 @@ function ensureHistorySchema(client, tableName) {
32
32
  var VALID_TABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
33
33
  var VALID_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
34
34
  var DEFAULT_MAX_PER_CONVERSATION = 200;
35
- function createTursoHistoryStore(client, tableName = "chat_history", maxPerConversation = DEFAULT_MAX_PER_CONVERSATION) {
35
+ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConversation = DEFAULT_MAX_PER_CONVERSATION, options) {
36
36
  if (!VALID_TABLE_NAME.test(tableName)) {
37
37
  throw new Error(`Invalid history store table name: ${tableName}`);
38
38
  }
39
39
  if (!Number.isInteger(maxPerConversation) || maxPerConversation < 1) {
40
40
  throw new Error(`Invalid history store maxPerConversation: ${maxPerConversation}`);
41
41
  }
42
+ if (options?.ttlMs !== void 0 && (!Number.isInteger(options.ttlMs) || options.ttlMs < 1)) {
43
+ throw new Error(`Invalid history store ttlMs: ${options.ttlMs}`);
44
+ }
45
+ const ttlMs = options?.ttlMs;
46
+ const now = options?.now ?? Date.now;
47
+ async function pruneExpired(conversationId) {
48
+ if (ttlMs === void 0) return;
49
+ await client.execute({
50
+ sql: `DELETE FROM ${tableName} WHERE conversation_id = ? AND created_at < ?`,
51
+ args: [conversationId, now() - ttlMs]
52
+ });
53
+ }
42
54
  return {
43
55
  async append(conversationId, message) {
44
56
  await ensureHistorySchema(client, tableName);
57
+ await pruneExpired(conversationId);
45
58
  await client.execute({
46
59
  sql: `INSERT INTO ${tableName} (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)`,
47
- args: [conversationId, message.role, message.content, Date.now()]
60
+ args: [conversationId, message.role, message.content, now()]
48
61
  });
49
62
  await client.execute({
50
63
  sql: `DELETE FROM ${tableName} WHERE conversation_id = ? AND id NOT IN (
@@ -57,6 +70,7 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
57
70
  const boundedLimit = Math.max(0, Math.trunc(limit));
58
71
  if (boundedLimit === 0) return [];
59
72
  await ensureHistorySchema(client, tableName);
73
+ await pruneExpired(conversationId);
60
74
  const result = await client.execute({
61
75
  sql: `SELECT role, content FROM ${tableName} WHERE conversation_id = ? ORDER BY id DESC LIMIT ?`,
62
76
  args: [conversationId, boundedLimit]
@@ -65,6 +79,13 @@ function createTursoHistoryStore(client, tableName = "chat_history", maxPerConve
65
79
  role: String(row.role),
66
80
  content: String(row.content)
67
81
  })).filter((row) => VALID_ROLES.has(row.role)).reverse();
82
+ },
83
+ async clear(conversationId) {
84
+ await ensureHistorySchema(client, tableName);
85
+ await client.execute({
86
+ sql: `DELETE FROM ${tableName} WHERE conversation_id = ?`,
87
+ args: [conversationId]
88
+ });
68
89
  }
69
90
  };
70
91
  }
@@ -10,6 +10,18 @@
10
10
  */
11
11
  import type { Client } from "@libsql/client";
12
12
  import type { HistoryStore } from "./types";
13
+ export interface TursoHistoryStoreOptions {
14
+ /**
15
+ * Retention window: rows older than this are pruned whenever the
16
+ * conversation they belong to is accessed (`append` or `load`) — there is
17
+ * no background sweep. Unset: rows never expire on their own (only the
18
+ * `maxPerConversation` count cap and explicit `clear` remove them). Must be
19
+ * a positive integer, validated at construction like `maxPerConversation`.
20
+ */
21
+ ttlMs?: number;
22
+ /** Injectable clock for tests. @default Date.now */
23
+ now?: () => number;
24
+ }
13
25
  /**
14
26
  * Creates a history store keyed by an arbitrary conversation id (a WhatsApp
15
27
  * chat jid, a phone call sid, a web session id) on `tableName`.
@@ -22,5 +34,5 @@ import type { HistoryStore } from "./types";
22
34
  * or disable pruning entirely (SQLite reads a negative `LIMIT` as
23
35
  * unlimited), so it is validated at construction the same way `tableName` is.
24
36
  */
25
- export declare function createTursoHistoryStore(client: Client, tableName?: string, maxPerConversation?: number): HistoryStore;
37
+ export declare function createTursoHistoryStore(client: Client, tableName?: string, maxPerConversation?: number, options?: TursoHistoryStoreOptions): HistoryStore;
26
38
  //# sourceMappingURL=tursoStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tursoStore.d.ts","sourceRoot":"","sources":["../../src/history/tursoStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,SAAS,CAAC;AAkD5D;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,SAAS,SAAiB,EAC1B,kBAAkB,SAA+B,GAChD,YAAY,CAkDd"}
1
+ {"version":3,"file":"tursoStore.d.ts","sourceRoot":"","sources":["../../src/history/tursoStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,KAAK,EAAkB,YAAY,EAAE,MAAM,SAAS,CAAC;AAkD5D,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,SAAS,SAAiB,EAC1B,kBAAkB,SAA+B,EACjD,OAAO,CAAC,EAAE,wBAAwB,GACjC,YAAY,CAyEd"}
@@ -14,5 +14,7 @@ export interface HistoryStore {
14
14
  append(conversationId: string, message: HistoryMessage): Promise<void>;
15
15
  /** Load up to `limit` most recent turns for `conversationId`, oldest first. */
16
16
  load(conversationId: string, limit: number): Promise<HistoryMessage[]>;
17
+ /** Reset primitive: erases all of `conversationId`'s history. Other conversations are untouched. */
18
+ clear(conversationId: string): Promise<void>;
17
19
  }
18
20
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/history/types.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,+EAA+E;IAC/E,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;CACxE"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/history/types.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,qDAAqD;IACrD,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,+EAA+E;IAC/E,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IACvE,oGAAoG;IACpG,KAAK,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C"}
package/dist/index.d.ts CHANGED
@@ -25,7 +25,7 @@ export type { Logger, LogLevel } from "./core/logger";
25
25
  export { createConsoleLogger } from "./core/logger";
26
26
  export type { NormalizedChatBody } from "./core/messages";
27
27
  export { normalizeChatBody, normalizeMessages } from "./core/messages";
28
- export type { PipelineMessage, PipelineMode, PreparedChat } from "./core/pipeline";
28
+ export type { PipelineMessage, PipelineMode, PreparedChat, RerankContext, RerankContextArgs, RewriteQuery, RewriteQueryArgs, } from "./core/pipeline";
29
29
  export { prepareChat } from "./core/pipeline";
30
30
  export { PromptLoader } from "./core/prompts";
31
31
  export type { VectorStoreOptions } from "./core/retrieval";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -458,14 +458,35 @@ async function prepareChat({
458
458
  messages,
459
459
  personaLayer,
460
460
  channelHint,
461
- buckets
461
+ buckets,
462
+ sender,
463
+ rewriteQuery,
464
+ rerankContext,
465
+ logger
462
466
  }) {
463
467
  const lastUserMsg = lastUserMessage(messages);
464
468
  if (!lastUserMsg) {
465
469
  throw new Error("no user message found in conversation");
466
470
  }
467
471
  const { topK, label } = MODE_SETTINGS[mode];
468
- const ctx = await store.query(lastUserMsg.content, topK, buckets ?? defaultBuckets(mode));
472
+ let query = lastUserMsg.content;
473
+ if (rewriteQuery) {
474
+ try {
475
+ const rewritten = await rewriteQuery({ query, mode, sender });
476
+ if (typeof rewritten === "string" && rewritten.trim()) query = rewritten;
477
+ } catch (error) {
478
+ logger?.error("rewriteQuery threw; falling back to the original query", error);
479
+ }
480
+ }
481
+ let ctx = await store.query(query, topK, buckets ?? defaultBuckets(mode));
482
+ if (rerankContext) {
483
+ try {
484
+ const reranked = await rerankContext({ query, chunks: ctx });
485
+ if (Array.isArray(reranked) && reranked.every((c) => typeof c === "string")) ctx = reranked;
486
+ } catch (error) {
487
+ logger?.error("rerankContext threw; falling back to the original chunks", error);
488
+ }
489
+ }
469
490
  const persona = personaLayer?.trim() || (mode === "private" ? prompts.privatePersona : prompts.publicPersona);
470
491
  const hint = channelHint?.trim();
471
492
  const system = [
@@ -1004,7 +1025,10 @@ async function createMCPServer(config) {
1004
1025
  prompts,
1005
1026
  mode: "public",
1006
1027
  messages: conversationMessages,
1007
- buckets
1028
+ buckets,
1029
+ rewriteQuery: config.rewriteQuery,
1030
+ rerankContext: config.rerankContext,
1031
+ logger: log
1008
1032
  });
1009
1033
  const result = await answerOnce({
1010
1034
  answerFn: config.answerFn,
@@ -1090,7 +1114,10 @@ async function createMCPServer(config) {
1090
1114
  prompts,
1091
1115
  mode: "private",
1092
1116
  messages: conversationMessages,
1093
- buckets
1117
+ buckets,
1118
+ rewriteQuery: config.rewriteQuery,
1119
+ rerankContext: config.rerankContext,
1120
+ logger: log
1094
1121
  });
1095
1122
  const result = await answerOnce({
1096
1123
  answerFn: config.answerFn,
@@ -1565,7 +1592,16 @@ function demoRoutes(deps) {
1565
1592
  return c.json({ error: "no user message found in conversation" }, 400);
1566
1593
  }
1567
1594
  const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor });
1568
- const { system } = await prepareChat({ store, prompts, mode: "public", messages, buckets });
1595
+ const { system } = await prepareChat({
1596
+ store,
1597
+ prompts,
1598
+ mode: "public",
1599
+ messages,
1600
+ buckets,
1601
+ rewriteQuery: config.rewriteQuery,
1602
+ rerankContext: config.rerankContext,
1603
+ logger
1604
+ });
1569
1605
  const model = config.openai.model || DEFAULT_MODEL;
1570
1606
  const url = new URL(c.req.url);
1571
1607
  const wantsStream3 = url.searchParams.get("stream") === "1";
@@ -1684,13 +1720,23 @@ function openaiRoutes(deps) {
1684
1720
  sender: jwtSubject(c),
1685
1721
  bucketsFor: config.bucketsFor
1686
1722
  });
1723
+ const sender = jwtSubject(c) ?? apiKeySubject(c);
1687
1724
  let system;
1688
1725
  try {
1689
- ({ system } = await prepareChat({ store, prompts, mode, messages, buckets }));
1726
+ ({ system } = await prepareChat({
1727
+ store,
1728
+ prompts,
1729
+ mode,
1730
+ messages,
1731
+ buckets,
1732
+ sender,
1733
+ rewriteQuery: config.rewriteQuery,
1734
+ rerankContext: config.rerankContext,
1735
+ logger
1736
+ }));
1690
1737
  } catch {
1691
1738
  return errorJson(c, 400, "Conversation must contain at least one user message");
1692
1739
  }
1693
- const sender = jwtSubject(c) ?? apiKeySubject(c);
1694
1740
  const conversationId = clientConversationId(c, body) ?? crypto.randomUUID();
1695
1741
  c.header("x-conversation-id", conversationId);
1696
1742
  const id = `chatcmpl-${crypto.randomUUID()}`;
@@ -1800,7 +1846,17 @@ function privateRoutes(deps) {
1800
1846
  });
1801
1847
  let system;
1802
1848
  try {
1803
- ({ system } = await prepareChat({ store, prompts, mode: "private", messages, buckets }));
1849
+ ({ system } = await prepareChat({
1850
+ store,
1851
+ prompts,
1852
+ mode: "private",
1853
+ messages,
1854
+ buckets,
1855
+ sender,
1856
+ rewriteQuery: config.rewriteQuery,
1857
+ rerankContext: config.rerankContext,
1858
+ logger
1859
+ }));
1804
1860
  } catch {
1805
1861
  return c.json({ error: "no user message found in conversation" }, 400);
1806
1862
  }
@@ -1876,7 +1932,16 @@ function publicRoutes(deps) {
1876
1932
  const buckets = await resolveBuckets({ mode: "public", bucketsFor: config.bucketsFor });
1877
1933
  let system;
1878
1934
  try {
1879
- ({ system } = await prepareChat({ store, prompts, mode: "public", messages, buckets }));
1935
+ ({ system } = await prepareChat({
1936
+ store,
1937
+ prompts,
1938
+ mode: "public",
1939
+ messages,
1940
+ buckets,
1941
+ rewriteQuery: config.rewriteQuery,
1942
+ rerankContext: config.rerankContext,
1943
+ logger
1944
+ }));
1880
1945
  } catch {
1881
1946
  return c.json({ error: "no user message found in conversation" }, 400);
1882
1947
  }