@diegoaltoworks/chatter 0.62.0 → 0.64.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.
package/dist/index.mjs CHANGED
@@ -393,6 +393,119 @@ var init_retrieval = __esm({
393
393
  }
394
394
  });
395
395
 
396
+ // src/core/knowledgeHealth.ts
397
+ var knowledgeHealth_exports = {};
398
+ __export(knowledgeHealth_exports, {
399
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS: () => DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS,
400
+ checkKnowledgeHealth: () => checkKnowledgeHealth,
401
+ scheduleKnowledgeHealthChecks: () => scheduleKnowledgeHealthChecks
402
+ });
403
+ async function checkKnowledgeHealth(db, logger) {
404
+ const [byBucket, totalChunksRes, totalEmbeddingsRes, orphanedRes, missingRes] = await Promise.all(
405
+ [
406
+ db.execute("SELECT bucket, COUNT(*) as count FROM chunks GROUP BY bucket"),
407
+ db.execute("SELECT COUNT(*) as count FROM chunks"),
408
+ db.execute("SELECT COUNT(*) as count FROM embeddings"),
409
+ db.execute(
410
+ "SELECT COUNT(*) as count FROM embeddings e LEFT JOIN chunks c ON c.id = e.id WHERE c.id IS NULL"
411
+ ),
412
+ db.execute(
413
+ "SELECT COUNT(*) as count FROM chunks c LEFT JOIN embeddings e ON e.id = c.id WHERE e.id IS NULL"
414
+ )
415
+ ]
416
+ );
417
+ const chunksByBucket = {};
418
+ for (const row of byBucket.rows) {
419
+ chunksByBucket[String(row.bucket)] = Number(row.count ?? 0);
420
+ }
421
+ const health = {
422
+ chunksByBucket,
423
+ totalChunks: Number(totalChunksRes.rows[0]?.count ?? 0),
424
+ totalEmbeddings: Number(totalEmbeddingsRes.rows[0]?.count ?? 0),
425
+ orphanedEmbeddings: Number(orphanedRes.rows[0]?.count ?? 0),
426
+ missingEmbeddings: Number(missingRes.rows[0]?.count ?? 0)
427
+ };
428
+ const summary = `chunksByBucket=${JSON.stringify(health.chunksByBucket)} totalChunks=${health.totalChunks} totalEmbeddings=${health.totalEmbeddings} orphanedEmbeddings=${health.orphanedEmbeddings} missingEmbeddings=${health.missingEmbeddings}`;
429
+ if (health.totalChunks === 0 || health.orphanedEmbeddings > 0) {
430
+ logger.warn(`Knowledge base health check: ${summary}`);
431
+ } else {
432
+ logger.info(`Knowledge base health check: ${summary}`);
433
+ }
434
+ return health;
435
+ }
436
+ async function cleanOrphanedEmbeddings(db, buildLock, instanceId, logger) {
437
+ const acquired = await buildLock.tryAcquire(
438
+ BUILD_LOCK_KEY,
439
+ instanceId,
440
+ Date.now(),
441
+ BUILD_LOCK_STALE_MS
442
+ );
443
+ if (!acquired) {
444
+ logger.warn(
445
+ "Knowledge health: skipping orphaned-embeddings cleanup - the build lock is held by another instance."
446
+ );
447
+ return;
448
+ }
449
+ try {
450
+ const result = await db.execute(
451
+ "DELETE FROM embeddings WHERE id NOT IN (SELECT id FROM chunks)"
452
+ );
453
+ if (result.rowsAffected > 0) {
454
+ logger.warn(`Knowledge health: deleted ${result.rowsAffected} orphaned embedding row(s).`);
455
+ }
456
+ } finally {
457
+ await buildLock.release(BUILD_LOCK_KEY, instanceId);
458
+ }
459
+ }
460
+ async function scheduleKnowledgeHealthChecks(options) {
461
+ const logger = options.logger ?? createConsoleLogger();
462
+ const enabled = options.config?.enabled !== false;
463
+ const intervalMs = options.config?.intervalMs ?? DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS;
464
+ const autoClean = options.config?.autoCleanOrphanedEmbeddings === true;
465
+ const buildLock = options.buildLock ?? createTursoBuildLock(options.db);
466
+ const instanceId = options.instanceId ?? crypto.randomUUID();
467
+ const schedule = options.schedule ?? defaultSchedule;
468
+ async function runOnce() {
469
+ const health = await checkKnowledgeHealth(options.db, logger);
470
+ if (autoClean && health.orphanedEmbeddings > 0) {
471
+ await cleanOrphanedEmbeddings(options.db, buildLock, instanceId, logger);
472
+ }
473
+ return health;
474
+ }
475
+ let handle;
476
+ if (enabled) {
477
+ await runOnce();
478
+ if (intervalMs > 0) {
479
+ let ticking = false;
480
+ handle = schedule(() => {
481
+ if (ticking) return;
482
+ ticking = true;
483
+ runOnce().catch((error) => logger.error("Knowledge health check failed:", error)).finally(() => {
484
+ ticking = false;
485
+ });
486
+ }, intervalMs);
487
+ }
488
+ }
489
+ return {
490
+ runOnce,
491
+ stop: () => handle?.stop()
492
+ };
493
+ }
494
+ var DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS, defaultSchedule;
495
+ var init_knowledgeHealth = __esm({
496
+ "src/core/knowledgeHealth.ts"() {
497
+ "use strict";
498
+ init_buildLock();
499
+ init_logger();
500
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS = 30 * 6e4;
501
+ defaultSchedule = (fn, ms) => {
502
+ const timer = setInterval(fn, ms);
503
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
504
+ return { stop: () => clearInterval(timer) };
505
+ };
506
+ }
507
+ });
508
+
396
509
  // src/auth/apikeys.ts
397
510
  import { randomUUID } from "node:crypto";
398
511
  import { jwtVerify, SignJWT } from "jose";
@@ -1146,6 +1259,7 @@ async function createMCPServer(config) {
1146
1259
  });
1147
1260
  const model = config.openai.model || DEFAULT_MODEL;
1148
1261
  let store;
1262
+ let db;
1149
1263
  if (config.retriever) {
1150
1264
  store = config.retriever;
1151
1265
  } else {
@@ -1153,7 +1267,7 @@ async function createMCPServer(config) {
1153
1267
  throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
1154
1268
  }
1155
1269
  const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
1156
- const db = await openLibsqlClient2(config.database);
1270
+ db = await openLibsqlClient2(config.database);
1157
1271
  store = new VectorStore2(createOpenAIEmbedder2(client), {
1158
1272
  databaseClient: db,
1159
1273
  knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
@@ -1162,6 +1276,15 @@ async function createMCPServer(config) {
1162
1276
  }
1163
1277
  await store.build?.();
1164
1278
  log.info("\u2705 Knowledge base ready");
1279
+ let knowledgeHealth;
1280
+ if (db) {
1281
+ const { scheduleKnowledgeHealthChecks: scheduleKnowledgeHealthChecks2 } = await Promise.resolve().then(() => (init_knowledgeHealth(), knowledgeHealth_exports));
1282
+ knowledgeHealth = await scheduleKnowledgeHealthChecks2({
1283
+ db,
1284
+ config: config.knowledgeHealthCheck,
1285
+ logger: log
1286
+ });
1287
+ }
1165
1288
  const prompts = new PromptLoader(promptsDir, config.bot);
1166
1289
  const server = new McpServer({
1167
1290
  name: config.bot.name,
@@ -1358,7 +1481,9 @@ async function createMCPServer(config) {
1358
1481
  log.info(
1359
1482
  `\u2705 ${config.bot.name} MCP Server ready (transport: ${transportMode}, tools: ${enabledTools.join(", ") || "none"})`
1360
1483
  );
1361
- return server;
1484
+ return Object.assign(server, {
1485
+ stopKnowledgeHealthChecks: () => knowledgeHealth?.stop()
1486
+ });
1362
1487
  }
1363
1488
 
1364
1489
  // src/middleware/apiKey.ts
@@ -2340,6 +2465,15 @@ async function createServer(config) {
2340
2465
  }
2341
2466
  await store.build?.();
2342
2467
  logger.info("\u2705 Knowledge base ready");
2468
+ let knowledgeHealth;
2469
+ if (db && !config.retriever) {
2470
+ const { scheduleKnowledgeHealthChecks: scheduleKnowledgeHealthChecks2 } = await Promise.resolve().then(() => (init_knowledgeHealth(), knowledgeHealth_exports));
2471
+ knowledgeHealth = await scheduleKnowledgeHealthChecks2({
2472
+ db,
2473
+ config: config.knowledgeHealthCheck,
2474
+ logger
2475
+ });
2476
+ }
2343
2477
  const prompts = new PromptLoader(promptsDir, config.bot);
2344
2478
  let apiKeyManager;
2345
2479
  const secret = config.auth?.secret || process.env.CHATTER_SECRET;
@@ -2470,6 +2604,7 @@ async function createServer(config) {
2470
2604
  // (sync or async) is caught per-channel so one misbehaving channel can't
2471
2605
  // stop the rest from cleaning up.
2472
2606
  stopChannels: async () => {
2607
+ knowledgeHealth?.stop();
2473
2608
  await Promise.allSettled(
2474
2609
  started.map(async (channel) => {
2475
2610
  try {
@@ -31,5 +31,7 @@ import type { MCPServerOptions } from "./mcp-server/types";
31
31
  * await server.connect(transport);
32
32
  * ```
33
33
  */
34
- export declare function createMCPServer(config: MCPServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
34
+ export declare function createMCPServer(config: MCPServerOptions): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer & {
35
+ stopKnowledgeHealthChecks: () => void;
36
+ }>;
35
37
  //# sourceMappingURL=mcp-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAoBH,YAAY,EACV,WAAW,EACX,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,gBAAgB,qEA0T7D"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsBH,YAAY,EACV,WAAW,EACX,QAAQ,EACR,cAAc,EACd,gBAAgB,EAChB,aAAa,EACb,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,gBAAgB;qCA4U3B,IAAI;GAEtC"}
@@ -411,6 +411,119 @@ var init_retrieval = __esm({
411
411
  }
412
412
  });
413
413
 
414
+ // src/core/knowledgeHealth.ts
415
+ var knowledgeHealth_exports = {};
416
+ __export(knowledgeHealth_exports, {
417
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS: () => DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS,
418
+ checkKnowledgeHealth: () => checkKnowledgeHealth,
419
+ scheduleKnowledgeHealthChecks: () => scheduleKnowledgeHealthChecks
420
+ });
421
+ async function checkKnowledgeHealth(db, logger) {
422
+ const [byBucket, totalChunksRes, totalEmbeddingsRes, orphanedRes, missingRes] = await Promise.all(
423
+ [
424
+ db.execute("SELECT bucket, COUNT(*) as count FROM chunks GROUP BY bucket"),
425
+ db.execute("SELECT COUNT(*) as count FROM chunks"),
426
+ db.execute("SELECT COUNT(*) as count FROM embeddings"),
427
+ db.execute(
428
+ "SELECT COUNT(*) as count FROM embeddings e LEFT JOIN chunks c ON c.id = e.id WHERE c.id IS NULL"
429
+ ),
430
+ db.execute(
431
+ "SELECT COUNT(*) as count FROM chunks c LEFT JOIN embeddings e ON e.id = c.id WHERE e.id IS NULL"
432
+ )
433
+ ]
434
+ );
435
+ const chunksByBucket = {};
436
+ for (const row of byBucket.rows) {
437
+ chunksByBucket[String(row.bucket)] = Number(row.count ?? 0);
438
+ }
439
+ const health = {
440
+ chunksByBucket,
441
+ totalChunks: Number(totalChunksRes.rows[0]?.count ?? 0),
442
+ totalEmbeddings: Number(totalEmbeddingsRes.rows[0]?.count ?? 0),
443
+ orphanedEmbeddings: Number(orphanedRes.rows[0]?.count ?? 0),
444
+ missingEmbeddings: Number(missingRes.rows[0]?.count ?? 0)
445
+ };
446
+ const summary = `chunksByBucket=${JSON.stringify(health.chunksByBucket)} totalChunks=${health.totalChunks} totalEmbeddings=${health.totalEmbeddings} orphanedEmbeddings=${health.orphanedEmbeddings} missingEmbeddings=${health.missingEmbeddings}`;
447
+ if (health.totalChunks === 0 || health.orphanedEmbeddings > 0) {
448
+ logger.warn(`Knowledge base health check: ${summary}`);
449
+ } else {
450
+ logger.info(`Knowledge base health check: ${summary}`);
451
+ }
452
+ return health;
453
+ }
454
+ async function cleanOrphanedEmbeddings(db, buildLock, instanceId, logger) {
455
+ const acquired = await buildLock.tryAcquire(
456
+ BUILD_LOCK_KEY,
457
+ instanceId,
458
+ Date.now(),
459
+ BUILD_LOCK_STALE_MS
460
+ );
461
+ if (!acquired) {
462
+ logger.warn(
463
+ "Knowledge health: skipping orphaned-embeddings cleanup - the build lock is held by another instance."
464
+ );
465
+ return;
466
+ }
467
+ try {
468
+ const result = await db.execute(
469
+ "DELETE FROM embeddings WHERE id NOT IN (SELECT id FROM chunks)"
470
+ );
471
+ if (result.rowsAffected > 0) {
472
+ logger.warn(`Knowledge health: deleted ${result.rowsAffected} orphaned embedding row(s).`);
473
+ }
474
+ } finally {
475
+ await buildLock.release(BUILD_LOCK_KEY, instanceId);
476
+ }
477
+ }
478
+ async function scheduleKnowledgeHealthChecks(options) {
479
+ const logger = options.logger ?? createConsoleLogger();
480
+ const enabled = options.config?.enabled !== false;
481
+ const intervalMs = options.config?.intervalMs ?? DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS;
482
+ const autoClean = options.config?.autoCleanOrphanedEmbeddings === true;
483
+ const buildLock = options.buildLock ?? createTursoBuildLock(options.db);
484
+ const instanceId = options.instanceId ?? crypto.randomUUID();
485
+ const schedule = options.schedule ?? defaultSchedule;
486
+ async function runOnce() {
487
+ const health = await checkKnowledgeHealth(options.db, logger);
488
+ if (autoClean && health.orphanedEmbeddings > 0) {
489
+ await cleanOrphanedEmbeddings(options.db, buildLock, instanceId, logger);
490
+ }
491
+ return health;
492
+ }
493
+ let handle;
494
+ if (enabled) {
495
+ await runOnce();
496
+ if (intervalMs > 0) {
497
+ let ticking = false;
498
+ handle = schedule(() => {
499
+ if (ticking) return;
500
+ ticking = true;
501
+ runOnce().catch((error) => logger.error("Knowledge health check failed:", error)).finally(() => {
502
+ ticking = false;
503
+ });
504
+ }, intervalMs);
505
+ }
506
+ }
507
+ return {
508
+ runOnce,
509
+ stop: () => handle?.stop()
510
+ };
511
+ }
512
+ var DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS, defaultSchedule;
513
+ var init_knowledgeHealth = __esm({
514
+ "src/core/knowledgeHealth.ts"() {
515
+ "use strict";
516
+ init_buildLock();
517
+ init_logger();
518
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS = 30 * 6e4;
519
+ defaultSchedule = (fn, ms) => {
520
+ const timer = setInterval(fn, ms);
521
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
522
+ return { stop: () => clearInterval(timer) };
523
+ };
524
+ }
525
+ });
526
+
414
527
  // src/mcp-server.ts
415
528
  var mcp_server_exports = {};
416
529
  __export(mcp_server_exports, {
@@ -899,6 +1012,7 @@ async function createMCPServer(config) {
899
1012
  });
900
1013
  const model = config.openai.model || DEFAULT_MODEL;
901
1014
  let store;
1015
+ let db;
902
1016
  if (config.retriever) {
903
1017
  store = config.retriever;
904
1018
  } else {
@@ -906,7 +1020,7 @@ async function createMCPServer(config) {
906
1020
  throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
907
1021
  }
908
1022
  const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
909
- const db = await openLibsqlClient2(config.database);
1023
+ db = await openLibsqlClient2(config.database);
910
1024
  store = new VectorStore2(createOpenAIEmbedder2(client), {
911
1025
  databaseClient: db,
912
1026
  knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
@@ -915,6 +1029,15 @@ async function createMCPServer(config) {
915
1029
  }
916
1030
  await store.build?.();
917
1031
  log.info("\u2705 Knowledge base ready");
1032
+ let knowledgeHealth;
1033
+ if (db) {
1034
+ const { scheduleKnowledgeHealthChecks: scheduleKnowledgeHealthChecks2 } = await Promise.resolve().then(() => (init_knowledgeHealth(), knowledgeHealth_exports));
1035
+ knowledgeHealth = await scheduleKnowledgeHealthChecks2({
1036
+ db,
1037
+ config: config.knowledgeHealthCheck,
1038
+ logger: log
1039
+ });
1040
+ }
918
1041
  const prompts = new PromptLoader(promptsDir, config.bot);
919
1042
  const server = new McpServer({
920
1043
  name: config.bot.name,
@@ -1111,7 +1234,9 @@ async function createMCPServer(config) {
1111
1234
  log.info(
1112
1235
  `\u2705 ${config.bot.name} MCP Server ready (transport: ${transportMode}, tools: ${enabledTools.join(", ") || "none"})`
1113
1236
  );
1114
- return server;
1237
+ return Object.assign(server, {
1238
+ stopKnowledgeHealthChecks: () => knowledgeHealth?.stop()
1239
+ });
1115
1240
  }
1116
1241
  // Annotate the CommonJS export names for ESM import in node:
1117
1242
  0 && (module.exports = {
@@ -388,6 +388,119 @@ var init_retrieval = __esm({
388
388
  }
389
389
  });
390
390
 
391
+ // src/core/knowledgeHealth.ts
392
+ var knowledgeHealth_exports = {};
393
+ __export(knowledgeHealth_exports, {
394
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS: () => DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS,
395
+ checkKnowledgeHealth: () => checkKnowledgeHealth,
396
+ scheduleKnowledgeHealthChecks: () => scheduleKnowledgeHealthChecks
397
+ });
398
+ async function checkKnowledgeHealth(db, logger) {
399
+ const [byBucket, totalChunksRes, totalEmbeddingsRes, orphanedRes, missingRes] = await Promise.all(
400
+ [
401
+ db.execute("SELECT bucket, COUNT(*) as count FROM chunks GROUP BY bucket"),
402
+ db.execute("SELECT COUNT(*) as count FROM chunks"),
403
+ db.execute("SELECT COUNT(*) as count FROM embeddings"),
404
+ db.execute(
405
+ "SELECT COUNT(*) as count FROM embeddings e LEFT JOIN chunks c ON c.id = e.id WHERE c.id IS NULL"
406
+ ),
407
+ db.execute(
408
+ "SELECT COUNT(*) as count FROM chunks c LEFT JOIN embeddings e ON e.id = c.id WHERE e.id IS NULL"
409
+ )
410
+ ]
411
+ );
412
+ const chunksByBucket = {};
413
+ for (const row of byBucket.rows) {
414
+ chunksByBucket[String(row.bucket)] = Number(row.count ?? 0);
415
+ }
416
+ const health = {
417
+ chunksByBucket,
418
+ totalChunks: Number(totalChunksRes.rows[0]?.count ?? 0),
419
+ totalEmbeddings: Number(totalEmbeddingsRes.rows[0]?.count ?? 0),
420
+ orphanedEmbeddings: Number(orphanedRes.rows[0]?.count ?? 0),
421
+ missingEmbeddings: Number(missingRes.rows[0]?.count ?? 0)
422
+ };
423
+ const summary = `chunksByBucket=${JSON.stringify(health.chunksByBucket)} totalChunks=${health.totalChunks} totalEmbeddings=${health.totalEmbeddings} orphanedEmbeddings=${health.orphanedEmbeddings} missingEmbeddings=${health.missingEmbeddings}`;
424
+ if (health.totalChunks === 0 || health.orphanedEmbeddings > 0) {
425
+ logger.warn(`Knowledge base health check: ${summary}`);
426
+ } else {
427
+ logger.info(`Knowledge base health check: ${summary}`);
428
+ }
429
+ return health;
430
+ }
431
+ async function cleanOrphanedEmbeddings(db, buildLock, instanceId, logger) {
432
+ const acquired = await buildLock.tryAcquire(
433
+ BUILD_LOCK_KEY,
434
+ instanceId,
435
+ Date.now(),
436
+ BUILD_LOCK_STALE_MS
437
+ );
438
+ if (!acquired) {
439
+ logger.warn(
440
+ "Knowledge health: skipping orphaned-embeddings cleanup - the build lock is held by another instance."
441
+ );
442
+ return;
443
+ }
444
+ try {
445
+ const result = await db.execute(
446
+ "DELETE FROM embeddings WHERE id NOT IN (SELECT id FROM chunks)"
447
+ );
448
+ if (result.rowsAffected > 0) {
449
+ logger.warn(`Knowledge health: deleted ${result.rowsAffected} orphaned embedding row(s).`);
450
+ }
451
+ } finally {
452
+ await buildLock.release(BUILD_LOCK_KEY, instanceId);
453
+ }
454
+ }
455
+ async function scheduleKnowledgeHealthChecks(options) {
456
+ const logger = options.logger ?? createConsoleLogger();
457
+ const enabled = options.config?.enabled !== false;
458
+ const intervalMs = options.config?.intervalMs ?? DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS;
459
+ const autoClean = options.config?.autoCleanOrphanedEmbeddings === true;
460
+ const buildLock = options.buildLock ?? createTursoBuildLock(options.db);
461
+ const instanceId = options.instanceId ?? crypto.randomUUID();
462
+ const schedule = options.schedule ?? defaultSchedule;
463
+ async function runOnce() {
464
+ const health = await checkKnowledgeHealth(options.db, logger);
465
+ if (autoClean && health.orphanedEmbeddings > 0) {
466
+ await cleanOrphanedEmbeddings(options.db, buildLock, instanceId, logger);
467
+ }
468
+ return health;
469
+ }
470
+ let handle;
471
+ if (enabled) {
472
+ await runOnce();
473
+ if (intervalMs > 0) {
474
+ let ticking = false;
475
+ handle = schedule(() => {
476
+ if (ticking) return;
477
+ ticking = true;
478
+ runOnce().catch((error) => logger.error("Knowledge health check failed:", error)).finally(() => {
479
+ ticking = false;
480
+ });
481
+ }, intervalMs);
482
+ }
483
+ }
484
+ return {
485
+ runOnce,
486
+ stop: () => handle?.stop()
487
+ };
488
+ }
489
+ var DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS, defaultSchedule;
490
+ var init_knowledgeHealth = __esm({
491
+ "src/core/knowledgeHealth.ts"() {
492
+ "use strict";
493
+ init_buildLock();
494
+ init_logger();
495
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS = 30 * 6e4;
496
+ defaultSchedule = (fn, ms) => {
497
+ const timer = setInterval(fn, ms);
498
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
499
+ return { stop: () => clearInterval(timer) };
500
+ };
501
+ }
502
+ });
503
+
391
504
  // src/mcp-server.ts
392
505
  import OpenAI from "openai";
393
506
 
@@ -871,6 +984,7 @@ async function createMCPServer(config) {
871
984
  });
872
985
  const model = config.openai.model || DEFAULT_MODEL;
873
986
  let store;
987
+ let db;
874
988
  if (config.retriever) {
875
989
  store = config.retriever;
876
990
  } else {
@@ -878,7 +992,7 @@ async function createMCPServer(config) {
878
992
  throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
879
993
  }
880
994
  const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
881
- const db = await openLibsqlClient2(config.database);
995
+ db = await openLibsqlClient2(config.database);
882
996
  store = new VectorStore2(createOpenAIEmbedder2(client), {
883
997
  databaseClient: db,
884
998
  knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
@@ -887,6 +1001,15 @@ async function createMCPServer(config) {
887
1001
  }
888
1002
  await store.build?.();
889
1003
  log.info("\u2705 Knowledge base ready");
1004
+ let knowledgeHealth;
1005
+ if (db) {
1006
+ const { scheduleKnowledgeHealthChecks: scheduleKnowledgeHealthChecks2 } = await Promise.resolve().then(() => (init_knowledgeHealth(), knowledgeHealth_exports));
1007
+ knowledgeHealth = await scheduleKnowledgeHealthChecks2({
1008
+ db,
1009
+ config: config.knowledgeHealthCheck,
1010
+ logger: log
1011
+ });
1012
+ }
890
1013
  const prompts = new PromptLoader(promptsDir, config.bot);
891
1014
  const server = new McpServer({
892
1015
  name: config.bot.name,
@@ -1083,7 +1206,9 @@ async function createMCPServer(config) {
1083
1206
  log.info(
1084
1207
  `\u2705 ${config.bot.name} MCP Server ready (transport: ${transportMode}, tools: ${enabledTools.join(", ") || "none"})`
1085
1208
  );
1086
- return server;
1209
+ return Object.assign(server, {
1210
+ stopKnowledgeHealthChecks: () => knowledgeHealth?.stop()
1211
+ });
1087
1212
  }
1088
1213
  export {
1089
1214
  createMCPServer
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAgB5B,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG;IAAE,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAEtE;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAiP7E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAIH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAiB5B,OAAO,KAAK,EAAE,aAAa,EAAsB,MAAM,SAAS,CAAC;AAEjE;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG;IAAE,YAAY,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAEtE;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CA+P7E"}
package/dist/server.js CHANGED
@@ -411,6 +411,119 @@ var init_retrieval = __esm({
411
411
  }
412
412
  });
413
413
 
414
+ // src/core/knowledgeHealth.ts
415
+ var knowledgeHealth_exports = {};
416
+ __export(knowledgeHealth_exports, {
417
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS: () => DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS,
418
+ checkKnowledgeHealth: () => checkKnowledgeHealth,
419
+ scheduleKnowledgeHealthChecks: () => scheduleKnowledgeHealthChecks
420
+ });
421
+ async function checkKnowledgeHealth(db, logger) {
422
+ const [byBucket, totalChunksRes, totalEmbeddingsRes, orphanedRes, missingRes] = await Promise.all(
423
+ [
424
+ db.execute("SELECT bucket, COUNT(*) as count FROM chunks GROUP BY bucket"),
425
+ db.execute("SELECT COUNT(*) as count FROM chunks"),
426
+ db.execute("SELECT COUNT(*) as count FROM embeddings"),
427
+ db.execute(
428
+ "SELECT COUNT(*) as count FROM embeddings e LEFT JOIN chunks c ON c.id = e.id WHERE c.id IS NULL"
429
+ ),
430
+ db.execute(
431
+ "SELECT COUNT(*) as count FROM chunks c LEFT JOIN embeddings e ON e.id = c.id WHERE e.id IS NULL"
432
+ )
433
+ ]
434
+ );
435
+ const chunksByBucket = {};
436
+ for (const row of byBucket.rows) {
437
+ chunksByBucket[String(row.bucket)] = Number(row.count ?? 0);
438
+ }
439
+ const health = {
440
+ chunksByBucket,
441
+ totalChunks: Number(totalChunksRes.rows[0]?.count ?? 0),
442
+ totalEmbeddings: Number(totalEmbeddingsRes.rows[0]?.count ?? 0),
443
+ orphanedEmbeddings: Number(orphanedRes.rows[0]?.count ?? 0),
444
+ missingEmbeddings: Number(missingRes.rows[0]?.count ?? 0)
445
+ };
446
+ const summary = `chunksByBucket=${JSON.stringify(health.chunksByBucket)} totalChunks=${health.totalChunks} totalEmbeddings=${health.totalEmbeddings} orphanedEmbeddings=${health.orphanedEmbeddings} missingEmbeddings=${health.missingEmbeddings}`;
447
+ if (health.totalChunks === 0 || health.orphanedEmbeddings > 0) {
448
+ logger.warn(`Knowledge base health check: ${summary}`);
449
+ } else {
450
+ logger.info(`Knowledge base health check: ${summary}`);
451
+ }
452
+ return health;
453
+ }
454
+ async function cleanOrphanedEmbeddings(db, buildLock, instanceId, logger) {
455
+ const acquired = await buildLock.tryAcquire(
456
+ BUILD_LOCK_KEY,
457
+ instanceId,
458
+ Date.now(),
459
+ BUILD_LOCK_STALE_MS
460
+ );
461
+ if (!acquired) {
462
+ logger.warn(
463
+ "Knowledge health: skipping orphaned-embeddings cleanup - the build lock is held by another instance."
464
+ );
465
+ return;
466
+ }
467
+ try {
468
+ const result = await db.execute(
469
+ "DELETE FROM embeddings WHERE id NOT IN (SELECT id FROM chunks)"
470
+ );
471
+ if (result.rowsAffected > 0) {
472
+ logger.warn(`Knowledge health: deleted ${result.rowsAffected} orphaned embedding row(s).`);
473
+ }
474
+ } finally {
475
+ await buildLock.release(BUILD_LOCK_KEY, instanceId);
476
+ }
477
+ }
478
+ async function scheduleKnowledgeHealthChecks(options) {
479
+ const logger = options.logger ?? createConsoleLogger();
480
+ const enabled = options.config?.enabled !== false;
481
+ const intervalMs = options.config?.intervalMs ?? DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS;
482
+ const autoClean = options.config?.autoCleanOrphanedEmbeddings === true;
483
+ const buildLock = options.buildLock ?? createTursoBuildLock(options.db);
484
+ const instanceId = options.instanceId ?? crypto.randomUUID();
485
+ const schedule = options.schedule ?? defaultSchedule;
486
+ async function runOnce() {
487
+ const health = await checkKnowledgeHealth(options.db, logger);
488
+ if (autoClean && health.orphanedEmbeddings > 0) {
489
+ await cleanOrphanedEmbeddings(options.db, buildLock, instanceId, logger);
490
+ }
491
+ return health;
492
+ }
493
+ let handle;
494
+ if (enabled) {
495
+ await runOnce();
496
+ if (intervalMs > 0) {
497
+ let ticking = false;
498
+ handle = schedule(() => {
499
+ if (ticking) return;
500
+ ticking = true;
501
+ runOnce().catch((error) => logger.error("Knowledge health check failed:", error)).finally(() => {
502
+ ticking = false;
503
+ });
504
+ }, intervalMs);
505
+ }
506
+ }
507
+ return {
508
+ runOnce,
509
+ stop: () => handle?.stop()
510
+ };
511
+ }
512
+ var DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS, defaultSchedule;
513
+ var init_knowledgeHealth = __esm({
514
+ "src/core/knowledgeHealth.ts"() {
515
+ "use strict";
516
+ init_buildLock();
517
+ init_logger();
518
+ DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS = 30 * 6e4;
519
+ defaultSchedule = (fn, ms) => {
520
+ const timer = setInterval(fn, ms);
521
+ if (typeof timer === "object" && "unref" in timer) timer.unref();
522
+ return { stop: () => clearInterval(timer) };
523
+ };
524
+ }
525
+ });
526
+
414
527
  // src/server.ts
415
528
  var server_exports = {};
416
529
  __export(server_exports, {
@@ -1891,6 +2004,15 @@ async function createServer(config) {
1891
2004
  }
1892
2005
  await store.build?.();
1893
2006
  logger.info("\u2705 Knowledge base ready");
2007
+ let knowledgeHealth;
2008
+ if (db && !config.retriever) {
2009
+ const { scheduleKnowledgeHealthChecks: scheduleKnowledgeHealthChecks2 } = await Promise.resolve().then(() => (init_knowledgeHealth(), knowledgeHealth_exports));
2010
+ knowledgeHealth = await scheduleKnowledgeHealthChecks2({
2011
+ db,
2012
+ config: config.knowledgeHealthCheck,
2013
+ logger
2014
+ });
2015
+ }
1894
2016
  const prompts = new PromptLoader(promptsDir, config.bot);
1895
2017
  let apiKeyManager;
1896
2018
  const secret = config.auth?.secret || process.env.CHATTER_SECRET;
@@ -2021,6 +2143,7 @@ async function createServer(config) {
2021
2143
  // (sync or async) is caught per-channel so one misbehaving channel can't
2022
2144
  // stop the rest from cleaning up.
2023
2145
  stopChannels: async () => {
2146
+ knowledgeHealth?.stop();
2024
2147
  await Promise.allSettled(
2025
2148
  started.map(async (channel) => {
2026
2149
  try {