MAESTRO: Merge PR #662 - Add save_memory MCP tool for manual memory storage

Adds save_memory MCP tool allowing users to manually save observations
for semantic search. Source changes cherry-picked from PR #662 by
@darconada (build artifact conflicts resolved by direct application).

Closes #645.

Co-Authored-By: darconadalabarga <darconada@arsys.es>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-02-06 04:13:44 -05:00
parent fbcbdfca62
commit 98920bd860
5 changed files with 155 additions and 2 deletions
+7 -2
View File
@@ -184,7 +184,7 @@ See [Architecture Overview](https://docs.claude-mem.ai/architecture/overview) fo
## MCP Search Tools
Claude-Mem provides intelligent memory search through **4 MCP tools** following a token-efficient **3-layer workflow pattern**:
Claude-Mem provides intelligent memory search through **5 MCP tools** following a token-efficient **3-layer workflow pattern**:
**The 3-Layer Workflow:**
@@ -197,6 +197,7 @@ Claude-Mem provides intelligent memory search through **4 MCP tools** following
- Start with `search` to get an index of results
- Use `timeline` to see what was happening around specific observations
- Use `get_observations` to fetch full details for relevant IDs
- Use `save_memory` to manually store important information
- **~10x token savings** by filtering before fetching details
**Available MCP Tools:**
@@ -204,7 +205,8 @@ Claude-Mem provides intelligent memory search through **4 MCP tools** following
1. **`search`** - Search memory index with full-text queries, filters by type/date/project
2. **`timeline`** - Get chronological context around a specific observation or query
3. **`get_observations`** - Fetch full observation details by IDs (always batch multiple IDs)
4. **`__IMPORTANT`** - Workflow documentation (always visible to Claude)
4. **`save_memory`** - Manually save a memory/observation for semantic search
5. **`__IMPORTANT`** - Workflow documentation (always visible to Claude)
**Example Usage:**
@@ -216,6 +218,9 @@ search(query="authentication bug", type="bugfix", limit=10)
// Step 3: Fetch full details
get_observations(ids=[123, 456])
// Save important information manually
save_memory(text="API requires auth header X-API-Key", title="API Auth")
```
See [Search Tools Guide](https://docs.claude-mem.ai/usage/search-tools) for detailed examples.
+25
View File
@@ -233,6 +233,31 @@ NEVER fetch full details without filtering first. 10x token savings.`,
handler: async (args: any) => {
return await callWorkerAPIPost('/api/observations/batch', args);
}
},
{
name: 'save_memory',
description: 'Save a manual memory/observation for semantic search. Use this to remember important information.',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'Content to remember (required)'
},
title: {
type: 'string',
description: 'Short title (auto-generated from text if omitted)'
},
project: {
type: 'string',
description: 'Project name (uses "claude-mem" if omitted)'
}
},
required: ['text']
},
handler: async (args: any) => {
return await callWorkerAPIPost('/api/memory/save', args);
}
}
];
+28
View File
@@ -2124,6 +2124,34 @@ export class SessionStore {
return stmt.get(id) || null;
}
/**
* Get or create a manual session for storing user-created observations
* Manual sessions use a predictable ID format: "manual-{project}"
*/
getOrCreateManualSession(project: string): string {
const memorySessionId = `manual-${project}`;
const contentSessionId = `manual-content-${project}`;
const existing = this.db.prepare(
'SELECT memory_session_id FROM sdk_sessions WHERE memory_session_id = ?'
).get(memorySessionId) as { memory_session_id: string } | undefined;
if (existing) {
return memorySessionId;
}
// Create new manual session
const now = new Date();
this.db.prepare(`
INSERT INTO sdk_sessions (memory_session_id, content_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(memorySessionId, contentSessionId, project, now.toISOString(), now.getTime());
logger.info('SESSION', 'Created manual session', { memorySessionId, project });
return memorySessionId;
}
/**
* Close the database connection
*/
+2
View File
@@ -108,6 +108,7 @@ import { DataRoutes } from './worker/http/routes/DataRoutes.js';
import { SearchRoutes } from './worker/http/routes/SearchRoutes.js';
import { SettingsRoutes } from './worker/http/routes/SettingsRoutes.js';
import { LogsRoutes } from './worker/http/routes/LogsRoutes.js';
import { MemoryRoutes } from './worker/http/routes/MemoryRoutes.js';
// Process management for zombie cleanup (Issue #737)
import { startOrphanReaper, reapOrphanedProcesses } from './worker/ProcessRegistry.js';
@@ -240,6 +241,7 @@ export class WorkerService {
this.server.registerRoutes(new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime));
this.server.registerRoutes(new SettingsRoutes(this.settingsManager));
this.server.registerRoutes(new LogsRoutes());
this.server.registerRoutes(new MemoryRoutes(this.dbManager, 'claude-mem'));
// Early handler for /api/context/inject — fail open if not yet initialized
this.server.app.get('/api/context/inject', async (req, res, next) => {
@@ -0,0 +1,93 @@
/**
* Memory Routes
*
* Handles manual memory/observation saving.
* POST /api/memory/save - Save a manual memory observation
*/
import express, { Request, Response } from 'express';
import { BaseRouteHandler } from '../BaseRouteHandler.js';
import { logger } from '../../../../utils/logger.js';
import type { DatabaseManager } from '../../DatabaseManager.js';
export class MemoryRoutes extends BaseRouteHandler {
constructor(
private dbManager: DatabaseManager,
private defaultProject: string
) {
super();
}
setupRoutes(app: express.Application): void {
app.post('/api/memory/save', this.handleSaveMemory.bind(this));
}
/**
* POST /api/memory/save - Save a manual memory/observation
* Body: { text: string, title?: string, project?: string }
*/
private handleSaveMemory = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
const { text, title, project } = req.body;
const targetProject = project || this.defaultProject;
if (!text || typeof text !== 'string' || text.trim().length === 0) {
this.badRequest(res, 'text is required and must be non-empty');
return;
}
const sessionStore = this.dbManager.getSessionStore();
const chromaSync = this.dbManager.getChromaSync();
// 1. Get or create manual session for project
const memorySessionId = sessionStore.getOrCreateManualSession(targetProject);
// 2. Build observation
const observation = {
type: 'discovery', // Use existing valid type
title: title || text.substring(0, 60).trim() + (text.length > 60 ? '...' : ''),
subtitle: 'Manual memory',
facts: [] as string[],
narrative: text,
concepts: [] as string[],
files_read: [] as string[],
files_modified: [] as string[]
};
// 3. Store to SQLite
const result = sessionStore.storeObservation(
memorySessionId,
targetProject,
observation,
0, // promptNumber
0 // discoveryTokens
);
logger.info('HTTP', 'Manual observation saved', {
id: result.id,
project: targetProject,
title: observation.title
});
// 4. Sync to ChromaDB (async, fire-and-forget)
chromaSync.syncObservation(
result.id,
memorySessionId,
targetProject,
observation,
0,
result.createdAtEpoch,
0
).catch(err => {
logger.error('CHROMA', 'ChromaDB sync failed', { id: result.id }, err as Error);
});
// 5. Return success
res.json({
success: true,
id: result.id,
title: observation.title,
project: targetProject,
message: `Memory saved as observation #${result.id}`
});
});
}