@diegoaltoworks/chatter 1.0.0 → 2.1.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 (77) hide show
  1. package/README.md +3 -1
  2. package/dist/channels/gates.d.ts +21 -2
  3. package/dist/channels/gates.d.ts.map +1 -1
  4. package/dist/channels/index.d.ts +5 -3
  5. package/dist/channels/index.d.ts.map +1 -1
  6. package/dist/channels/index.js +31 -13
  7. package/dist/channels/index.mjs +30 -13
  8. package/dist/channels/matrix/channel.d.ts +2 -0
  9. package/dist/channels/matrix/channel.d.ts.map +1 -1
  10. package/dist/channels/matrix/handler.d.ts +9 -0
  11. package/dist/channels/matrix/handler.d.ts.map +1 -1
  12. package/dist/channels/matrix/index.d.ts +1 -1
  13. package/dist/channels/matrix/index.d.ts.map +1 -1
  14. package/dist/channels/matrix/index.js +75 -20
  15. package/dist/channels/matrix/index.mjs +74 -20
  16. package/dist/channels/matrix/updates.d.ts +8 -2
  17. package/dist/channels/matrix/updates.d.ts.map +1 -1
  18. package/dist/channels/pipeline.d.ts +34 -4
  19. package/dist/channels/pipeline.d.ts.map +1 -1
  20. package/dist/channels/polling.d.ts +11 -0
  21. package/dist/channels/polling.d.ts.map +1 -1
  22. package/dist/channels/telegram/channel.d.ts +2 -0
  23. package/dist/channels/telegram/channel.d.ts.map +1 -1
  24. package/dist/channels/telegram/handler.d.ts +9 -0
  25. package/dist/channels/telegram/handler.d.ts.map +1 -1
  26. package/dist/channels/telegram/index.d.ts +1 -1
  27. package/dist/channels/telegram/index.d.ts.map +1 -1
  28. package/dist/channels/telegram/index.js +73 -24
  29. package/dist/channels/telegram/index.mjs +72 -24
  30. package/dist/channels/telegram/updates.d.ts +8 -2
  31. package/dist/channels/telegram/updates.d.ts.map +1 -1
  32. package/dist/channels/telegram/webhook.d.ts +10 -0
  33. package/dist/channels/telegram/webhook.d.ts.map +1 -1
  34. package/dist/channels/whatsapp/channel.d.ts +8 -0
  35. package/dist/channels/whatsapp/channel.d.ts.map +1 -1
  36. package/dist/channels/whatsapp/inbound.d.ts +2 -0
  37. package/dist/channels/whatsapp/inbound.d.ts.map +1 -1
  38. package/dist/channels/whatsapp/index.js +45 -18
  39. package/dist/channels/whatsapp/index.mjs +45 -18
  40. package/dist/core/answer.d.ts +9 -2
  41. package/dist/core/answer.d.ts.map +1 -1
  42. package/dist/core/chunking.d.ts +31 -0
  43. package/dist/core/chunking.d.ts.map +1 -0
  44. package/dist/core/guardrails.d.ts +11 -1
  45. package/dist/core/guardrails.d.ts.map +1 -1
  46. package/dist/core/llm.d.ts +3 -1
  47. package/dist/core/llm.d.ts.map +1 -1
  48. package/dist/core/retrieval.d.ts +39 -0
  49. package/dist/core/retrieval.d.ts.map +1 -1
  50. package/dist/history/index.js +12 -9
  51. package/dist/history/index.mjs +12 -9
  52. package/dist/index.d.ts +4 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +261 -52
  55. package/dist/index.mjs +254 -47
  56. package/dist/mcp-server.d.ts.map +1 -1
  57. package/dist/mcp-server.js +217 -30
  58. package/dist/mcp-server.mjs +216 -29
  59. package/dist/personas/index.d.ts +1 -1
  60. package/dist/personas/index.d.ts.map +1 -1
  61. package/dist/personas/index.js +21 -7
  62. package/dist/personas/index.mjs +21 -7
  63. package/dist/personas/resolver.d.ts +35 -8
  64. package/dist/personas/resolver.d.ts.map +1 -1
  65. package/dist/personas/timeContext.d.ts.map +1 -1
  66. package/dist/routes/demo.d.ts.map +1 -1
  67. package/dist/routes/openai.d.ts.map +1 -1
  68. package/dist/routes/private.d.ts.map +1 -1
  69. package/dist/routes/public.d.ts.map +1 -1
  70. package/dist/scheduler/index.js +12 -9
  71. package/dist/scheduler/index.mjs +12 -9
  72. package/dist/server.d.ts.map +1 -1
  73. package/dist/server.js +251 -50
  74. package/dist/server.mjs +246 -45
  75. package/dist/types.d.ts +39 -1
  76. package/dist/types.d.ts.map +1 -1
  77. package/package.json +13 -13
@@ -92,6 +92,126 @@ var init_buildLock = __esm({
92
92
  }
93
93
  });
94
94
 
95
+ // src/core/chunking.ts
96
+ function headingOf(line) {
97
+ const match = HEADING.exec(line);
98
+ if (!match) return void 0;
99
+ const title = (match[2] ?? "").replace(/[ \t]+#+$/, "").replace(/^#+$/, "").trim();
100
+ return { level: match[1].length, title };
101
+ }
102
+ function startsTable(lines, i) {
103
+ return lines[i].includes("|") && i + 1 < lines.length && TABLE_DELIMITER.test(lines[i + 1]);
104
+ }
105
+ function fenceEnd(lines, start) {
106
+ const opener = FENCE.exec(lines[start]);
107
+ if (!opener) return start + 1;
108
+ const marker = opener[1];
109
+ const closer = new RegExp(`^ {0,3}${marker[0] === "`" ? "`" : "~"}{${marker.length},}[ \\t]*$`);
110
+ for (let i = start + 1; i < lines.length; i++) if (closer.test(lines[i])) return i + 1;
111
+ return lines.length;
112
+ }
113
+ function listEnd(lines, start) {
114
+ let end = start + 1;
115
+ for (let i = start + 1; i < lines.length; i++) {
116
+ const line = lines[i];
117
+ if (isBlank(line)) {
118
+ let next = i + 1;
119
+ while (next < lines.length && isBlank(lines[next])) next++;
120
+ if (next < lines.length && (LIST_ITEM.test(lines[next]) || isIndented(lines[next]))) {
121
+ i = next - 1;
122
+ continue;
123
+ }
124
+ break;
125
+ }
126
+ if (headingOf(line) && !isIndented(line)) break;
127
+ if (FENCE.test(line) && !isIndented(line)) break;
128
+ end = i + 1;
129
+ }
130
+ return end;
131
+ }
132
+ function tableEnd(lines, start) {
133
+ let end = start + 2;
134
+ while (end < lines.length && !isBlank(lines[end]) && lines[end].includes("|")) end++;
135
+ return end;
136
+ }
137
+ function paragraphEnd(lines, start) {
138
+ let end = start + 1;
139
+ while (end < lines.length && !isBlank(lines[end]) && !headingOf(lines[end]) && !FENCE.test(lines[end]) && !LIST_ITEM.test(lines[end])) {
140
+ end++;
141
+ }
142
+ return end;
143
+ }
144
+ function scan(text) {
145
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
146
+ const sections = [{ trail: [], blocks: [] }];
147
+ const stack = [];
148
+ let i = 0;
149
+ while (i < lines.length) {
150
+ const line = lines[i];
151
+ if (isBlank(line)) {
152
+ i++;
153
+ continue;
154
+ }
155
+ const heading = FENCE.test(line) ? void 0 : headingOf(line);
156
+ if (heading) {
157
+ while (stack.length && stack[stack.length - 1].level >= heading.level) stack.pop();
158
+ stack.push(heading);
159
+ sections.push({ trail: stack.map((h) => h.title), heading: line, blocks: [] });
160
+ i++;
161
+ continue;
162
+ }
163
+ let end;
164
+ if (FENCE.test(line)) end = fenceEnd(lines, i);
165
+ else if (LIST_ITEM.test(line)) end = listEnd(lines, i);
166
+ else if (startsTable(lines, i)) end = tableEnd(lines, i);
167
+ else end = paragraphEnd(lines, i);
168
+ sections[sections.length - 1].blocks.push({ lines: lines.slice(i, end) });
169
+ i = end;
170
+ }
171
+ return sections;
172
+ }
173
+ function chunkSections(text, max = 900) {
174
+ const cap = Math.max(1, max);
175
+ const out = [];
176
+ const emit = (parts, section) => {
177
+ const body = parts.join("\n\n");
178
+ if (body.trim() === "") return;
179
+ out.push({ text: body, section: [...section], position: out.length });
180
+ };
181
+ for (const section of scan(text)) {
182
+ let parts = section.heading ? [section.heading.trim()] : [];
183
+ let size = parts.join("\n\n").length;
184
+ let hasBlock = false;
185
+ for (const block of section.blocks) {
186
+ const piece = block.lines.join("\n");
187
+ const added = piece.length + (parts.length ? 2 : 0);
188
+ if (hasBlock && size + added > cap) {
189
+ emit(parts, section.trail);
190
+ parts = [];
191
+ size = 0;
192
+ hasBlock = false;
193
+ }
194
+ size += piece.length + (parts.length ? 2 : 0);
195
+ parts.push(piece);
196
+ hasBlock = true;
197
+ }
198
+ emit(parts, section.trail);
199
+ }
200
+ return out;
201
+ }
202
+ var FENCE, HEADING, LIST_ITEM, TABLE_DELIMITER, isBlank, isIndented;
203
+ var init_chunking = __esm({
204
+ "src/core/chunking.ts"() {
205
+ "use strict";
206
+ FENCE = /^ {0,3}(`{3,}|~{3,})/;
207
+ HEADING = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/;
208
+ LIST_ITEM = /^[ \t]*(?:[-*+]|\d{1,9}[.)])[ \t]+\S/;
209
+ TABLE_DELIMITER = /^[ \t]*\|?[ \t]*:?-+:?[ \t]*(?:\|[ \t]*:?-+:?[ \t]*)*\|?[ \t]*$/;
210
+ isBlank = (line) => line.trim() === "";
211
+ isIndented = (line) => /^(?: {2,}|\t)/.test(line);
212
+ }
213
+ });
214
+
95
215
  // src/core/loaders.ts
96
216
  import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
97
217
  import { join as join2 } from "node:path";
@@ -134,6 +254,7 @@ __export(retrieval_exports, {
134
254
  openLibsqlClient: () => openLibsqlClient,
135
255
  wrapMissingLibsqlError: () => wrapMissingLibsqlError
136
256
  });
257
+ import { relative, sep } from "node:path";
137
258
  function createOpenAIEmbedder(client) {
138
259
  return async (input) => {
139
260
  const res = await client.embeddings.create({ model: EMB_MODEL, input });
@@ -176,6 +297,7 @@ var init_retrieval = __esm({
176
297
  "src/core/retrieval.ts"() {
177
298
  "use strict";
178
299
  init_buildLock();
300
+ init_chunking();
179
301
  init_loaders();
180
302
  init_logger();
181
303
  EMB_MODEL = "text-embedding-3-large";
@@ -186,6 +308,7 @@ var init_retrieval = __esm({
186
308
  this.embed = embed;
187
309
  this.db = options.databaseClient;
188
310
  this.knowledgeDir = options.knowledgeDir || DEFAULT_KNOWLEDGE_DIR;
311
+ this.chunking = options.chunking ?? "lines";
189
312
  this.logger = options.logger ?? createConsoleLogger();
190
313
  this.buildLock = options.buildLock ?? createTursoBuildLock(this.db);
191
314
  this.instanceId = options.instanceId ?? crypto.randomUUID();
@@ -199,6 +322,7 @@ var init_retrieval = __esm({
199
322
  */
200
323
  db;
201
324
  knowledgeDir;
325
+ chunking;
202
326
  logger;
203
327
  buildLock;
204
328
  instanceId;
@@ -259,11 +383,41 @@ var init_retrieval = __esm({
259
383
  this.logger.info("No knowledge documents and no existing chunks - nothing to build");
260
384
  return;
261
385
  }
386
+ this.logger.info(`Chunking mode: ${this.chunking}`);
387
+ if (this.chunking === "sections") await this.ensureSectionColumns();
262
388
  const rows = [];
263
389
  for (const d of docs) {
264
- for (const part of chunk(d.text)) {
265
- const id = await sha256(`${d.bucket}|${d.source}|${part}`);
266
- rows.push({ id, bucket: d.bucket, source: d.source, text: part });
390
+ if (this.chunking === "sections") {
391
+ const source = relative(this.knowledgeDir, d.source).split(sep).join("/");
392
+ for (const c of chunkSections(d.text)) {
393
+ const section = c.section.join(" > ");
394
+ const id = await sha256(`${d.bucket}|${source}|${section}|${c.text}`);
395
+ const context = section ? `${source} > ${section}` : source;
396
+ rows.push({
397
+ id,
398
+ bucket: d.bucket,
399
+ source,
400
+ text: c.text,
401
+ embedText: `${context}
402
+
403
+ ${c.text}`,
404
+ section,
405
+ position: c.position
406
+ });
407
+ }
408
+ } else {
409
+ for (const part of chunk(d.text)) {
410
+ const id = await sha256(`${d.bucket}|${d.source}|${part}`);
411
+ rows.push({
412
+ id,
413
+ bucket: d.bucket,
414
+ source: d.source,
415
+ text: part,
416
+ embedText: part,
417
+ section: null,
418
+ position: null
419
+ });
420
+ }
267
421
  }
268
422
  }
269
423
  this.logger.info(`Created ${rows.length} chunks from knowledge documents`);
@@ -272,11 +426,17 @@ var init_retrieval = __esm({
272
426
  for (let i = 0; i < rows.length; i += UPSERT_BATCH) {
273
427
  const batch = rows.slice(i, i + UPSERT_BATCH);
274
428
  await this.db.batch(
275
- batch.map((r) => ({
276
- sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
277
- ON CONFLICT(id) DO NOTHING`,
278
- args: [r.id, r.bucket, r.source, r.text]
279
- })),
429
+ batch.map(
430
+ (r) => this.chunking === "sections" ? {
431
+ sql: `INSERT INTO chunks(id,bucket,source,text,section,position) VALUES(?,?,?,?,?,?)
432
+ ON CONFLICT(id) DO NOTHING`,
433
+ args: [r.id, r.bucket, r.source, r.text, r.section, r.position]
434
+ } : {
435
+ sql: `INSERT INTO chunks(id,bucket,source,text) VALUES(?,?,?,?)
436
+ ON CONFLICT(id) DO NOTHING`,
437
+ args: [r.id, r.bucket, r.source, r.text]
438
+ }
439
+ ),
280
440
  "write"
281
441
  );
282
442
  }
@@ -297,7 +457,7 @@ var init_retrieval = __esm({
297
457
  return;
298
458
  }
299
459
  this.logger.info(`Embedding ${missing.length} new/updated chunks with ${EMB_MODEL}...`);
300
- const textById = new Map(rows.map((r) => [r.id, r.text]));
460
+ const textById = new Map(rows.map((r) => [r.id, r.embedText]));
301
461
  const BATCH = 96;
302
462
  for (let i = 0; i < missing.length; i += BATCH) {
303
463
  const batchIds = missing.slice(i, i + BATCH);
@@ -318,6 +478,18 @@ var init_retrieval = __esm({
318
478
  }
319
479
  this.logger.info(`Successfully embedded ${missing.length} new chunks`);
320
480
  }
481
+ /**
482
+ * Adds the nullable `section`/`position` columns to a `chunks` table that
483
+ * every released version created without them. Checked via table_info so it
484
+ * is a no-op once present; only `'sections'` mode ever calls it.
485
+ */
486
+ async ensureSectionColumns() {
487
+ const info = await this.db.execute("PRAGMA table_info(chunks)");
488
+ const have = new Set(info.rows.map((r) => String(r.name)));
489
+ if (!have.has("section")) await this.db.execute("ALTER TABLE chunks ADD COLUMN section TEXT");
490
+ if (!have.has("position"))
491
+ await this.db.execute("ALTER TABLE chunks ADD COLUMN position INTEGER");
492
+ }
321
493
  // Remove chunks from database that no longer exist in markdown files
322
494
  async cleanupStaleChunks(currentIds) {
323
495
  const result = await this.db.execute("SELECT id FROM chunks");
@@ -364,25 +536,34 @@ var init_retrieval = __esm({
364
536
  * interpolated. An empty list retrieves nothing, and short-circuits before
365
537
  * the embedding call.
366
538
  */
367
- async query(q, k = 6, allowed = ["base"]) {
539
+ async queryChunks(q, k = 6, allowed = ["base"]) {
368
540
  if (allowed.length === 0) return [];
369
541
  const [qv] = await this.embed([q]);
370
542
  const placeholders = allowed.map(() => "?").join(",");
371
543
  const res = await this.db.execute({
372
- sql: `SELECT c.id, c.text, e.embedding
544
+ sql: `SELECT c.*, e.embedding
373
545
  FROM chunks c
374
546
  JOIN embeddings e ON e.id = c.id
375
547
  WHERE c.bucket IN (${placeholders})`,
376
548
  args: allowed
377
549
  });
378
- const scored = [];
379
- for (const row of res.rows) {
550
+ const scored = res.rows.map((row) => {
380
551
  const emb = JSON.parse(String(row.embedding));
381
- const s = _VectorStore.cosine(qv, emb);
382
- scored.push({ s, text: String(row.text) });
383
- }
384
- scored.sort((a, b) => b.s - a.s);
385
- return scored.slice(0, k).map((r) => r.text);
552
+ const trail = row.section == null ? "" : String(row.section);
553
+ return {
554
+ text: String(row.text),
555
+ bucket: String(row.bucket ?? ""),
556
+ source: String(row.source ?? ""),
557
+ section: trail ? trail.split(" > ") : [],
558
+ score: _VectorStore.cosine(qv, emb)
559
+ };
560
+ });
561
+ scored.sort((a, b) => b.score - a.score);
562
+ return scored.slice(0, k);
563
+ }
564
+ /** Same results as {@link queryChunks}, as bare text. */
565
+ async query(q, k = 6, allowed = ["base"]) {
566
+ return (await this.queryChunks(q, k, allowed)).map((c) => c.text);
386
567
  }
387
568
  };
388
569
  }
@@ -518,9 +699,10 @@ function detectLeakage(text) {
518
699
  function scrubOutput(text) {
519
700
  return text.replace(/sk-[A-Za-z0-9]{15,}/g, "[REDACTED]").replace(/AIza[0-9A-Za-z_-]{30,}/g, "[REDACTED]");
520
701
  }
521
- function guardOutput(text) {
702
+ var DEFAULT_REFUSAL = "Sorry, I can't share internal instructions. How else can I help?";
703
+ function guardOutput(text, refusal) {
522
704
  if (detectLeakage(text)) {
523
- return "Sorry, I can't share internal instructions. How else can I help?";
705
+ return typeof refusal === "string" && refusal.trim() ? refusal : DEFAULT_REFUSAL;
524
706
  }
525
707
  return scrubOutput(text);
526
708
  }
@@ -532,7 +714,8 @@ async function completeOnce({
532
714
  system,
533
715
  messages,
534
716
  temperature = 0.2,
535
- model = DEFAULT_MODEL
717
+ model = DEFAULT_MODEL,
718
+ refusal
536
719
  }) {
537
720
  const res = await client.chat.completions.create({
538
721
  model,
@@ -541,19 +724,19 @@ async function completeOnce({
541
724
  });
542
725
  const text = res.choices[0]?.message?.content ?? "";
543
726
  return {
544
- content: guardOutput(text),
727
+ content: guardOutput(text, refusal),
545
728
  usage: res.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
546
729
  };
547
730
  }
548
731
 
549
732
  // src/core/answer.ts
550
733
  var noUsage = () => ({ prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 });
551
- async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }) {
734
+ async function runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal) {
552
735
  const result = await answerFn({ system, messages, mode, sender, conversationId });
553
736
  const raw = typeof result === "string" ? result : result?.content;
554
737
  const usage = typeof result === "string" ? void 0 : result?.usage;
555
738
  return {
556
- content: guardOutput(typeof raw === "string" ? raw : ""),
739
+ content: guardOutput(typeof raw === "string" ? raw : "", refusal),
557
740
  usage: usage ?? noUsage()
558
741
  };
559
742
  }
@@ -566,12 +749,13 @@ async function answerOnce({
566
749
  sender,
567
750
  conversationId,
568
751
  temperature,
569
- model
752
+ model,
753
+ refusal
570
754
  }) {
571
755
  if (answerFn) {
572
- return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId });
756
+ return runAnswerFn(answerFn, { system, messages, mode, sender, conversationId }, refusal);
573
757
  }
574
- return completeOnce({ client, system, messages, temperature, model });
758
+ return completeOnce({ client, system, messages, temperature, model, refusal });
575
759
  }
576
760
  async function applyTransformReply(transformReply, input, logger) {
577
761
  if (!transformReply) return input.text;
@@ -1001,6 +1185,7 @@ async function createMCPServer(config) {
1001
1185
  store = new VectorStore2(createOpenAIEmbedder2(client), {
1002
1186
  databaseClient: db,
1003
1187
  knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
1188
+ chunking: config.chunking,
1004
1189
  logger: log
1005
1190
  });
1006
1191
  }
@@ -1072,7 +1257,8 @@ async function createMCPServer(config) {
1072
1257
  messages: conversationMessages,
1073
1258
  mode: "public",
1074
1259
  conversationId: convId,
1075
- model
1260
+ model,
1261
+ refusal: config.refusal
1076
1262
  });
1077
1263
  const replyText = await applyTransformReply(
1078
1264
  config.transformReply,
@@ -1162,7 +1348,8 @@ async function createMCPServer(config) {
1162
1348
  messages: conversationMessages,
1163
1349
  mode: "private",
1164
1350
  conversationId: convId,
1165
- model
1351
+ model,
1352
+ refusal: config.refusal
1166
1353
  });
1167
1354
  const replyText = await applyTransformReply(
1168
1355
  config.transformReply,
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export type { Greeter, GreeterOptions } from "./greeter";
10
10
  export { createGreeter } from "./greeter";
11
- export type { PersonaContact, PersonaDefinition, PersonaRegistry, PersonaResolver, PersonaResolverOptions, } from "./resolver";
11
+ export type { PersonaContact, PersonaDefinition, PersonaEndpoint, PersonaKey, PersonaRegistry, PersonaResolver, PersonaResolverOptions, } from "./resolver";
12
12
  export { createPersonaResolver } from "./resolver";
13
13
  export { timeContext } from "./timeContext";
14
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/personas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/personas/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,YAAY,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,UAAU,EACV,eAAe,EACf,eAAe,EACf,sBAAsB,GACvB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -124,7 +124,19 @@ function createPersonaResolver(options = {}) {
124
124
  return void 0;
125
125
  }
126
126
  }
127
- function rollPersonaId(contactId) {
127
+ function endpointPersonaId(endpointId) {
128
+ if (!endpointId) return null;
129
+ try {
130
+ return loadRegistry().endpoints?.[endpointId]?.persona ?? null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ function rollPersonaId(key) {
136
+ const { contactId, endpointId } = typeof key === "string" ? { contactId: key } : key;
137
+ const boundToEndpoint = endpointPersonaId(endpointId);
138
+ if (boundToEndpoint) return boundToEndpoint;
139
+ if (contactId === void 0) return null;
128
140
  try {
129
141
  const reg = loadRegistry();
130
142
  const nowMs = now();
@@ -132,8 +144,8 @@ function createPersonaResolver(options = {}) {
132
144
  if (existing && existing.expiresAt > nowMs) {
133
145
  return existing.personaId;
134
146
  }
135
- for (const [key, value] of window) {
136
- if (value.expiresAt <= nowMs) window.delete(key);
147
+ for (const [held, value] of window) {
148
+ if (value.expiresAt <= nowMs) window.delete(held);
137
149
  }
138
150
  const contact = reg.contacts?.[contactId];
139
151
  let personaId = null;
@@ -181,8 +193,8 @@ function createPersonaResolver(options = {}) {
181
193
  return null;
182
194
  }
183
195
  }
184
- function resolvePersonaLayer(contactId) {
185
- return personaLayerFor(rollPersonaId(contactId)) ?? personaLayerFor(defaultPersonaId());
196
+ function resolvePersonaLayer(key) {
197
+ return personaLayerFor(rollPersonaId(key)) ?? personaLayerFor(defaultPersonaId());
186
198
  }
187
199
  return {
188
200
  rollPersonaId,
@@ -199,7 +211,7 @@ function timeContext(zones, now = Date.now()) {
199
211
  const at = new Date(now);
200
212
  const parts = zones.flatMap((zone) => {
201
213
  try {
202
- const formatted = new Intl.DateTimeFormat("en-GB", {
214
+ const fields = new Intl.DateTimeFormat("en-GB", {
203
215
  timeZone: zone,
204
216
  weekday: "long",
205
217
  day: "numeric",
@@ -208,7 +220,9 @@ function timeContext(zones, now = Date.now()) {
208
220
  hour: "2-digit",
209
221
  minute: "2-digit",
210
222
  hour12: false
211
- }).format(at);
223
+ }).formatToParts(at);
224
+ const part = (type) => fields.find((field) => field.type === type)?.value ?? "";
225
+ const formatted = `${part("weekday")} ${part("day")} ${part("month")} ${part("year")} at ${part("hour")}:${part("minute")}`;
212
226
  return [`${formatted} (${zone})`];
213
227
  } catch {
214
228
  return [];
@@ -96,7 +96,19 @@ function createPersonaResolver(options = {}) {
96
96
  return void 0;
97
97
  }
98
98
  }
99
- function rollPersonaId(contactId) {
99
+ function endpointPersonaId(endpointId) {
100
+ if (!endpointId) return null;
101
+ try {
102
+ return loadRegistry().endpoints?.[endpointId]?.persona ?? null;
103
+ } catch {
104
+ return null;
105
+ }
106
+ }
107
+ function rollPersonaId(key) {
108
+ const { contactId, endpointId } = typeof key === "string" ? { contactId: key } : key;
109
+ const boundToEndpoint = endpointPersonaId(endpointId);
110
+ if (boundToEndpoint) return boundToEndpoint;
111
+ if (contactId === void 0) return null;
100
112
  try {
101
113
  const reg = loadRegistry();
102
114
  const nowMs = now();
@@ -104,8 +116,8 @@ function createPersonaResolver(options = {}) {
104
116
  if (existing && existing.expiresAt > nowMs) {
105
117
  return existing.personaId;
106
118
  }
107
- for (const [key, value] of window) {
108
- if (value.expiresAt <= nowMs) window.delete(key);
119
+ for (const [held, value] of window) {
120
+ if (value.expiresAt <= nowMs) window.delete(held);
109
121
  }
110
122
  const contact = reg.contacts?.[contactId];
111
123
  let personaId = null;
@@ -153,8 +165,8 @@ function createPersonaResolver(options = {}) {
153
165
  return null;
154
166
  }
155
167
  }
156
- function resolvePersonaLayer(contactId) {
157
- return personaLayerFor(rollPersonaId(contactId)) ?? personaLayerFor(defaultPersonaId());
168
+ function resolvePersonaLayer(key) {
169
+ return personaLayerFor(rollPersonaId(key)) ?? personaLayerFor(defaultPersonaId());
158
170
  }
159
171
  return {
160
172
  rollPersonaId,
@@ -171,7 +183,7 @@ function timeContext(zones, now = Date.now()) {
171
183
  const at = new Date(now);
172
184
  const parts = zones.flatMap((zone) => {
173
185
  try {
174
- const formatted = new Intl.DateTimeFormat("en-GB", {
186
+ const fields = new Intl.DateTimeFormat("en-GB", {
175
187
  timeZone: zone,
176
188
  weekday: "long",
177
189
  day: "numeric",
@@ -180,7 +192,9 @@ function timeContext(zones, now = Date.now()) {
180
192
  hour: "2-digit",
181
193
  minute: "2-digit",
182
194
  hour12: false
183
- }).format(at);
195
+ }).formatToParts(at);
196
+ const part = (type) => fields.find((field) => field.type === type)?.value ?? "";
197
+ const formatted = `${part("weekday")} ${part("day")} ${part("month")} ${part("year")} at ${part("hour")}:${part("minute")}`;
184
198
  return [`${formatted} (${zone})`];
185
199
  } catch {
186
200
  return [];
@@ -20,6 +20,14 @@ export interface PersonaContact {
20
20
  /** Overrides `personaProbability` for this contact when set. */
21
21
  probability?: number;
22
22
  }
23
+ /**
24
+ * A persona bound to one of the bot's own endpoints - which of its numbers,
25
+ * tokens or rooms the message arrived on, rather than who sent it.
26
+ */
27
+ export interface PersonaEndpoint {
28
+ /** Persona id this endpoint always answers as. */
29
+ persona: string;
30
+ }
23
31
  export interface PersonaRegistry {
24
32
  /** Persona id used when a contact is unmapped or its roll doesn't land. */
25
33
  defaultPersona: string;
@@ -28,8 +36,23 @@ export interface PersonaRegistry {
28
36
  /** Minutes a roll is held before the next message re-rolls it. Default 10. */
29
37
  personaWindowMinutes?: number;
30
38
  contacts?: Record<string, PersonaContact>;
39
+ /**
40
+ * Personas keyed on the endpoint that received the message
41
+ * (`ChannelMessage.endpointId`), for a bot whose voice is decided by which
42
+ * of its own identities was reached rather than by who reached it.
43
+ */
44
+ endpoints?: Record<string, PersonaEndpoint>;
31
45
  personas: Record<string, PersonaDefinition>;
32
46
  }
47
+ /**
48
+ * Who a persona is resolved for. A bare string is the contact id, which is
49
+ * every call that predates endpoint keying; the object form adds the endpoint
50
+ * the message arrived on, and either half may be omitted.
51
+ */
52
+ export type PersonaKey = string | {
53
+ contactId?: string;
54
+ endpointId?: string;
55
+ };
33
56
  export interface PersonaResolverOptions {
34
57
  /** Inline registry — takes priority over `registryPath` when both are set. */
35
58
  registry?: PersonaRegistry;
@@ -48,17 +71,21 @@ export interface PersonaResolverOptions {
48
71
  }
49
72
  export interface PersonaResolver {
50
73
  /**
51
- * The persona id in effect for a contact right now: rolled at
52
- * `personaProbability` once per window, held for `personaWindowMinutes`.
53
- * `null` means "use the default persona" (unmapped contact or a lost roll).
74
+ * The persona id in effect right now. A key naming an endpoint that the
75
+ * registry binds resolves to that persona deterministically - no roll, no
76
+ * window - because an endpoint-bound voice is a property of the identity
77
+ * reached, not something that varies over a conversation. Otherwise the
78
+ * contact is rolled at `personaProbability` once per window and held for
79
+ * `personaWindowMinutes`. `null` means "use the default persona" (no key,
80
+ * an unmapped contact, or a lost roll).
54
81
  */
55
- rollPersonaId(contactId: string): string | null;
82
+ rollPersonaId(key: PersonaKey): string | null;
56
83
  /**
57
- * The interpolated prompt layer for the contact's currently-held persona,
58
- * falling back to the default persona's layer. Feeds `prepareChat`'s
59
- * `personaLayer` directly.
84
+ * The interpolated prompt layer for the persona {@link rollPersonaId}
85
+ * yields, falling back to the default persona's layer. Feeds
86
+ * `prepareChat`'s `personaLayer` directly.
60
87
  */
61
- resolvePersonaLayer(contactId: string): string | null;
88
+ resolvePersonaLayer(key: PersonaKey): string | null;
62
89
  /** Deterministic layer for an explicit persona id (no dice roll), or null. */
63
90
  personaLayerFor(personaId: string | null): string | null;
64
91
  /** Registry contact record, or undefined. */
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/personas/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0HAA0H;IAC1H,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;OAIG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAChD;;;;OAIG;IACH,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACtD,8EAA8E;IAC9E,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IACzD,6CAA6C;IAC7C,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;IAC1D,wEAAwE;IACxE,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAAC;CACnC;AAYD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAgI3F"}
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/personas/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,mFAAmF;IACnF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC1C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC5C,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9E,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,0HAA0H;IAC1H,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;IACtB,sDAAsD;IACtD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC;IAC9C;;;;OAIG;IACH,mBAAmB,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC;IACpD,8EAA8E;IAC9E,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAAC;IACzD,6CAA6C;IAC7C,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;IAC1D,wEAAwE;IACxE,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAAC;CACnC;AAYD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,eAAe,CAmJ3F"}
@@ -1 +1 @@
1
- {"version":3,"file":"timeContext.d.ts","sourceRoot":"","sources":["../../src/personas/timeContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CAuB7E"}
1
+ {"version":3,"file":"timeContext.d.ts","sourceRoot":"","sources":["../../src/personas/timeContext.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,CA6B7E"}
@@ -1 +1 @@
1
- {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../src/routes/demo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAa5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AA6FnD,wBAAgB,UAAU,CAAC,IAAI,EAAE,kBAAkB,8EAmIlD"}
1
+ {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../src/routes/demo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAa5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AA6FnD,wBAAgB,UAAU,CAAC,IAAI,EAAE,kBAAkB,8EAqIlD"}
@@ -1 +1 @@
1
- {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/routes/openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAsDnD,wBAAgB,YAAY,CAAC,IAAI,EAAE,kBAAkB,8EAoLpD"}
1
+ {"version":3,"file":"openai.d.ts","sourceRoot":"","sources":["../../src/routes/openai.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAsDnD,wBAAgB,YAAY,CAAC,IAAI,EAAE,kBAAkB,8EAsLpD"}
@@ -1 +1 @@
1
- {"version":3,"file":"private.d.ts","sourceRoot":"","sources":["../../src/routes/private.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AASnD,wBAAgB,aAAa,CAAC,IAAI,EAAE,kBAAkB,8EAuFrD"}
1
+ {"version":3,"file":"private.d.ts","sourceRoot":"","sources":["../../src/routes/private.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAS5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AASnD,wBAAgB,aAAa,CAAC,IAAI,EAAE,kBAAkB,8EAyFrD"}
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/routes/public.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AASnD,wBAAgB,YAAY,CAAC,IAAI,EAAE,kBAAkB,8EAsFpD"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/routes/public.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AASnD,wBAAgB,YAAY,CAAC,IAAI,EAAE,kBAAkB,8EA+FpD"}