@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/channels/whatsapp/channel.d.ts +14 -0
- package/dist/channels/whatsapp/channel.d.ts.map +1 -1
- package/dist/channels/whatsapp/index.js +65 -25
- package/dist/channels/whatsapp/index.mjs +65 -25
- package/dist/core/knowledgeHealth.d.ts +70 -0
- package/dist/core/knowledgeHealth.d.ts.map +1 -0
- package/dist/index.js +137 -2
- package/dist/index.mjs +137 -2
- package/dist/mcp-server.d.ts +3 -1
- package/dist/mcp-server.d.ts.map +1 -1
- package/dist/mcp-server.js +127 -2
- package/dist/mcp-server.mjs +127 -2
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +123 -0
- package/dist/server.mjs +123 -0
- package/dist/types.d.ts +10 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -90,6 +90,15 @@ export interface LeaseGatedConnectDeps {
|
|
|
90
90
|
waitMs?: number;
|
|
91
91
|
schedule?: (fn: () => void, ms: number) => void;
|
|
92
92
|
logger?: Logger;
|
|
93
|
+
/**
|
|
94
|
+
* Recovery for a `connect` that threw, replacing the default (release the
|
|
95
|
+
* session, re-check after `waitMs`). The channel injects one so a thrown
|
|
96
|
+
* connect backs off on the same failure counter a closed connection uses
|
|
97
|
+
* instead of re-attempting at a flat interval. Whatever it does, it must
|
|
98
|
+
* end with another attempt scheduled: a session whose lease is held with
|
|
99
|
+
* no live socket and nothing pending never recovers on its own.
|
|
100
|
+
*/
|
|
101
|
+
onConnectFailed?: (sessionId: string, error: unknown) => void;
|
|
93
102
|
}
|
|
94
103
|
/**
|
|
95
104
|
* Acquire `sessionId`'s lease before connecting — two instances must never
|
|
@@ -99,6 +108,11 @@ export interface LeaseGatedConnectDeps {
|
|
|
99
108
|
* heartbeat ever finds the lease gone (this instance went stale and another
|
|
100
109
|
* took over), the session is torn down and this re-enters the same
|
|
101
110
|
* wait-and-retry loop rather than staying connected alongside the new holder.
|
|
111
|
+
*
|
|
112
|
+
* Every failed connect attempt lands back here too, whether it threw on its
|
|
113
|
+
* first try or on a reconnect: this loop is the only way a session recovers,
|
|
114
|
+
* so an attempt that fails without leaving a socket behind (no socket, no
|
|
115
|
+
* future "close" event) must re-enter it rather than stop at a log line.
|
|
102
116
|
*/
|
|
103
117
|
export declare function acquireSessionLease(sessionId: string, deps: LeaseGatedConnectDeps): Promise<void>;
|
|
104
118
|
/** Sender-registry name for a session — the default session stays unprefixed so single-number deployments see a plain `"whatsapp"` name. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAgB,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEjF,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAErE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,KAAK,EAAoB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE9D,OAAO,EAAE,KAAK,OAAO,EAAmD,MAAM,WAAW,CAAC;AAC1F,OAAO,EAKL,KAAK,YAAY,EAClB,MAAM,SAAS,CAAC;AAKjB;gFACgF;AAChF,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEzD;AAED,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,CAAC;IACf,OAAO,EAAE,SAAS,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,uJAAuJ;IACvJ,aAAa,EAAE,MAAM,CAAC;IACtB,8JAA8J;IAC9J,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qLAAqL;IACrL,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,iIAAiI;IACjI,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8KAA8K;IAC9K,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,4KAA4K;IAC5K,SAAS,CAAC,EAAE,QAAQ,CAAC;CACtB;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE;QAAE,GAAG,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,CAAA;KAAE,CAAC;IACvC,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ9F;
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../../src/channels/whatsapp/channel.ts"],"names":[],"mappings":"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"}
|
|
@@ -360,11 +360,17 @@ async function shutdownWaSessions(sessions) {
|
|
|
360
360
|
})
|
|
361
361
|
);
|
|
362
362
|
}
|
|
363
|
-
|
|
363
|
+
function claimSession(sessionId, sessions) {
|
|
364
364
|
const handle = sessions.get(sessionId);
|
|
365
|
-
if (!handle) return;
|
|
365
|
+
if (!handle) return void 0;
|
|
366
366
|
sessions.delete(sessionId);
|
|
367
|
+
return handle;
|
|
368
|
+
}
|
|
369
|
+
async function loseSession(sessionId, sessions) {
|
|
370
|
+
const handle = claimSession(sessionId, sessions);
|
|
371
|
+
if (!handle) return false;
|
|
367
372
|
await shutdownWaSessions(/* @__PURE__ */ new Map([[sessionId, handle]]));
|
|
373
|
+
return true;
|
|
368
374
|
}
|
|
369
375
|
async function acquireSessionLease(sessionId, deps) {
|
|
370
376
|
const { leaseStore, instanceId, sessions, isStopped, connect } = deps;
|
|
@@ -400,7 +406,9 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
400
406
|
void leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs).then((stillHeld) => {
|
|
401
407
|
if (stillHeld || isStopped()) return;
|
|
402
408
|
logger.error(`WhatsApp[${sessionId}]: lost the lease to another instance \u2014 disconnecting`);
|
|
403
|
-
void loseSession(sessionId, sessions).then(
|
|
409
|
+
void loseSession(sessionId, sessions).then((lost) => {
|
|
410
|
+
if (lost) retry();
|
|
411
|
+
});
|
|
404
412
|
}).catch((error) => logger.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
|
|
405
413
|
}, heartbeatMs);
|
|
406
414
|
if (typeof heartbeat === "object" && "unref" in heartbeat) heartbeat.unref();
|
|
@@ -412,6 +420,10 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
412
420
|
try {
|
|
413
421
|
await connect(sessionId);
|
|
414
422
|
} catch (error) {
|
|
423
|
+
if (deps.onConnectFailed) {
|
|
424
|
+
deps.onConnectFailed(sessionId, error);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
415
427
|
logger.error(`WhatsApp[${sessionId}]: connect failed right after acquiring the lease:`, error);
|
|
416
428
|
await loseSession(sessionId, sessions);
|
|
417
429
|
retry();
|
|
@@ -458,6 +470,50 @@ function createWhatsAppChannel(config) {
|
|
|
458
470
|
const authStore = config.authStore ?? createTursoWaAuthKV(deps.db);
|
|
459
471
|
senders = deps.senders;
|
|
460
472
|
await loadBaileys2();
|
|
473
|
+
const leaseDeps = {
|
|
474
|
+
leaseStore,
|
|
475
|
+
instanceId,
|
|
476
|
+
sessions,
|
|
477
|
+
isStopped: () => stopped,
|
|
478
|
+
connect,
|
|
479
|
+
now,
|
|
480
|
+
staleMs,
|
|
481
|
+
heartbeatMs,
|
|
482
|
+
waitMs,
|
|
483
|
+
schedule,
|
|
484
|
+
logger: log,
|
|
485
|
+
onConnectFailed: recoverFromFailedConnect
|
|
486
|
+
};
|
|
487
|
+
function nextRetryDelayMs(sessionId) {
|
|
488
|
+
const failures = failureCounts.get(sessionId) ?? 0;
|
|
489
|
+
failureCounts.set(sessionId, failures + 1);
|
|
490
|
+
return reconnectDelayMs(failures);
|
|
491
|
+
}
|
|
492
|
+
function attemptConnect(sessionId) {
|
|
493
|
+
void connect(sessionId).catch((error) => recoverFromFailedConnect(sessionId, error));
|
|
494
|
+
}
|
|
495
|
+
function recoverFromFailedConnect(sessionId, error) {
|
|
496
|
+
if (stopped) return;
|
|
497
|
+
const handle = claimSession(sessionId, sessions);
|
|
498
|
+
if (!handle) return;
|
|
499
|
+
const delay = nextRetryDelayMs(sessionId);
|
|
500
|
+
log.error(
|
|
501
|
+
`WhatsApp[${sessionId}]: connect attempt failed, releasing the lease and retrying in ${Math.round(delay / 1e3)}s:`,
|
|
502
|
+
error
|
|
503
|
+
);
|
|
504
|
+
schedule(
|
|
505
|
+
() => void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
506
|
+
(retryError) => log.error(`WhatsApp[${sessionId}]: lease re-check failed:`, retryError)
|
|
507
|
+
),
|
|
508
|
+
delay
|
|
509
|
+
);
|
|
510
|
+
void shutdownWaSessions(/* @__PURE__ */ new Map([[sessionId, handle]])).catch(
|
|
511
|
+
(teardownError) => log.warn(
|
|
512
|
+
`WhatsApp[${sessionId}]: teardown after a failed connect failed:`,
|
|
513
|
+
teardownError
|
|
514
|
+
)
|
|
515
|
+
);
|
|
516
|
+
}
|
|
461
517
|
async function connect(sessionId) {
|
|
462
518
|
if (stopped || !sessions.has(sessionId)) return;
|
|
463
519
|
const baileys = await loadBaileys2();
|
|
@@ -476,7 +532,7 @@ function createWhatsAppChannel(config) {
|
|
|
476
532
|
log.warn(
|
|
477
533
|
`WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
|
|
478
534
|
);
|
|
479
|
-
schedule(() =>
|
|
535
|
+
schedule(() => attemptConnect(sessionId), 6e4);
|
|
480
536
|
return;
|
|
481
537
|
}
|
|
482
538
|
const { version } = await baileys.fetchLatestBaileysVersion();
|
|
@@ -542,17 +598,11 @@ function createWhatsAppChannel(config) {
|
|
|
542
598
|
return;
|
|
543
599
|
}
|
|
544
600
|
if (!stopped && sessions.has(sessionId)) {
|
|
545
|
-
const delay =
|
|
546
|
-
failureCounts.set(sessionId, (failureCounts.get(sessionId) ?? 0) + 1);
|
|
601
|
+
const delay = nextRetryDelayMs(sessionId);
|
|
547
602
|
log.warn(
|
|
548
603
|
`WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
|
|
549
604
|
);
|
|
550
|
-
schedule(
|
|
551
|
-
() => void connect(sessionId).catch(
|
|
552
|
-
(error) => log.error(`WhatsApp[${sessionId}]: reconnect failed:`, error)
|
|
553
|
-
),
|
|
554
|
-
delay
|
|
555
|
-
);
|
|
605
|
+
schedule(() => attemptConnect(sessionId), delay);
|
|
556
606
|
}
|
|
557
607
|
}
|
|
558
608
|
});
|
|
@@ -570,19 +620,9 @@ function createWhatsAppChannel(config) {
|
|
|
570
620
|
}
|
|
571
621
|
}
|
|
572
622
|
for (const sessionId of sessionIds) {
|
|
573
|
-
void acquireSessionLease(sessionId,
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
sessions,
|
|
577
|
-
isStopped: () => stopped,
|
|
578
|
-
connect,
|
|
579
|
-
now,
|
|
580
|
-
staleMs,
|
|
581
|
-
heartbeatMs,
|
|
582
|
-
waitMs,
|
|
583
|
-
schedule,
|
|
584
|
-
logger: log
|
|
585
|
-
}).catch((error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error));
|
|
623
|
+
void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
624
|
+
(error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error)
|
|
625
|
+
);
|
|
586
626
|
}
|
|
587
627
|
},
|
|
588
628
|
async stop() {
|
|
@@ -295,11 +295,17 @@ async function shutdownWaSessions(sessions) {
|
|
|
295
295
|
})
|
|
296
296
|
);
|
|
297
297
|
}
|
|
298
|
-
|
|
298
|
+
function claimSession(sessionId, sessions) {
|
|
299
299
|
const handle = sessions.get(sessionId);
|
|
300
|
-
if (!handle) return;
|
|
300
|
+
if (!handle) return void 0;
|
|
301
301
|
sessions.delete(sessionId);
|
|
302
|
+
return handle;
|
|
303
|
+
}
|
|
304
|
+
async function loseSession(sessionId, sessions) {
|
|
305
|
+
const handle = claimSession(sessionId, sessions);
|
|
306
|
+
if (!handle) return false;
|
|
302
307
|
await shutdownWaSessions(/* @__PURE__ */ new Map([[sessionId, handle]]));
|
|
308
|
+
return true;
|
|
303
309
|
}
|
|
304
310
|
async function acquireSessionLease(sessionId, deps) {
|
|
305
311
|
const { leaseStore, instanceId, sessions, isStopped, connect } = deps;
|
|
@@ -335,7 +341,9 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
335
341
|
void leaseStore.tryAcquire(sessionId, instanceId, now(), staleMs).then((stillHeld) => {
|
|
336
342
|
if (stillHeld || isStopped()) return;
|
|
337
343
|
logger.error(`WhatsApp[${sessionId}]: lost the lease to another instance \u2014 disconnecting`);
|
|
338
|
-
void loseSession(sessionId, sessions).then(
|
|
344
|
+
void loseSession(sessionId, sessions).then((lost) => {
|
|
345
|
+
if (lost) retry();
|
|
346
|
+
});
|
|
339
347
|
}).catch((error) => logger.warn(`WhatsApp[${sessionId}]: lease heartbeat failed:`, error));
|
|
340
348
|
}, heartbeatMs);
|
|
341
349
|
if (typeof heartbeat === "object" && "unref" in heartbeat) heartbeat.unref();
|
|
@@ -347,6 +355,10 @@ async function acquireSessionLease(sessionId, deps) {
|
|
|
347
355
|
try {
|
|
348
356
|
await connect(sessionId);
|
|
349
357
|
} catch (error) {
|
|
358
|
+
if (deps.onConnectFailed) {
|
|
359
|
+
deps.onConnectFailed(sessionId, error);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
350
362
|
logger.error(`WhatsApp[${sessionId}]: connect failed right after acquiring the lease:`, error);
|
|
351
363
|
await loseSession(sessionId, sessions);
|
|
352
364
|
retry();
|
|
@@ -393,6 +405,50 @@ function createWhatsAppChannel(config) {
|
|
|
393
405
|
const authStore = config.authStore ?? createTursoWaAuthKV(deps.db);
|
|
394
406
|
senders = deps.senders;
|
|
395
407
|
await loadBaileys2();
|
|
408
|
+
const leaseDeps = {
|
|
409
|
+
leaseStore,
|
|
410
|
+
instanceId,
|
|
411
|
+
sessions,
|
|
412
|
+
isStopped: () => stopped,
|
|
413
|
+
connect,
|
|
414
|
+
now,
|
|
415
|
+
staleMs,
|
|
416
|
+
heartbeatMs,
|
|
417
|
+
waitMs,
|
|
418
|
+
schedule,
|
|
419
|
+
logger: log,
|
|
420
|
+
onConnectFailed: recoverFromFailedConnect
|
|
421
|
+
};
|
|
422
|
+
function nextRetryDelayMs(sessionId) {
|
|
423
|
+
const failures = failureCounts.get(sessionId) ?? 0;
|
|
424
|
+
failureCounts.set(sessionId, failures + 1);
|
|
425
|
+
return reconnectDelayMs(failures);
|
|
426
|
+
}
|
|
427
|
+
function attemptConnect(sessionId) {
|
|
428
|
+
void connect(sessionId).catch((error) => recoverFromFailedConnect(sessionId, error));
|
|
429
|
+
}
|
|
430
|
+
function recoverFromFailedConnect(sessionId, error) {
|
|
431
|
+
if (stopped) return;
|
|
432
|
+
const handle = claimSession(sessionId, sessions);
|
|
433
|
+
if (!handle) return;
|
|
434
|
+
const delay = nextRetryDelayMs(sessionId);
|
|
435
|
+
log.error(
|
|
436
|
+
`WhatsApp[${sessionId}]: connect attempt failed, releasing the lease and retrying in ${Math.round(delay / 1e3)}s:`,
|
|
437
|
+
error
|
|
438
|
+
);
|
|
439
|
+
schedule(
|
|
440
|
+
() => void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
441
|
+
(retryError) => log.error(`WhatsApp[${sessionId}]: lease re-check failed:`, retryError)
|
|
442
|
+
),
|
|
443
|
+
delay
|
|
444
|
+
);
|
|
445
|
+
void shutdownWaSessions(/* @__PURE__ */ new Map([[sessionId, handle]])).catch(
|
|
446
|
+
(teardownError) => log.warn(
|
|
447
|
+
`WhatsApp[${sessionId}]: teardown after a failed connect failed:`,
|
|
448
|
+
teardownError
|
|
449
|
+
)
|
|
450
|
+
);
|
|
451
|
+
}
|
|
396
452
|
async function connect(sessionId) {
|
|
397
453
|
if (stopped || !sessions.has(sessionId)) return;
|
|
398
454
|
const baileys = await loadBaileys2();
|
|
@@ -411,7 +467,7 @@ function createWhatsAppChannel(config) {
|
|
|
411
467
|
log.warn(
|
|
412
468
|
`WhatsApp[${sessionId}]: no paired session found \u2014 pair it first (see docs/channels.md). Re-checking in 60s.`
|
|
413
469
|
);
|
|
414
|
-
schedule(() =>
|
|
470
|
+
schedule(() => attemptConnect(sessionId), 6e4);
|
|
415
471
|
return;
|
|
416
472
|
}
|
|
417
473
|
const { version } = await baileys.fetchLatestBaileysVersion();
|
|
@@ -477,17 +533,11 @@ function createWhatsAppChannel(config) {
|
|
|
477
533
|
return;
|
|
478
534
|
}
|
|
479
535
|
if (!stopped && sessions.has(sessionId)) {
|
|
480
|
-
const delay =
|
|
481
|
-
failureCounts.set(sessionId, (failureCounts.get(sessionId) ?? 0) + 1);
|
|
536
|
+
const delay = nextRetryDelayMs(sessionId);
|
|
482
537
|
log.warn(
|
|
483
538
|
`WhatsApp[${sessionId}]: connection closed (${statusCode ?? "?"}), reconnecting in ${Math.round(delay / 1e3)}s...`
|
|
484
539
|
);
|
|
485
|
-
schedule(
|
|
486
|
-
() => void connect(sessionId).catch(
|
|
487
|
-
(error) => log.error(`WhatsApp[${sessionId}]: reconnect failed:`, error)
|
|
488
|
-
),
|
|
489
|
-
delay
|
|
490
|
-
);
|
|
540
|
+
schedule(() => attemptConnect(sessionId), delay);
|
|
491
541
|
}
|
|
492
542
|
}
|
|
493
543
|
});
|
|
@@ -505,19 +555,9 @@ function createWhatsAppChannel(config) {
|
|
|
505
555
|
}
|
|
506
556
|
}
|
|
507
557
|
for (const sessionId of sessionIds) {
|
|
508
|
-
void acquireSessionLease(sessionId,
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
sessions,
|
|
512
|
-
isStopped: () => stopped,
|
|
513
|
-
connect,
|
|
514
|
-
now,
|
|
515
|
-
staleMs,
|
|
516
|
-
heartbeatMs,
|
|
517
|
-
waitMs,
|
|
518
|
-
schedule,
|
|
519
|
-
logger: log
|
|
520
|
-
}).catch((error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error));
|
|
558
|
+
void acquireSessionLease(sessionId, leaseDeps).catch(
|
|
559
|
+
(error) => log.error(`WhatsApp[${sessionId}]: failed to start:`, error)
|
|
560
|
+
);
|
|
521
561
|
}
|
|
522
562
|
},
|
|
523
563
|
async stop() {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-boot and periodic health checks for the default `VectorStore`
|
|
3
|
+
* knowledge base, so a collapse is visible in logs the moment it happens
|
|
4
|
+
* instead of waiting for a user to notice broken answers - the gap that let
|
|
5
|
+
* two knowledge-base wipes go unnoticed until someone spotted broken
|
|
6
|
+
* answers.
|
|
7
|
+
*/
|
|
8
|
+
import type { Client as LibsqlClient } from "@libsql/client";
|
|
9
|
+
import { type BuildLock } from "./buildLock";
|
|
10
|
+
import { type Logger } from "./logger";
|
|
11
|
+
export interface KnowledgeHealth {
|
|
12
|
+
chunksByBucket: Record<string, number>;
|
|
13
|
+
totalChunks: number;
|
|
14
|
+
totalEmbeddings: number;
|
|
15
|
+
/** Embeddings rows with no matching chunks row. */
|
|
16
|
+
orphanedEmbeddings: number;
|
|
17
|
+
/** Chunks rows with no matching embeddings row (e.g. a build interrupted mid-embed). */
|
|
18
|
+
missingEmbeddings: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read-only snapshot of chunks/embeddings consistency, logged at `info` -
|
|
22
|
+
* escalating to `warn` when `totalChunks === 0` (the exact signature of both
|
|
23
|
+
* knowledge-base wipes) or `orphanedEmbeddings > 0`. The query shape mirrors
|
|
24
|
+
* the diagnostic used live to investigate the second incident.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkKnowledgeHealth(db: LibsqlClient, logger: Logger): Promise<KnowledgeHealth>;
|
|
27
|
+
export interface KnowledgeHealthCheckConfig {
|
|
28
|
+
/** Run post-boot and periodic health checks. Default: true. */
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
/** How often to re-run the check after boot. Default: 30 minutes. */
|
|
31
|
+
intervalMs?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Delete orphaned embeddings rows (no matching chunk) whenever a check
|
|
34
|
+
* finds them. Default: false - opt-in, since it's a delete even though a
|
|
35
|
+
* safe/idempotent one. Under normal operation orphans should be rare
|
|
36
|
+
* (`cleanupStaleChunks` already deletes a stale chunk and its embedding
|
|
37
|
+
* together); this exists chiefly to clean up historical damage and catch
|
|
38
|
+
* any future code path that doesn't maintain that invariant.
|
|
39
|
+
*/
|
|
40
|
+
autoCleanOrphanedEmbeddings?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export declare const DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS: number;
|
|
43
|
+
export interface KnowledgeHealthScheduler {
|
|
44
|
+
/** Runs one check immediately, applying auto-clean if configured. */
|
|
45
|
+
runOnce(): Promise<KnowledgeHealth>;
|
|
46
|
+
/** Stops periodic re-checks. Safe to call even if periodic checks never started. */
|
|
47
|
+
stop(): void;
|
|
48
|
+
}
|
|
49
|
+
export interface ScheduleKnowledgeHealthChecksOptions {
|
|
50
|
+
db: LibsqlClient;
|
|
51
|
+
config?: KnowledgeHealthCheckConfig;
|
|
52
|
+
/** Default: a console logger writing to stderr. */
|
|
53
|
+
logger?: Logger;
|
|
54
|
+
/** Default: a lock table in `db` (see buildLock.ts) - the same lock `VectorStore.build()` uses. */
|
|
55
|
+
buildLock?: BuildLock;
|
|
56
|
+
/** Identifies this process while it holds the build lock during cleanup. Default: a random id. */
|
|
57
|
+
instanceId?: string;
|
|
58
|
+
/** Injectable for tests; defaults to `setInterval`/`clearInterval`. */
|
|
59
|
+
schedule?: (fn: () => void, ms: number) => {
|
|
60
|
+
stop: () => void;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Runs {@link checkKnowledgeHealth} once immediately and, unless disabled,
|
|
65
|
+
* again on `intervalMs` via an unref'd timer - so it never blocks process
|
|
66
|
+
* exit or leaves an open handle in tests. `enabled: false` skips both the
|
|
67
|
+
* immediate run and the periodic one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function scheduleKnowledgeHealthChecks(options: ScheduleKnowledgeHealthChecksOptions): Promise<KnowledgeHealthScheduler>;
|
|
70
|
+
//# sourceMappingURL=knowledgeHealth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"knowledgeHealth.d.ts","sourceRoot":"","sources":["../../src/core/knowledgeHealth.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAGL,KAAK,SAAS,EAEf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAuB,KAAK,MAAM,EAAE,MAAM,UAAU,CAAC;AAE5D,MAAM,WAAW,eAAe;IAC9B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,CAAC,CAuC1B;AAsCD,MAAM,WAAW,0BAA0B;IACzC,+DAA+D;IAC/D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC;AAED,eAAO,MAAM,oCAAoC,QAAc,CAAC;AAEhE,MAAM,WAAW,wBAAwB;IACvC,qEAAqE;IACrE,OAAO,IAAI,OAAO,CAAC,eAAe,CAAC,CAAC;IACpC,oFAAoF;IACpF,IAAI,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,oCAAoC;IACnD,EAAE,EAAE,YAAY,CAAC;IACjB,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,kGAAkG;IAClG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CACjE;AAQD;;;;;GAKG;AACH,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,oCAAoC,GAC5C,OAAO,CAAC,wBAAwB,CAAC,CAyCnC"}
|
package/dist/index.js
CHANGED
|
@@ -416,6 +416,119 @@ var init_retrieval = __esm({
|
|
|
416
416
|
}
|
|
417
417
|
});
|
|
418
418
|
|
|
419
|
+
// src/core/knowledgeHealth.ts
|
|
420
|
+
var knowledgeHealth_exports = {};
|
|
421
|
+
__export(knowledgeHealth_exports, {
|
|
422
|
+
DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS: () => DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS,
|
|
423
|
+
checkKnowledgeHealth: () => checkKnowledgeHealth,
|
|
424
|
+
scheduleKnowledgeHealthChecks: () => scheduleKnowledgeHealthChecks
|
|
425
|
+
});
|
|
426
|
+
async function checkKnowledgeHealth(db, logger) {
|
|
427
|
+
const [byBucket, totalChunksRes, totalEmbeddingsRes, orphanedRes, missingRes] = await Promise.all(
|
|
428
|
+
[
|
|
429
|
+
db.execute("SELECT bucket, COUNT(*) as count FROM chunks GROUP BY bucket"),
|
|
430
|
+
db.execute("SELECT COUNT(*) as count FROM chunks"),
|
|
431
|
+
db.execute("SELECT COUNT(*) as count FROM embeddings"),
|
|
432
|
+
db.execute(
|
|
433
|
+
"SELECT COUNT(*) as count FROM embeddings e LEFT JOIN chunks c ON c.id = e.id WHERE c.id IS NULL"
|
|
434
|
+
),
|
|
435
|
+
db.execute(
|
|
436
|
+
"SELECT COUNT(*) as count FROM chunks c LEFT JOIN embeddings e ON e.id = c.id WHERE e.id IS NULL"
|
|
437
|
+
)
|
|
438
|
+
]
|
|
439
|
+
);
|
|
440
|
+
const chunksByBucket = {};
|
|
441
|
+
for (const row of byBucket.rows) {
|
|
442
|
+
chunksByBucket[String(row.bucket)] = Number(row.count ?? 0);
|
|
443
|
+
}
|
|
444
|
+
const health = {
|
|
445
|
+
chunksByBucket,
|
|
446
|
+
totalChunks: Number(totalChunksRes.rows[0]?.count ?? 0),
|
|
447
|
+
totalEmbeddings: Number(totalEmbeddingsRes.rows[0]?.count ?? 0),
|
|
448
|
+
orphanedEmbeddings: Number(orphanedRes.rows[0]?.count ?? 0),
|
|
449
|
+
missingEmbeddings: Number(missingRes.rows[0]?.count ?? 0)
|
|
450
|
+
};
|
|
451
|
+
const summary = `chunksByBucket=${JSON.stringify(health.chunksByBucket)} totalChunks=${health.totalChunks} totalEmbeddings=${health.totalEmbeddings} orphanedEmbeddings=${health.orphanedEmbeddings} missingEmbeddings=${health.missingEmbeddings}`;
|
|
452
|
+
if (health.totalChunks === 0 || health.orphanedEmbeddings > 0) {
|
|
453
|
+
logger.warn(`Knowledge base health check: ${summary}`);
|
|
454
|
+
} else {
|
|
455
|
+
logger.info(`Knowledge base health check: ${summary}`);
|
|
456
|
+
}
|
|
457
|
+
return health;
|
|
458
|
+
}
|
|
459
|
+
async function cleanOrphanedEmbeddings(db, buildLock, instanceId, logger) {
|
|
460
|
+
const acquired = await buildLock.tryAcquire(
|
|
461
|
+
BUILD_LOCK_KEY,
|
|
462
|
+
instanceId,
|
|
463
|
+
Date.now(),
|
|
464
|
+
BUILD_LOCK_STALE_MS
|
|
465
|
+
);
|
|
466
|
+
if (!acquired) {
|
|
467
|
+
logger.warn(
|
|
468
|
+
"Knowledge health: skipping orphaned-embeddings cleanup - the build lock is held by another instance."
|
|
469
|
+
);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
const result = await db.execute(
|
|
474
|
+
"DELETE FROM embeddings WHERE id NOT IN (SELECT id FROM chunks)"
|
|
475
|
+
);
|
|
476
|
+
if (result.rowsAffected > 0) {
|
|
477
|
+
logger.warn(`Knowledge health: deleted ${result.rowsAffected} orphaned embedding row(s).`);
|
|
478
|
+
}
|
|
479
|
+
} finally {
|
|
480
|
+
await buildLock.release(BUILD_LOCK_KEY, instanceId);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
async function scheduleKnowledgeHealthChecks(options) {
|
|
484
|
+
const logger = options.logger ?? createConsoleLogger();
|
|
485
|
+
const enabled = options.config?.enabled !== false;
|
|
486
|
+
const intervalMs = options.config?.intervalMs ?? DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS;
|
|
487
|
+
const autoClean = options.config?.autoCleanOrphanedEmbeddings === true;
|
|
488
|
+
const buildLock = options.buildLock ?? createTursoBuildLock(options.db);
|
|
489
|
+
const instanceId = options.instanceId ?? crypto.randomUUID();
|
|
490
|
+
const schedule = options.schedule ?? defaultSchedule;
|
|
491
|
+
async function runOnce() {
|
|
492
|
+
const health = await checkKnowledgeHealth(options.db, logger);
|
|
493
|
+
if (autoClean && health.orphanedEmbeddings > 0) {
|
|
494
|
+
await cleanOrphanedEmbeddings(options.db, buildLock, instanceId, logger);
|
|
495
|
+
}
|
|
496
|
+
return health;
|
|
497
|
+
}
|
|
498
|
+
let handle;
|
|
499
|
+
if (enabled) {
|
|
500
|
+
await runOnce();
|
|
501
|
+
if (intervalMs > 0) {
|
|
502
|
+
let ticking = false;
|
|
503
|
+
handle = schedule(() => {
|
|
504
|
+
if (ticking) return;
|
|
505
|
+
ticking = true;
|
|
506
|
+
runOnce().catch((error) => logger.error("Knowledge health check failed:", error)).finally(() => {
|
|
507
|
+
ticking = false;
|
|
508
|
+
});
|
|
509
|
+
}, intervalMs);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
runOnce,
|
|
514
|
+
stop: () => handle?.stop()
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
var DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS, defaultSchedule;
|
|
518
|
+
var init_knowledgeHealth = __esm({
|
|
519
|
+
"src/core/knowledgeHealth.ts"() {
|
|
520
|
+
"use strict";
|
|
521
|
+
init_buildLock();
|
|
522
|
+
init_logger();
|
|
523
|
+
DEFAULT_KNOWLEDGE_HEALTH_INTERVAL_MS = 30 * 6e4;
|
|
524
|
+
defaultSchedule = (fn, ms) => {
|
|
525
|
+
const timer = setInterval(fn, ms);
|
|
526
|
+
if (typeof timer === "object" && "unref" in timer) timer.unref();
|
|
527
|
+
return { stop: () => clearInterval(timer) };
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
419
532
|
// src/index.ts
|
|
420
533
|
var index_exports = {};
|
|
421
534
|
__export(index_exports, {
|
|
@@ -1221,6 +1334,7 @@ async function createMCPServer(config) {
|
|
|
1221
1334
|
});
|
|
1222
1335
|
const model = config.openai.model || DEFAULT_MODEL;
|
|
1223
1336
|
let store;
|
|
1337
|
+
let db;
|
|
1224
1338
|
if (config.retriever) {
|
|
1225
1339
|
store = config.retriever;
|
|
1226
1340
|
} else {
|
|
@@ -1228,7 +1342,7 @@ async function createMCPServer(config) {
|
|
|
1228
1342
|
throw new Error(DATABASE_CONFIG_REQUIRED_MESSAGE);
|
|
1229
1343
|
}
|
|
1230
1344
|
const { DEFAULT_KNOWLEDGE_DIR: DEFAULT_KNOWLEDGE_DIR2, VectorStore: VectorStore2, createOpenAIEmbedder: createOpenAIEmbedder2, openLibsqlClient: openLibsqlClient2 } = await Promise.resolve().then(() => (init_retrieval(), retrieval_exports));
|
|
1231
|
-
|
|
1345
|
+
db = await openLibsqlClient2(config.database);
|
|
1232
1346
|
store = new VectorStore2(createOpenAIEmbedder2(client), {
|
|
1233
1347
|
databaseClient: db,
|
|
1234
1348
|
knowledgeDir: config.knowledgeDir || DEFAULT_KNOWLEDGE_DIR2,
|
|
@@ -1237,6 +1351,15 @@ async function createMCPServer(config) {
|
|
|
1237
1351
|
}
|
|
1238
1352
|
await store.build?.();
|
|
1239
1353
|
log.info("\u2705 Knowledge base ready");
|
|
1354
|
+
let knowledgeHealth;
|
|
1355
|
+
if (db) {
|
|
1356
|
+
const { scheduleKnowledgeHealthChecks: scheduleKnowledgeHealthChecks2 } = await Promise.resolve().then(() => (init_knowledgeHealth(), knowledgeHealth_exports));
|
|
1357
|
+
knowledgeHealth = await scheduleKnowledgeHealthChecks2({
|
|
1358
|
+
db,
|
|
1359
|
+
config: config.knowledgeHealthCheck,
|
|
1360
|
+
logger: log
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1240
1363
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
1241
1364
|
const server = new McpServer({
|
|
1242
1365
|
name: config.bot.name,
|
|
@@ -1433,7 +1556,9 @@ async function createMCPServer(config) {
|
|
|
1433
1556
|
log.info(
|
|
1434
1557
|
`\u2705 ${config.bot.name} MCP Server ready (transport: ${transportMode}, tools: ${enabledTools.join(", ") || "none"})`
|
|
1435
1558
|
);
|
|
1436
|
-
return server
|
|
1559
|
+
return Object.assign(server, {
|
|
1560
|
+
stopKnowledgeHealthChecks: () => knowledgeHealth?.stop()
|
|
1561
|
+
});
|
|
1437
1562
|
}
|
|
1438
1563
|
|
|
1439
1564
|
// src/middleware/apiKey.ts
|
|
@@ -2416,6 +2541,15 @@ async function createServer(config) {
|
|
|
2416
2541
|
}
|
|
2417
2542
|
await store.build?.();
|
|
2418
2543
|
logger.info("\u2705 Knowledge base ready");
|
|
2544
|
+
let knowledgeHealth;
|
|
2545
|
+
if (db && !config.retriever) {
|
|
2546
|
+
const { scheduleKnowledgeHealthChecks: scheduleKnowledgeHealthChecks2 } = await Promise.resolve().then(() => (init_knowledgeHealth(), knowledgeHealth_exports));
|
|
2547
|
+
knowledgeHealth = await scheduleKnowledgeHealthChecks2({
|
|
2548
|
+
db,
|
|
2549
|
+
config: config.knowledgeHealthCheck,
|
|
2550
|
+
logger
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2419
2553
|
const prompts = new PromptLoader(promptsDir, config.bot);
|
|
2420
2554
|
let apiKeyManager;
|
|
2421
2555
|
const secret = config.auth?.secret || process.env.CHATTER_SECRET;
|
|
@@ -2546,6 +2680,7 @@ async function createServer(config) {
|
|
|
2546
2680
|
// (sync or async) is caught per-channel so one misbehaving channel can't
|
|
2547
2681
|
// stop the rest from cleaning up.
|
|
2548
2682
|
stopChannels: async () => {
|
|
2683
|
+
knowledgeHealth?.stop();
|
|
2549
2684
|
await Promise.allSettled(
|
|
2550
2685
|
started.map(async (channel) => {
|
|
2551
2686
|
try {
|