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
|
## 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:**
|
**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
|
- Start with `search` to get an index of results
|
||||||
- Use `timeline` to see what was happening around specific observations
|
- Use `timeline` to see what was happening around specific observations
|
||||||
- Use `get_observations` to fetch full details for relevant IDs
|
- 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
|
- **~10x token savings** by filtering before fetching details
|
||||||
|
|
||||||
**Available MCP Tools:**
|
**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
|
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
|
2. **`timeline`** - Get chronological context around a specific observation or query
|
||||||
3. **`get_observations`** - Fetch full observation details by IDs (always batch multiple IDs)
|
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:**
|
**Example Usage:**
|
||||||
|
|
||||||
@@ -216,6 +218,9 @@ search(query="authentication bug", type="bugfix", limit=10)
|
|||||||
|
|
||||||
// Step 3: Fetch full details
|
// Step 3: Fetch full details
|
||||||
get_observations(ids=[123, 456])
|
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.
|
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) => {
|
handler: async (args: any) => {
|
||||||
return await callWorkerAPIPost('/api/observations/batch', args);
|
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;
|
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
|
* 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 { SearchRoutes } from './worker/http/routes/SearchRoutes.js';
|
||||||
import { SettingsRoutes } from './worker/http/routes/SettingsRoutes.js';
|
import { SettingsRoutes } from './worker/http/routes/SettingsRoutes.js';
|
||||||
import { LogsRoutes } from './worker/http/routes/LogsRoutes.js';
|
import { LogsRoutes } from './worker/http/routes/LogsRoutes.js';
|
||||||
|
import { MemoryRoutes } from './worker/http/routes/MemoryRoutes.js';
|
||||||
|
|
||||||
// Process management for zombie cleanup (Issue #737)
|
// Process management for zombie cleanup (Issue #737)
|
||||||
import { startOrphanReaper, reapOrphanedProcesses } from './worker/ProcessRegistry.js';
|
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 DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime));
|
||||||
this.server.registerRoutes(new SettingsRoutes(this.settingsManager));
|
this.server.registerRoutes(new SettingsRoutes(this.settingsManager));
|
||||||
this.server.registerRoutes(new LogsRoutes());
|
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
|
// Early handler for /api/context/inject — fail open if not yet initialized
|
||||||
this.server.app.get('/api/context/inject', async (req, res, next) => {
|
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