feat(CF-1351): Remove unused memory_* tools from session-mcp
Auto memory (MEMORY.md + topic files) is the sole memory system. The session-mcp memory_* tools (PostgreSQL + pgvector) had zero entries after months of use — removing dead code to simplify the server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
35
src/index.ts
35
src/index.ts
@@ -38,7 +38,7 @@ import {
|
|||||||
import { testConnection, close } from './db.js';
|
import { testConnection, close } from './db.js';
|
||||||
import { toolDefinitions } from './tools/index.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 { taskDelegations, taskDelegationQuery } from './tools/delegations.js';
|
||||||
import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js';
|
import { projectLock, projectUnlock, projectLockStatus, projectContext } from './tools/locks.js';
|
||||||
import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js';
|
import { taskCommitAdd, taskCommitRemove, taskCommitsList, taskLinkCommits, sessionTasks } from './tools/commits.js';
|
||||||
@@ -53,7 +53,6 @@ import {
|
|||||||
impactLearn,
|
impactLearn,
|
||||||
componentGraph,
|
componentGraph,
|
||||||
} from './tools/impact.js';
|
} 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 { toolDocAdd, toolDocSearch, toolDocGet, toolDocList, toolDocExport } from './tools/tool-docs.js';
|
||||||
import {
|
import {
|
||||||
sessionStart,
|
sessionStart,
|
||||||
@@ -230,38 +229,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|||||||
result = JSON.stringify(await componentGraph(a.component_id), null, 2);
|
result = JSON.stringify(await componentGraph(a.component_id), null, 2);
|
||||||
break;
|
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
|
// Tool Documentation
|
||||||
case 'tool_doc_add':
|
case 'tool_doc_add':
|
||||||
result = await toolDocAdd({
|
result = await toolDocAdd({
|
||||||
|
|||||||
@@ -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
|
// Tool Documentation Tools
|
||||||
{
|
{
|
||||||
name: 'tool_doc_add',
|
name: 'tool_doc_add',
|
||||||
|
|||||||
@@ -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<string> {
|
|
||||||
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<string> {
|
|
||||||
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<number, Memory & { similarity: number }> = 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<Memory & { similarity: number }>(
|
|
||||||
`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<number, Memory & { rank: number }> = new Map();
|
|
||||||
|
|
||||||
if (search_mode !== 'vector') {
|
|
||||||
const filter = buildFilter(3);
|
|
||||||
const params: unknown[] = [searchQuery, limit, ...filter.params];
|
|
||||||
|
|
||||||
const rows = await query<Memory & { rank: number }>(
|
|
||||||
`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<string> {
|
|
||||||
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<Memory>(
|
|
||||||
`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<string> {
|
|
||||||
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<string> {
|
|
||||||
const lines: string[] = [];
|
|
||||||
|
|
||||||
// Get project-specific memories
|
|
||||||
if (project) {
|
|
||||||
const projectMemories = await query<Memory>(
|
|
||||||
`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<Memory>(
|
|
||||||
`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<Memory>(
|
|
||||||
`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';
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user