feat: add heartbeat monitor and vector memory search (Tier 2)

Heartbeat:
- HeartbeatMonitor with 5 checks: gateway, model, channels, memory, disk
- Configurable interval, failure threshold, notification channel
- Recovery notifications when health restores
- 25 new tests

Vector Memory Search:
- EmbeddingProvider interface with OpenAI, Gemini, Ollama, LlamaCpp backends
- SQLite-backed VectorStore with cosine similarity search
- Text chunker with paragraph-aware splitting and overlap
- HybridSearch merging keyword + vector results with configurable weight
- Background indexer with dirty-namespace tracking
- Graceful fallback to keyword search when embeddings unavailable
- 51 new tests

Config: automation.heartbeat + memory.embedding schema sections
Total: 950 tests passing, all types clean
This commit is contained in:
William Valentin
2026-02-07 14:45:11 -08:00
parent b50c140d25
commit 88731a50e3
17 changed files with 2354 additions and 7 deletions
+3 -2
View File
@@ -22,6 +22,7 @@ export { createCronTools } from './cron.js';
import type { Tool } from '../types.js';
import type { MemoryStore } from '../../memory/store.js';
import type { HybridSearch } from '../../memory/hybrid-search.js';
import type { WebSearchConfig } from './web-search.js';
import { shellExecTool } from './shell.js';
import { fileReadTool } from './file-read.js';
@@ -47,11 +48,11 @@ export const allBuiltinTools: Tool[] = [
];
/** Create memory tools that require a MemoryStore instance. */
export function createMemoryTools(store: MemoryStore): Tool[] {
export function createMemoryTools(store: MemoryStore, hybridSearch?: HybridSearch): Tool[] {
return [
createMemoryReadTool(store),
createMemoryWriteTool(store),
createMemorySearchTool(store),
createMemorySearchTool(store, hybridSearch),
];
}
+33 -3
View File
@@ -1,5 +1,6 @@
import type { Tool, ToolResult } from '../types.js';
import type { MemoryStore } from '../../memory/store.js';
import type { HybridSearch } from '../../memory/hybrid-search.js';
interface MemorySearchArgs {
query: string;
@@ -7,13 +8,15 @@ interface MemorySearchArgs {
/**
* Creates a memory.search tool bound to the given MemoryStore instance.
* Searches across all memory namespaces for matching content.
* When a HybridSearch instance is provided, uses vector + keyword search;
* otherwise falls back to keyword-only search.
*/
export function createMemorySearchTool(store: MemoryStore): Tool {
export function createMemorySearchTool(store: MemoryStore, hybridSearch?: HybridSearch): Tool {
return {
name: 'memory.search',
description:
'Search across all memory files for a keyword or phrase. Returns matching lines with surrounding context from every namespace.',
'Search across all memory files for a keyword or phrase. Returns matching lines with surrounding context from every namespace.' +
(hybridSearch ? ' Uses semantic vector search combined with keyword matching for better results.' : ''),
inputSchema: {
type: 'object',
properties: {
@@ -28,6 +31,33 @@ export function createMemorySearchTool(store: MemoryStore): Tool {
const args = rawArgs as MemorySearchArgs;
try {
// Try hybrid search first if available
if (hybridSearch) {
try {
const results = await hybridSearch.search(args.query);
if (results.length === 0) {
return { success: true, output: `No matches found for "${args.query}".` };
}
const formatted = results.map((result) => {
const sourceLabel = result.source === 'both' ? 'keyword+vector'
: result.source === 'vector' ? 'vector'
: 'keyword';
return `[${result.namespace}:${result.line}] (${sourceLabel}, score: ${result.score.toFixed(3)}) ${result.content}\n context: ${result.context}`;
}).join('\n\n');
return {
success: true,
output: `Found ${results.length} match${results.length === 1 ? '' : 'es'} for "${args.query}":\n\n${formatted}`,
};
} catch (hybridError) {
// Fall back to keyword search on hybrid failure
console.error('Hybrid search failed, falling back to keyword search:', hybridError);
}
}
// Keyword-only fallback
const results = store.search(args.query);
if (results.length === 0) {