diff --git a/src/index.ts b/src/index.ts index da224ad..880895f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -38,7 +38,7 @@ import { import { testConnection, close } from './db.js'; import { toolDefinitions } from './tools/index.js'; -// Kept tools (sessions, memory, archives, infrastructure, docs, delegations, commits) +// Kept tools (sessions, archives, infrastructure, docs, delegations, commits) import { taskDelegations, taskDelegationQuery } from './tools/delegations.js'; import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js'; import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js'; @@ -53,7 +53,6 @@ import { impactLearn, componentGraph, } from './tools/impact.js'; -import { memoryAdd, memorySearch, memoryList, memoryContext } from './tools/memories.js'; import { toolDocAdd, toolDocSearch, toolDocGet, toolDocList, toolDocExport } from './tools/tool-docs.js'; import { sessionStart, @@ -230,38 +229,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { result = JSON.stringify(await componentGraph(a.component_id), null, 2); break; - // Memories - case 'memory_add': - result = await memoryAdd({ - category: a.category, - title: a.title, - content: a.content, - context: a.context, - project: a.project, - session_id: a.session_id, - task_id: a.task_id, - }); - break; - case 'memory_search': - result = await memorySearch({ - query: a.query, - project: a.project, - category: a.category, - limit: a.limit, - search_mode: a.search_mode, - }); - break; - case 'memory_list': - result = await memoryList({ - project: a.project, - category: a.category, - limit: a.limit, - }); - break; - case 'memory_context': - result = await memoryContext(a.project, a.task_description); - break; - // Tool Documentation case 'tool_doc_add': result = await toolDocAdd({ diff --git a/src/tools/index.ts b/src/tools/index.ts index e60b5b2..9a11db9 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -298,63 +298,6 @@ export const toolDefinitions = [ }, }, - // Memory Tools - { - name: 'memory_add', - description: 'Store a learning/memory for future sessions.', - inputSchema: { - type: 'object', - properties: { - category: { type: 'string', enum: ['pattern', 'fix', 'preference', 'gotcha', 'architecture'], description: 'Memory category' }, - title: { type: 'string', description: 'Short title' }, - content: { type: 'string', description: 'The learning/insight to remember' }, - context: { type: 'string', description: 'When/where this applies (optional)' }, - project: { type: 'string', description: 'Project (optional)' }, - session_id: { type: 'string', description: 'Session ID (optional)' }, - task_id: { type: 'string', description: 'Jira issue key (optional)' }, - }, - required: ['category', 'title', 'content'], - }, - }, - { - name: 'memory_search', - description: 'Search memories using hybrid (vector + keyword), vector-only, or keyword-only search.', - inputSchema: { - type: 'object', - properties: { - query: { type: 'string', description: 'Search query' }, - project: { type: 'string', description: 'Filter by project (optional)' }, - category: { type: 'string', enum: ['pattern', 'fix', 'preference', 'gotcha', 'architecture'], description: 'Filter by category (optional)' }, - limit: { type: 'number', description: 'Max results (default: 5)' }, - search_mode: { type: 'string', enum: ['hybrid', 'vector', 'keyword'], description: 'Search mode (default: hybrid)' }, - }, - required: ['query'], - }, - }, - { - name: 'memory_list', - description: 'List stored memories (non-semantic)', - inputSchema: { - type: 'object', - properties: { - project: { type: 'string', description: 'Filter by project (optional)' }, - category: { type: 'string', enum: ['pattern', 'fix', 'preference', 'gotcha', 'architecture'], description: 'Filter by category (optional)' }, - limit: { type: 'number', description: 'Max results (default: 20)' }, - }, - }, - }, - { - name: 'memory_context', - description: 'Get memories relevant to current session context.', - inputSchema: { - type: 'object', - properties: { - project: { type: 'string', description: 'Current project' }, - task_description: { type: 'string', description: 'Description of planned work' }, - }, - }, - }, - // Tool Documentation Tools { name: 'tool_doc_add', diff --git a/src/tools/memories.ts b/src/tools/memories.ts deleted file mode 100644 index 7c9b28a..0000000 --- a/src/tools/memories.ts +++ /dev/null @@ -1,340 +0,0 @@ -// Session memory operations for persistent learnings - -import { query, queryOne, execute } from '../db.js'; -import { getEmbedding, formatEmbedding, generateContentHash, rrfMerge } from '../embeddings.js'; - -type MemoryCategory = 'pattern' | 'fix' | 'preference' | 'gotcha' | 'architecture'; - -interface Memory { - id: number; - category: MemoryCategory; - title: string; - content: string; - context: string | null; - project: string | null; - session_id: string | null; - task_id: string | null; - access_count: number; - created_at: string; -} - -interface MemoryAddArgs { - category: MemoryCategory; - title: string; - content: string; - context?: string; - project?: string; - session_id?: string; - task_id?: string; -} - -type SearchMode = 'hybrid' | 'vector' | 'keyword'; - -interface MemorySearchArgs { - query: string; - project?: string; - category?: MemoryCategory; - limit?: number; - search_mode?: SearchMode; -} - -interface MemoryListArgs { - project?: string; - category?: MemoryCategory; - limit?: number; -} - -/** - * Add a new memory/learning (enhanced with session_id and task_id) - * CF-306: Validates session_id exists before inserting to prevent foreign key violations - */ -export async function memoryAdd(args: MemoryAddArgs): Promise { - const { category, title, content, context, project, session_id, task_id } = args; - - // CF-306: Validate session_id exists if provided - let validSessionId = session_id || null; - if (session_id) { - const sessionExists = await queryOne<{ exists: boolean }>( - `SELECT EXISTS(SELECT 1 FROM sessions WHERE id = $1) as exists`, - [session_id] - ); - if (!sessionExists?.exists) { - console.warn(`[CF-306] Session ${session_id} not found in database - using NULL instead`); - validSessionId = null; - } - } - - // CF-1314: Hash content for dedup before embedding API call - const embedText = `${title}. ${content}`; - const contentHash = generateContentHash(embedText); - - // Scope dedup to project if provided, otherwise global - const existing = project - ? await queryOne<{ id: number }>( - 'SELECT id FROM memories WHERE content_hash = $1 AND project = $2 LIMIT 1', - [contentHash, project] - ) - : await queryOne<{ id: number }>( - 'SELECT id FROM memories WHERE content_hash = $1 AND project IS NULL LIMIT 1', - [contentHash] - ); - if (existing) { - return `Memory already exists (id: ${existing.id}): [${category}] ${title}`; - } - - // Generate embedding for semantic search - const embedding = await getEmbedding(embedText); - const embeddingValue = embedding ? formatEmbedding(embedding) : null; - - await execute( - `INSERT INTO memories (category, title, content, context, project, session_id, task_id, embedding, content_hash) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, - [category, title, content, context || null, project || null, validSessionId, task_id || null, embeddingValue, contentHash] - ); - - return `Stored memory: [${category}] ${title}`; -} - -/** - * Search memories with hybrid (vector + keyword), vector-only, or keyword-only mode (CF-1315) - */ -export async function memorySearch(args: MemorySearchArgs): Promise { - const { query: searchQuery, project, category, limit = 5, search_mode = 'hybrid' } = args; - - // Build shared filter clause - const buildFilter = (startIdx: number) => { - let where = ''; - const params: unknown[] = []; - let idx = startIdx; - if (project) { - where += ` AND (project = $${idx++} OR project IS NULL)`; - params.push(project); - } - if (category) { - where += ` AND category = $${idx++}`; - params.push(category); - } - return { where, params, nextIdx: idx }; - }; - - // Vector search - let vectorIds: number[] = []; - let vectorRows: Map = new Map(); - let embeddingFailed = false; - - if (search_mode !== 'keyword') { - const embedding = await getEmbedding(searchQuery); - if (embedding) { - const embeddingStr = formatEmbedding(embedding); - const filter = buildFilter(3); - const params: unknown[] = [embeddingStr, limit, ...filter.params]; - - const rows = await query( - `SELECT id, category, title, content, context, project, access_count, - to_char(created_at, 'YYYY-MM-DD') as created_at, - 1 - (embedding <=> $1) as similarity - FROM memories - WHERE embedding IS NOT NULL${filter.where} - ORDER BY embedding <=> $1 - LIMIT $2`, - params - ); - vectorIds = rows.map(r => r.id); - for (const r of rows) vectorRows.set(r.id, r); - } else { - embeddingFailed = true; - if (search_mode === 'vector') { - return 'Error: Could not generate embedding for vector search'; - } - } - } - - // Keyword search - let keywordIds: number[] = []; - let keywordRows: Map = new Map(); - - if (search_mode !== 'vector') { - const filter = buildFilter(3); - const params: unknown[] = [searchQuery, limit, ...filter.params]; - - const rows = await query( - `SELECT id, category, title, content, context, project, access_count, - to_char(created_at, 'YYYY-MM-DD') as created_at, - ts_rank(search_vector, plainto_tsquery('english', $1)) as rank - FROM memories - WHERE search_vector @@ plainto_tsquery('english', $1)${filter.where} - ORDER BY rank DESC - LIMIT $2`, - params - ); - keywordIds = rows.map(r => r.id); - for (const r of rows) keywordRows.set(r.id, r); - } - - // Merge results - let finalIds: number[]; - let searchLabel: string; - - if (search_mode === 'hybrid' && vectorIds.length > 0 && keywordIds.length > 0) { - const merged = rrfMerge(vectorIds, keywordIds); - finalIds = merged.slice(0, limit).map(m => m.id as number); - searchLabel = 'hybrid'; - } else if (vectorIds.length > 0) { - finalIds = vectorIds; - searchLabel = 'vector'; - } else if (keywordIds.length > 0) { - finalIds = keywordIds; - searchLabel = embeddingFailed ? 'keyword (embedding unavailable)' : 'keyword'; - } else { - return 'No relevant memories found'; - } - - // Update access_count for returned memories - await execute( - `UPDATE memories SET access_count = access_count + 1, last_accessed_at = NOW() WHERE id = ANY($1)`, - [finalIds] - ); - - // Format output - const lines = [`Relevant memories (${searchLabel}):\n`]; - for (const id of finalIds) { - const m = vectorRows.get(id) || keywordRows.get(id); - if (!m) continue; - const sim = vectorRows.has(id) ? ` (${Math.round((vectorRows.get(id)!).similarity * 100)}% match)` : ''; - const proj = m.project ? ` [${m.project}]` : ''; - lines.push(`**[${m.category}]${proj}** ${m.title}${sim}`); - lines.push(` ${m.content}`); - if (m.context) { - lines.push(` _Context: ${m.context}_`); - } - lines.push(''); - } - - return lines.join('\n'); -} - -/** - * List memories (non-semantic) - */ -export async function memoryList(args: MemoryListArgs): Promise { - const { project, category, limit = 20 } = args; - - let whereClause = 'WHERE 1=1'; - const params: unknown[] = []; - let paramIndex = 1; - - if (project) { - whereClause += ` AND (project = $${paramIndex++} OR project IS NULL)`; - params.push(project); - } - if (category) { - whereClause += ` AND category = $${paramIndex++}`; - params.push(category); - } - - params.push(limit); - - const memories = await query( - `SELECT id, category, title, content, context, project, access_count, - to_char(created_at, 'YYYY-MM-DD') as created_at - FROM memories - ${whereClause} - ORDER BY created_at DESC - LIMIT $${paramIndex}`, - params - ); - - if (memories.length === 0) { - return `No memories found${project ? ` for project ${project}` : ''}`; - } - - const lines = [`Memories${project ? ` (${project})` : ''}:\n`]; - for (const m of memories) { - const proj = m.project ? `[${m.project}] ` : ''; - const accessed = m.access_count > 0 ? ` (accessed ${m.access_count}x)` : ''; - lines.push(`• [${m.category}] ${proj}${m.title}${accessed}`); - lines.push(` ${m.content.slice(0, 100)}${m.content.length > 100 ? '...' : ''}`); - } - - return lines.join('\n'); -} - -/** - * Delete a memory by ID - */ -export async function memoryDelete(id: number): Promise { - const result = await execute('DELETE FROM memories WHERE id = $1', [id]); - - if (result === 0) { - return `Memory not found: ${id}`; - } - - return `Deleted memory: ${id}`; -} - -/** - * Get memories relevant to current context (for session start) - */ -export async function memoryContext(project: string | null, taskDescription?: string): Promise { - const lines: string[] = []; - - // Get project-specific memories - if (project) { - const projectMemories = await query( - `SELECT category, title, content FROM memories - WHERE project = $1 - ORDER BY access_count DESC, created_at DESC - LIMIT 5`, - [project] - ); - - if (projectMemories.length > 0) { - lines.push(`**${project} Memories:**`); - for (const m of projectMemories) { - lines.push(`• [${m.category}] ${m.title}: ${m.content}`); - } - lines.push(''); - } - } - - // If task description provided, do semantic search - if (taskDescription) { - const embedding = await getEmbedding(taskDescription); - if (embedding) { - const relevant = await query( - `SELECT category, title, content, project - FROM memories - WHERE embedding IS NOT NULL - ORDER BY embedding <=> $1 - LIMIT 3`, - [formatEmbedding(embedding)] - ); - - if (relevant.length > 0) { - lines.push('**Relevant memories for this task:**'); - for (const m of relevant) { - const proj = m.project ? `[${m.project}] ` : ''; - lines.push(`• ${proj}${m.title}: ${m.content}`); - } - } - } - } - - // Get recent gotchas (always useful) - const gotchas = await query( - `SELECT title, content FROM memories - WHERE category = 'gotcha' - ORDER BY created_at DESC - LIMIT 3`, - [] - ); - - if (gotchas.length > 0) { - lines.push('\n**Recent gotchas:**'); - for (const g of gotchas) { - lines.push(`⚠️ ${g.title}: ${g.content}`); - } - } - - return lines.length > 0 ? lines.join('\n') : 'No memories to surface'; -}