From 709c5ff32456468835b9ff42f2a9367fd7d6a9a5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:42:11 +0000 Subject: [PATCH 1/7] Initial plan From 4a3bdc14339d53672b028c35b55304c32a28ce2b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 15:45:23 +0000 Subject: [PATCH 2/7] Initial commit - preparing for update-memory feature Co-authored-by: chrisribe <1999791+chrisribe@users.noreply.github.com> --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e452864..00018d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-memory-mcp", - "version": "1.0.0", + "version": "1.0.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-memory-mcp", - "version": "1.0.0", + "version": "1.0.10", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.3", diff --git a/package.json b/package.json index 6ceda9d..daa9458 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-memory-mcp", - "version": "1.0.10", + "version": "1.0.11", "description": "Simple memory MCP server for storing and retrieving memories with tags", "main": "dist/index.js", "type": "module", From 5a864971b2be58c19be970d446ba0423a2756db7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:08:47 +0000 Subject: [PATCH 3/7] Add update-memory functionality with tests Co-authored-by: chrisribe <1999791+chrisribe@users.noreply.github.com> --- package.json | 2 +- src/services/database-optimizer.ts | 14 +- src/services/memory-service.ts | 77 ++++++++++- src/services/migrations.ts | 24 +++- src/tests/memory-server-tests.ts | 183 ++++++++++++++++++++++++++ src/tools/index.ts | 2 + src/tools/update-memory/cli-parser.ts | 32 +++++ src/tools/update-memory/executor.ts | 58 ++++++++ src/tools/update-memory/index.ts | 58 ++++++++ 9 files changed, 440 insertions(+), 10 deletions(-) create mode 100644 src/tools/update-memory/cli-parser.ts create mode 100644 src/tools/update-memory/executor.ts create mode 100644 src/tools/update-memory/index.ts diff --git a/package.json b/package.json index daa9458..a6f8cba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-memory-mcp", - "version": "1.0.11", + "version": "1.0.23", "description": "Simple memory MCP server for storing and retrieving memories with tags", "main": "dist/index.js", "type": "module", diff --git a/src/services/database-optimizer.ts b/src/services/database-optimizer.ts index e7ea99e..1c96583 100644 --- a/src/services/database-optimizer.ts +++ b/src/services/database-optimizer.ts @@ -40,10 +40,18 @@ export class DatabaseOptimizer { */ static optimizeFTS(db: Database.Database): void { try { - db.exec(`INSERT INTO memories_fts(memories_fts) VALUES('optimize')`); - debugLog('FTS index optimized'); + // Check if FTS table exists and has data before optimizing + const tableExists = db.prepare(` + SELECT COUNT(*) as count FROM sqlite_master + WHERE type='table' AND name='memories_fts' + `).get() as any; + + if (tableExists && tableExists.count > 0) { + db.exec(`INSERT INTO memories_fts(memories_fts) VALUES('optimize')`); + debugLog('FTS index optimized'); + } } catch (error: any) { - // FTS table might not exist yet, that's fine + // FTS optimization is optional - don't fail if it doesn't work debugLog('FTS optimization skipped:', error.message); } } diff --git a/src/services/memory-service.ts b/src/services/memory-service.ts index 500c407..1b39bd3 100644 --- a/src/services/memory-service.ts +++ b/src/services/memory-service.ts @@ -171,8 +171,8 @@ export class MemoryService { // Create trigger to automatically update FTS when memories are updated this.db.exec(` CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN - UPDATE memories_fts SET content = new.content - WHERE rowid = new.id; + DELETE FROM memories_fts WHERE rowid = old.id; + INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); END; `); @@ -187,8 +187,9 @@ export class MemoryService { // This is where all the magic happens - automatic, tracked, safe runMigrations(this.db, this.dbPath); - // Optimize FTS after migrations - DatabaseOptimizer.optimizeFTS(this.db); + // Don't optimize FTS on init - it can corrupt fresh tables + // FTS will be optimized naturally during normal operations + // DatabaseOptimizer.optimizeFTS(this.db); // Prepare statements for better performance this.prepareStatements(); @@ -485,6 +486,74 @@ export class MemoryService { return result.changes; } + /** + * Update a memory's content and/or tags by hash + */ + update(hash: string, content?: string, tags?: string[]): boolean { + if (!this.db) { + throw new Error('Database not initialized'); + } + + // Validate that at least one field is being updated + if (content === undefined && tags === undefined) { + throw new Error('Must provide either content or tags to update'); + } + + // Validate content size if provided + if (content !== undefined && content.length > this.maxContentSize) { + throw new Error(`Content exceeds maximum size of ${this.maxContentSize} characters`); + } + + // Check if memory exists + const existing = this.stmts.getMemoryByHash.get(hash) as any; + if (!existing) { + return false; + } + + try { + // Prepare statements outside transaction + const updateContentStmt = content !== undefined ? + this.db!.prepare(`UPDATE memories SET content = ? WHERE hash = ?`) : null; + + const deleteTagsStmt = tags !== undefined ? + this.db!.prepare(`DELETE FROM tags WHERE memory_id = ?`) : null; + + const updateMemory = this.db.transaction(() => { + // Update content if provided + if (content !== undefined && updateContentStmt) { + updateContentStmt.run(content, hash); + } + + // Update tags if provided + if (tags !== undefined && deleteTagsStmt) { + // Delete existing tags for this memory + deleteTagsStmt.run(existing.id); + + // Insert new tags + for (const tag of tags) { + const normalizedTag = tag.trim().toLowerCase(); + if (normalizedTag) { + this.stmts.insertTag.run(existing.id, normalizedTag); + } + } + } + + return true; + }); + + const updated = updateMemory(); + debugLogHash('MemoryService: Updated memory with hash:', hash); + + // Backup if needed (lazy, throttled) + this.backup?.backupIfNeeded(); + + return updated; + } catch (error: any) { + debugLog('MemoryService: Error updating memory:', error); + throw error; + } + } + /** * Bulk link memories in a single transaction for performance * Returns the number of relationships successfully created diff --git a/src/services/migrations.ts b/src/services/migrations.ts index 3c0c426..344ad55 100644 --- a/src/services/migrations.ts +++ b/src/services/migrations.ts @@ -109,8 +109,8 @@ export const migrations: Migration[] = [ db.exec(` CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN - UPDATE memories_fts SET content = new.content - WHERE rowid = new.id; + DELETE FROM memories_fts WHERE rowid = old.id; + INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); END; `); @@ -135,6 +135,26 @@ export const migrations: Migration[] = [ debugLog('Migration 3: FTS table updated successfully'); } + }, + { + version: 4, + description: 'Fix FTS update trigger for content updates', + up: (db: Database.Database) => { + debugLog('Migration 4: Fixing FTS update trigger'); + + // Drop old trigger that uses UPDATE (incompatible with FTS5) + db.exec(`DROP TRIGGER IF EXISTS memories_au`); + + // Create new trigger that uses DELETE + INSERT (compatible with FTS5) + db.exec(` + CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN + DELETE FROM memories_fts WHERE rowid = old.id; + INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); + END; + `); + + debugLog('Migration 4: FTS update trigger fixed'); + } } // Future migrations go here - just add to the array! ]; diff --git a/src/tests/memory-server-tests.ts b/src/tests/memory-server-tests.ts index 32c044d..f845dbb 100644 --- a/src/tests/memory-server-tests.ts +++ b/src/tests/memory-server-tests.ts @@ -375,6 +375,185 @@ async function testSearchWithRelationships(): Promise { } } +/** + * Test updating memory content + */ +async function testUpdateMemoryContent(): Promise { + // Store a memory first + const storeResult = await executeCommand(['store-memory', '--content', 'Original content', '--tags', 'update-test']); + const storeOutput = parseJsonOutput(storeResult.stdout); + + if (!storeOutput?.success || !storeOutput.hash) { + throw new Error('Failed to store memory for update test'); + } + + const hash = storeOutput.hash; + console.log(` Stored memory with hash: ${formatHash(hash)}`); + + // Update the content + const updateResult = await executeCommand(['update-memory', '--hash', hash, '--content', 'Updated content']); + + if (updateResult.exitCode !== 0) { + throw new Error(`Update memory failed: ${updateResult.stderr}`); + } + + const updateOutput = parseJsonOutput(updateResult.stdout); + if (!updateOutput?.success) { + throw new Error('Update memory did not return expected success response'); + } + + // Verify the update by searching + const searchResult = await executeCommand(['search-memory', '--query', 'Updated content']); + const searchOutput = parseJsonOutput(searchResult.stdout); + + if (!searchOutput?.memories || searchOutput.memories.length === 0) { + throw new Error('Updated memory not found in search'); + } + + const updatedMemory = searchOutput.memories.find((m: any) => m.hash === hash); + if (!updatedMemory) { + throw new Error('Updated memory not found with correct hash'); + } + + if (updatedMemory.content !== 'Updated content') { + throw new Error(`Content not updated correctly: ${updatedMemory.content}`); + } + + console.log('✓ Memory content updated successfully'); +} + +/** + * Test updating memory tags + */ +async function testUpdateMemoryTags(): Promise { + // Store a memory first + const storeResult = await executeCommand(['store-memory', '--content', 'Content for tag update', '--tags', 'old,tag']); + const storeOutput = parseJsonOutput(storeResult.stdout); + + if (!storeOutput?.success || !storeOutput.hash) { + throw new Error('Failed to store memory for tag update test'); + } + + const hash = storeOutput.hash; + console.log(` Stored memory with hash: ${formatHash(hash)}`); + + // Update the tags + const updateResult = await executeCommand(['update-memory', '--hash', hash, '--tags', 'new,updated,tag']); + + if (updateResult.exitCode !== 0) { + throw new Error(`Update memory tags failed: ${updateResult.stderr}`); + } + + const updateOutput = parseJsonOutput(updateResult.stdout); + if (!updateOutput?.success) { + throw new Error('Update memory tags did not return expected success response'); + } + + // Verify the update by searching with new tags + const searchResult = await executeCommand(['search-memory', '--tags', 'new']); + const searchOutput = parseJsonOutput(searchResult.stdout); + + if (!searchOutput?.memories || searchOutput.memories.length === 0) { + throw new Error('Updated memory not found in tag search'); + } + + const updatedMemory = searchOutput.memories.find((m: any) => m.hash === hash); + if (!updatedMemory) { + throw new Error('Updated memory not found with correct hash'); + } + + if (!updatedMemory.tags.includes('new') || !updatedMemory.tags.includes('updated')) { + throw new Error(`Tags not updated correctly: ${updatedMemory.tags.join(',')}`); + } + + // Verify old tag is gone + if (updatedMemory.tags.includes('old')) { + throw new Error('Old tag still present after update'); + } + + console.log('✓ Memory tags updated successfully'); +} + +/** + * Test updating both content and tags + */ +async function testUpdateMemoryBoth(): Promise { + // Store a memory first + const storeResult = await executeCommand(['store-memory', '--content', 'Original for both update', '--tags', 'original']); + const storeOutput = parseJsonOutput(storeResult.stdout); + + if (!storeOutput?.success || !storeOutput.hash) { + throw new Error('Failed to store memory for combined update test'); + } + + const hash = storeOutput.hash; + console.log(` Stored memory with hash: ${formatHash(hash)}`); + + // Update both content and tags + const updateResult = await executeCommand([ + 'update-memory', + '--hash', hash, + '--content', 'Updated both content and tags', + '--tags', 'combined,update' + ]); + + if (updateResult.exitCode !== 0) { + throw new Error(`Update memory (both) failed: ${updateResult.stderr}`); + } + + const updateOutput = parseJsonOutput(updateResult.stdout); + if (!updateOutput?.success) { + throw new Error('Update memory (both) did not return expected success response'); + } + + // Verify the update + const searchResult = await executeCommand(['search-memory', '--tags', 'combined']); + const searchOutput = parseJsonOutput(searchResult.stdout); + + if (!searchOutput?.memories || searchOutput.memories.length === 0) { + throw new Error('Updated memory not found in search'); + } + + const updatedMemory = searchOutput.memories.find((m: any) => m.hash === hash); + if (!updatedMemory) { + throw new Error('Updated memory not found with correct hash'); + } + + if (updatedMemory.content !== 'Updated both content and tags') { + throw new Error(`Content not updated correctly: ${updatedMemory.content}`); + } + + if (!updatedMemory.tags.includes('combined') || !updatedMemory.tags.includes('update')) { + throw new Error(`Tags not updated correctly: ${updatedMemory.tags.join(',')}`); + } + + console.log('✓ Memory content and tags updated successfully'); +} + +/** + * Test updating non-existent memory + */ +async function testUpdateNonExistentMemory(): Promise { + const fakeHash = 'nonexistent123456789abcdef'; + + const updateResult = await executeCommand(['update-memory', '--hash', fakeHash, '--content', 'Should fail']); + + if (updateResult.exitCode !== 0) { + throw new Error(`Update should return gracefully even for non-existent hash`); + } + + const updateOutput = parseJsonOutput(updateResult.stdout); + if (updateOutput?.success === true) { + throw new Error('Update should fail for non-existent memory'); + } + + if (!updateOutput?.message || !updateOutput.message.includes('not found')) { + throw new Error('Update should return "not found" message'); + } + + console.log('✓ Non-existent memory update handled correctly'); +} + /** * Main test runner */ @@ -393,6 +572,10 @@ async function runAllTests(): Promise { { name: 'Memory Statistics', fn: testMemoryStats }, { name: 'Integrated Relationships', fn: testIntegratedRelationships }, { name: 'Search with Relationships', fn: testSearchWithRelationships }, + { name: 'Update Memory Content', fn: testUpdateMemoryContent }, + { name: 'Update Memory Tags', fn: testUpdateMemoryTags }, + { name: 'Update Memory Both', fn: testUpdateMemoryBoth }, + { name: 'Update Non-Existent Memory', fn: testUpdateNonExistentMemory }, { name: 'Delete Memory by Tag', fn: testDeleteMemoryByTag }, { name: 'Search with Limit', fn: testSearchWithLimit }, { name: 'Improved Search (OR + BM25)', fn: testImprovedSearch } diff --git a/src/tools/index.ts b/src/tools/index.ts index 200fc93..ed07670 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -6,6 +6,7 @@ import type { Tool, ToolDefinition, ToolContext } from '../types/tools.js'; import { storeMemoryTool } from './store-memory/index.js'; import { searchMemoryTool } from './search-memory/index.js'; import { deleteMemoryTool } from './delete-memory/index.js'; +import { updateMemoryTool } from './update-memory/index.js'; import { memoryStatsTool } from './memory-stats/index.js'; import { exportMemoryTool } from './export-memory/index.js'; import { importMemoryTool } from './import-memory/index.js'; @@ -18,6 +19,7 @@ export class ToolRegistry { this.registerTool(storeMemoryTool); this.registerTool(searchMemoryTool); this.registerTool(deleteMemoryTool); + this.registerTool(updateMemoryTool); this.registerTool(memoryStatsTool); this.registerTool(exportMemoryTool); this.registerTool(importMemoryTool); diff --git a/src/tools/update-memory/cli-parser.ts b/src/tools/update-memory/cli-parser.ts new file mode 100644 index 0000000..23c6c9c --- /dev/null +++ b/src/tools/update-memory/cli-parser.ts @@ -0,0 +1,32 @@ +import { parseCommandLineArgs } from '../../utils/cli-parser.js'; + +export function parseCliArgs(args: string[]) { + // PHASE 1: Convert string array to object (shared utility) + const rawArgs = parseCommandLineArgs(args); + + // PHASE 2: Validate and transform (command-specific logic) + const result: any = {}; + + if (rawArgs.hash) { + result.hash = rawArgs.hash; + } + + if (rawArgs.content) { + result.content = rawArgs.content; + } + + if (rawArgs.tags) { + result.tags = (rawArgs.tags as string).split(',').map((tag: string) => tag.trim()); + } + + // Validation + if (!result.hash) { + throw new Error('--hash is required'); + } + + if (!result.content && !result.tags) { + throw new Error('Must provide either --content or --tags to update'); + } + + return result; +} diff --git a/src/tools/update-memory/executor.ts b/src/tools/update-memory/executor.ts new file mode 100644 index 0000000..7ce0b78 --- /dev/null +++ b/src/tools/update-memory/executor.ts @@ -0,0 +1,58 @@ +import type { ToolContext } from '../../types/tools.js'; +import { debugLog, formatHash } from '../../utils/debug.js'; + +interface UpdateMemoryArgs { + hash: string; + content?: string; + tags?: string[]; +} + +interface UpdateMemoryResult { + success: boolean; + hash: string; + message: string; +} + +export async function execute(args: UpdateMemoryArgs, context: ToolContext): Promise { + try { + // Validate hash + if (!args.hash || args.hash.trim().length === 0) { + throw new Error('Hash cannot be empty'); + } + + // Validate that at least one field is being updated + if (!args.content && !args.tags) { + throw new Error('Must provide either content or tags to update'); + } + + // Log content size for large memories + if (args.content) { + const contentSize = args.content.length; + if (contentSize > 100000) { + debugLog(`Updating with large content: ${contentSize} characters`); + } + } + + const updated = context.memoryService.update(args.hash, args.content, args.tags); + + if (!updated) { + return { + success: false, + hash: args.hash, + message: `Memory with hash ${formatHash(args.hash)} not found` + }; + } + + return { + success: true, + hash: args.hash, + message: `Memory updated successfully with hash: ${formatHash(args.hash)}` + }; + } catch (error) { + return { + success: false, + hash: args.hash || '', + message: `Failed to update memory: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } +} diff --git a/src/tools/update-memory/index.ts b/src/tools/update-memory/index.ts new file mode 100644 index 0000000..2ef4d5a --- /dev/null +++ b/src/tools/update-memory/index.ts @@ -0,0 +1,58 @@ +import type { Tool } from '../../types/tools.js'; +import { execute } from './executor.js'; +import { parseCliArgs } from './cli-parser.js'; + +export const updateMemoryTool: Tool = { + definition: { + name: 'update-memory', + description: 'Update an existing memory\'s content and/or tags. Use this to refine, correct, or enhance stored information instead of creating duplicates.', + inputSchema: { + type: 'object', + properties: { + hash: { + type: 'string', + description: 'The hash of the memory to update (required)' + }, + content: { + type: 'string', + description: 'New content to replace existing content (optional if tags provided)' + }, + tags: { + type: 'array', + items: { type: 'string' }, + description: 'New tags to replace existing tags (optional if content provided)' + } + }, + required: ['hash'] + } + }, + handler: execute, + cliParser: parseCliArgs, + cliMetadata: { + options: [ + { + name: '--hash', + description: 'Hash of the memory to update', + hasValue: true, + example: '--hash abc123...' + }, + { + name: '--content', + description: 'New content for the memory', + hasValue: true, + example: '--content "Updated information"' + }, + { + name: '--tags', + description: 'New comma-separated tags', + hasValue: true, + example: '--tags "tag1,tag2,tag3"' + } + ], + examples: [ + 'update-memory --hash abc123... --content "Corrected information"', + 'update-memory --hash abc123... --tags "new,tags"', + 'update-memory --hash abc123... --content "Updated" --tags "updated,info"' + ] + } +}; From a96d325d4b17498c8550338a29f45b322f245175 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:13:12 +0000 Subject: [PATCH 4/7] Fix FTS external content corruption issue Co-authored-by: chrisribe <1999791+chrisribe@users.noreply.github.com> --- package.json | 2 +- src/services/memory-service.ts | 6 ++-- src/services/migrations.ts | 60 +++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index a6f8cba..66ca896 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-memory-mcp", - "version": "1.0.23", + "version": "1.0.27", "description": "Simple memory MCP server for storing and retrieving memories with tags", "main": "dist/index.js", "type": "module", diff --git a/src/services/memory-service.ts b/src/services/memory-service.ts index 1b39bd3..d2d350e 100644 --- a/src/services/memory-service.ts +++ b/src/services/memory-service.ts @@ -154,10 +154,12 @@ export class MemoryService { CREATE INDEX IF NOT EXISTS idx_relationships_composite ON relationships(from_memory_id, to_memory_id); `); - // Create FTS table for fast text search (content only, tags in separate table) + // Create FTS table for fast text search (stores its own copy of content) + // Note: Not using external content (content='memories') to avoid corruption issues + // FTS5 external content tables can have sync issues with triggers and WAL mode this.db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts - USING fts5(content, content='memories', content_rowid='id') + USING fts5(content) `); // Create trigger to automatically update FTS when memories are inserted diff --git a/src/services/migrations.ts b/src/services/migrations.ts index 344ad55..917387a 100644 --- a/src/services/migrations.ts +++ b/src/services/migrations.ts @@ -94,9 +94,10 @@ export const migrations: Migration[] = [ db.exec(`DROP TABLE IF EXISTS memories_fts`); // Create new FTS table without tags column + // Note: Not using external content (content='memories') to avoid corruption issues db.exec(` CREATE VIRTUAL TABLE memories_fts - USING fts5(content, content='memories', content_rowid='id') + USING fts5(content) `); // Recreate triggers @@ -155,6 +156,63 @@ export const migrations: Migration[] = [ debugLog('Migration 4: FTS update trigger fixed'); } + }, + { + version: 5, + description: 'Convert FTS from external content to standalone to fix corruption', + up: (db: Database.Database) => { + debugLog('Migration 5: Converting FTS to standalone mode'); + + // Drop old triggers + db.exec(`DROP TRIGGER IF EXISTS memories_ai`); + db.exec(`DROP TRIGGER IF EXISTS memories_au`); + db.exec(`DROP TRIGGER IF EXISTS memories_ad`); + + // Drop old FTS table (external content mode) + db.exec(`DROP TABLE IF EXISTS memories_fts`); + + // Create new FTS table without external content + db.exec(` + CREATE VIRTUAL TABLE memories_fts + USING fts5(content) + `); + + // Recreate triggers for standalone FTS + db.exec(` + CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN + INSERT INTO memories_fts (rowid, content) + VALUES (new.id, new.content); + END; + `); + + db.exec(` + CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN + DELETE FROM memories_fts WHERE rowid = old.id; + INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); + END; + `); + + db.exec(` + CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN + DELETE FROM memories_fts WHERE rowid = old.id; + END; + `); + + // Repopulate FTS table with existing memories + const memories = db.prepare(`SELECT id, content FROM memories`).all() as Array<{ id: number; content: string }>; + if (memories.length > 0) { + debugLog(`Repopulating FTS table with ${memories.length} memories`); + const insertFts = db.prepare(`INSERT INTO memories_fts (rowid, content) VALUES (?, ?)`); + const populateFts = db.transaction(() => { + for (const memory of memories) { + insertFts.run(memory.id, memory.content); + } + }); + populateFts(); + } + + debugLog('Migration 5: FTS converted to standalone mode successfully'); + } } // Future migrations go here - just add to the array! ]; From caf1df864053e1d42cef669b3c74d9ad0d4b0e2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:16:05 +0000 Subject: [PATCH 5/7] Add update-memory tool to prevent memory duplication Co-authored-by: chrisribe <1999791+chrisribe@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66ca896..d8238c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-memory-mcp", - "version": "1.0.27", + "version": "1.0.30", "description": "Simple memory MCP server for storing and retrieving memories with tags", "main": "dist/index.js", "type": "module", From 5006de889dc3820aaf2b8076ec2eb24877d647c9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 03:35:31 +0000 Subject: [PATCH 6/7] Revert out-of-scope database schema changes, keep only update() method Co-authored-by: chrisribe <1999791+chrisribe@users.noreply.github.com> --- package-lock.json | 4 +- package.json | 2 +- src/services/database-optimizer.ts | 14 ++--- src/services/memory-service.ts | 15 +++--- src/services/migrations.ts | 84 ++---------------------------- 5 files changed, 15 insertions(+), 104 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00018d3..8dc0e3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "simple-memory-mcp", - "version": "1.0.10", + "version": "1.0.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "simple-memory-mcp", - "version": "1.0.10", + "version": "1.0.31", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.17.3", diff --git a/package.json b/package.json index d8238c9..fe34034 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-memory-mcp", - "version": "1.0.30", + "version": "1.0.33", "description": "Simple memory MCP server for storing and retrieving memories with tags", "main": "dist/index.js", "type": "module", diff --git a/src/services/database-optimizer.ts b/src/services/database-optimizer.ts index 1c96583..e7ea99e 100644 --- a/src/services/database-optimizer.ts +++ b/src/services/database-optimizer.ts @@ -40,18 +40,10 @@ export class DatabaseOptimizer { */ static optimizeFTS(db: Database.Database): void { try { - // Check if FTS table exists and has data before optimizing - const tableExists = db.prepare(` - SELECT COUNT(*) as count FROM sqlite_master - WHERE type='table' AND name='memories_fts' - `).get() as any; - - if (tableExists && tableExists.count > 0) { - db.exec(`INSERT INTO memories_fts(memories_fts) VALUES('optimize')`); - debugLog('FTS index optimized'); - } + db.exec(`INSERT INTO memories_fts(memories_fts) VALUES('optimize')`); + debugLog('FTS index optimized'); } catch (error: any) { - // FTS optimization is optional - don't fail if it doesn't work + // FTS table might not exist yet, that's fine debugLog('FTS optimization skipped:', error.message); } } diff --git a/src/services/memory-service.ts b/src/services/memory-service.ts index d2d350e..dac9036 100644 --- a/src/services/memory-service.ts +++ b/src/services/memory-service.ts @@ -154,12 +154,10 @@ export class MemoryService { CREATE INDEX IF NOT EXISTS idx_relationships_composite ON relationships(from_memory_id, to_memory_id); `); - // Create FTS table for fast text search (stores its own copy of content) - // Note: Not using external content (content='memories') to avoid corruption issues - // FTS5 external content tables can have sync issues with triggers and WAL mode + // Create FTS table for fast text search (content only, tags in separate table) this.db.exec(` CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts - USING fts5(content) + USING fts5(content, content='memories', content_rowid='id') `); // Create trigger to automatically update FTS when memories are inserted @@ -173,8 +171,8 @@ export class MemoryService { // Create trigger to automatically update FTS when memories are updated this.db.exec(` CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN - DELETE FROM memories_fts WHERE rowid = old.id; - INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); + UPDATE memories_fts SET content = new.content + WHERE rowid = new.id; END; `); @@ -189,9 +187,8 @@ export class MemoryService { // This is where all the magic happens - automatic, tracked, safe runMigrations(this.db, this.dbPath); - // Don't optimize FTS on init - it can corrupt fresh tables - // FTS will be optimized naturally during normal operations - // DatabaseOptimizer.optimizeFTS(this.db); + // Optimize FTS after migrations + DatabaseOptimizer.optimizeFTS(this.db); // Prepare statements for better performance this.prepareStatements(); diff --git a/src/services/migrations.ts b/src/services/migrations.ts index 917387a..3c0c426 100644 --- a/src/services/migrations.ts +++ b/src/services/migrations.ts @@ -94,10 +94,9 @@ export const migrations: Migration[] = [ db.exec(`DROP TABLE IF EXISTS memories_fts`); // Create new FTS table without tags column - // Note: Not using external content (content='memories') to avoid corruption issues db.exec(` CREATE VIRTUAL TABLE memories_fts - USING fts5(content) + USING fts5(content, content='memories', content_rowid='id') `); // Recreate triggers @@ -110,8 +109,8 @@ export const migrations: Migration[] = [ db.exec(` CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN - DELETE FROM memories_fts WHERE rowid = old.id; - INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); + UPDATE memories_fts SET content = new.content + WHERE rowid = new.id; END; `); @@ -136,83 +135,6 @@ export const migrations: Migration[] = [ debugLog('Migration 3: FTS table updated successfully'); } - }, - { - version: 4, - description: 'Fix FTS update trigger for content updates', - up: (db: Database.Database) => { - debugLog('Migration 4: Fixing FTS update trigger'); - - // Drop old trigger that uses UPDATE (incompatible with FTS5) - db.exec(`DROP TRIGGER IF EXISTS memories_au`); - - // Create new trigger that uses DELETE + INSERT (compatible with FTS5) - db.exec(` - CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN - DELETE FROM memories_fts WHERE rowid = old.id; - INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); - END; - `); - - debugLog('Migration 4: FTS update trigger fixed'); - } - }, - { - version: 5, - description: 'Convert FTS from external content to standalone to fix corruption', - up: (db: Database.Database) => { - debugLog('Migration 5: Converting FTS to standalone mode'); - - // Drop old triggers - db.exec(`DROP TRIGGER IF EXISTS memories_ai`); - db.exec(`DROP TRIGGER IF EXISTS memories_au`); - db.exec(`DROP TRIGGER IF EXISTS memories_ad`); - - // Drop old FTS table (external content mode) - db.exec(`DROP TABLE IF EXISTS memories_fts`); - - // Create new FTS table without external content - db.exec(` - CREATE VIRTUAL TABLE memories_fts - USING fts5(content) - `); - - // Recreate triggers for standalone FTS - db.exec(` - CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN - INSERT INTO memories_fts (rowid, content) - VALUES (new.id, new.content); - END; - `); - - db.exec(` - CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN - DELETE FROM memories_fts WHERE rowid = old.id; - INSERT INTO memories_fts (rowid, content) VALUES (new.id, new.content); - END; - `); - - db.exec(` - CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN - DELETE FROM memories_fts WHERE rowid = old.id; - END; - `); - - // Repopulate FTS table with existing memories - const memories = db.prepare(`SELECT id, content FROM memories`).all() as Array<{ id: number; content: string }>; - if (memories.length > 0) { - debugLog(`Repopulating FTS table with ${memories.length} memories`); - const insertFts = db.prepare(`INSERT INTO memories_fts (rowid, content) VALUES (?, ?)`); - const populateFts = db.transaction(() => { - for (const memory of memories) { - insertFts.run(memory.id, memory.content); - } - }); - populateFts(); - } - - debugLog('Migration 5: FTS converted to standalone mode successfully'); - } } // Future migrations go here - just add to the array! ]; From e36db1acc2b5ab00908fc908a5279bf7c27ec9f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Oct 2025 03:37:06 +0000 Subject: [PATCH 7/7] Add update-memory tool to prevent memory duplication Co-authored-by: chrisribe <1999791+chrisribe@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index fe34034..98df138 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "simple-memory-mcp", - "version": "1.0.33", + "version": "1.0.34", "description": "Simple memory MCP server for storing and retrieving memories with tags", "main": "dist/index.js", "type": "module",