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:
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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}`
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user