memory_journal
Search your conversation journal to recall past work or find discussions by topic. Use semantic queries or browse recent entries.
Instructions
Search or browse the conversation journal. Use to answer 'what did I work on?' or find past conversations by topic.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Search query (semantic). Omit to list recent entries. | |
| days | No | How many days back to look (default: 7) |
Implementation Reference
- The handler function `handleMemoryJournal` that executes the memory_journal tool logic. If a query is provided, it performs semantic search via `searchMemories` and filters for journal entries. If no query, it queries the `journal_entries` table directly for recent entries within the specified day range (default 7).
export async function handleMemoryJournal( query?: string, days?: number, ): Promise<string> { const db = getDb(); if (query) { const results = await searchMemories(query, { topK: 5, minScore: 0.25, }); const journalResults = results.filter((r) => r.source === "journal"); if (journalResults.length === 0) { return `No journal entries found matching "${query}".`; } const lines = journalResults.map((r) => { const date = new Date(r.created_at).toISOString().slice(0, 10); const score = (r.score * 100).toFixed(0); const project = r.project ? ` [${r.project}]` : ""; return `**${score}% match**${project} — ${date}\n${r.content}`; }); return `Found ${journalResults.length} journal entry(s):\n\n${lines.join("\n\n")}`; } const cutoff = Date.now() - (days ?? 7) * 24 * 60 * 60 * 1000; const entries = db .prepare( `SELECT id, content, project, created_at FROM journal_entries WHERE created_at >= ? ORDER BY created_at DESC LIMIT 20`, ) .all(cutoff) as Array<{ id: string; content: string; project: string | null; created_at: number; }>; if (entries.length === 0) { return `No journal entries in the last ${days ?? 7} day(s).`; } const lines = entries.map((e) => { const date = new Date(e.created_at).toISOString().slice(0, 16).replace("T", " "); const project = e.project ? ` [${e.project}]` : ""; return `**${date}**${project}\n${e.content.slice(0, 200)}`; }); return `${entries.length} journal entry(s) from the last ${days ?? 7} day(s):\n\n${lines.join("\n\n")}`; } - packages/server/src/index.ts:271-289 (registration)Registration of the 'memory_journal' tool on the MCP server with Zod schema for optional 'query' and 'days' parameters, wired to call `handleMemoryJournal`.
server.tool( "memory_journal", "Search or browse the conversation journal. Use to answer 'what did I work on?' or find past conversations by topic.", { query: z.string().optional().describe("Search query (semantic). Omit to list recent entries."), days: z.number().optional().describe("How many days back to look (default: 7)"), }, async ({ query, days }) => { try { const result = await handleMemoryJournal(query, days); return { content: [{ type: "text", text: result }] }; } catch (err) { return { content: [{ type: "text", text: `Error reading journal: ${err}` }], isError: true, }; } }, ); - packages/server/src/index.ts:274-277 (schema)Input schema for the memory_journal tool: optional 'query' (string) for semantic search, optional 'days' (number) for recency filter (default 7).
{ query: z.string().optional().describe("Search query (semantic). Omit to list recent entries."), days: z.number().optional().describe("How many days back to look (default: 7)"), }, - The `searchMemories` helper used by the handler for semantic search across both memories and journal entries. The handler filters results to only those with `source === 'journal'`.
export async function searchMemories( query: string, options: { topK?: number; category?: string; project?: string; includeArchived?: boolean; minScore?: number; } = {}, ): Promise<SearchResult[]> { const { topK = 5, category, project, includeArchived = false, minScore = 0.3, } = options; // Fall back to keyword search when Ollama is unavailable if (isUsingFallback()) { return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 }); } const db = getDb(); const queryEmbedding = await embed(query); if (!queryEmbedding) { return keywordSearch(query, { topK, category, project, includeArchived, minScore: 0.2 }); } let memorySql = `SELECT id, content, category, project, created_at, embedding FROM memories WHERE embedding IS NOT NULL`; const params: unknown[] = []; if (!includeArchived) { memorySql += ` AND archived = 0`; } if (category) { memorySql += ` AND category = ?`; params.push(category); } if (project) { memorySql += ` AND project = ?`; params.push(project); } const memories = db.prepare(memorySql).all(...params) as Array<{ id: string; content: string; category: string; project: string | null; created_at: number; embedding: Buffer; }>; let journalSql = `SELECT id, content, project, created_at, embedding FROM journal_entries WHERE embedding IS NOT NULL`; const journalParams: unknown[] = []; if (project) { journalSql += ` AND project = ?`; journalParams.push(project); } const journals = db.prepare(journalSql).all(...journalParams) as Array<{ id: string; content: string; project: string | null; created_at: number; embedding: Buffer; }>; const results: SearchResult[] = []; for (const mem of memories) { const memEmbedding = bufferToEmbedding(mem.embedding); const score = cosineSimilarity(queryEmbedding, memEmbedding); if (score >= minScore) { results.push({ id: mem.id, content: mem.content, category: mem.category, project: mem.project, score, created_at: mem.created_at, source: "memory", }); } } for (const entry of journals) { const entryEmbedding = bufferToEmbedding(entry.embedding); const score = cosineSimilarity(queryEmbedding, entryEmbedding); if (score >= minScore) { results.push({ id: entry.id, content: entry.content, category: "journal", project: entry.project, score, created_at: entry.created_at, source: "journal", }); } } results.sort((a, b) => b.score - a.score); const updateAccess = db.prepare( `UPDATE memories SET accessed_at = ?, access_count = access_count + 1 WHERE id = ?`, ); const now = Date.now(); for (const r of results.slice(0, topK)) { if (r.source === "memory") { updateAccess.run(now, r.id); } } db.prepare(`INSERT INTO search_log (query, results_count, created_at) VALUES (?, ?, ?)`).run( query, Math.min(results.length, topK), now, ); return results.slice(0, topK); } - The `journal_entries` table schema in SQLite, which the handler queries directly when no search query is provided.
CREATE TABLE IF NOT EXISTS journal_entries ( id TEXT PRIMARY KEY, session_id TEXT, project TEXT, content TEXT NOT NULL, created_at INTEGER NOT NULL, embedding BLOB );