UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

346 lines (345 loc) 10 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { TraceType } from "./types.js"; import { logger } from "../monitoring/logger.js"; class TraceStore { db; constructor(db) { this.db = db; this.initializeSchema(); } /** * Initialize database schema for traces */ initializeSchema() { const hasFramesTable = this.db.prepare( ` SELECT name FROM sqlite_master WHERE type='table' AND name='frames' ` ).get(); if (hasFramesTable) { this.db.exec(` CREATE TABLE IF NOT EXISTS traces ( id TEXT PRIMARY KEY, type TEXT NOT NULL, score REAL NOT NULL, summary TEXT NOT NULL, start_time INTEGER NOT NULL, end_time INTEGER NOT NULL, frame_id TEXT, user_id TEXT, files_modified TEXT, errors_encountered TEXT, decisions_recorded TEXT, causal_chain INTEGER, compressed_data TEXT, created_at INTEGER DEFAULT (unixepoch()), FOREIGN KEY (frame_id) REFERENCES frames(frame_id) ON DELETE SET NULL ) `); } else { this.db.exec(` CREATE TABLE IF NOT EXISTS traces ( id TEXT PRIMARY KEY, type TEXT NOT NULL, score REAL NOT NULL, summary TEXT NOT NULL, start_time INTEGER NOT NULL, end_time INTEGER NOT NULL, frame_id TEXT, user_id TEXT, files_modified TEXT, errors_encountered TEXT, decisions_recorded TEXT, causal_chain INTEGER, compressed_data TEXT, created_at INTEGER DEFAULT (unixepoch()) ) `); } this.db.exec(` CREATE TABLE IF NOT EXISTS tool_calls ( id TEXT PRIMARY KEY, trace_id TEXT NOT NULL, tool TEXT NOT NULL, arguments TEXT, timestamp INTEGER NOT NULL, result TEXT, error TEXT, files_affected TEXT, duration INTEGER, sequence_number INTEGER NOT NULL, FOREIGN KEY (trace_id) REFERENCES traces(id) ON DELETE CASCADE ) `); this.db.exec(` CREATE INDEX IF NOT EXISTS idx_traces_type ON traces(type); CREATE INDEX IF NOT EXISTS idx_traces_frame_id ON traces(frame_id); CREATE INDEX IF NOT EXISTS idx_traces_start_time ON traces(start_time); CREATE INDEX IF NOT EXISTS idx_traces_score ON traces(score); CREATE INDEX IF NOT EXISTS idx_tool_calls_trace_id ON tool_calls(trace_id); CREATE INDEX IF NOT EXISTS idx_tool_calls_timestamp ON tool_calls(timestamp); `); } /** * Save a trace to the database */ saveTrace(trace) { const traceStmt = this.db.prepare(` INSERT OR REPLACE INTO traces ( id, type, score, summary, start_time, end_time, frame_id, user_id, files_modified, errors_encountered, decisions_recorded, causal_chain, compressed_data ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const toolCallStmt = this.db.prepare(` INSERT OR REPLACE INTO tool_calls ( id, trace_id, tool, arguments, timestamp, result, error, files_affected, duration, sequence_number ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); try { this.db.transaction(() => { traceStmt.run( trace.id, trace.type, trace.score, trace.summary, trace.metadata.startTime, trace.metadata.endTime, trace.metadata.frameId || null, trace.metadata.userId || null, JSON.stringify(trace.metadata.filesModified), JSON.stringify(trace.metadata.errorsEncountered), JSON.stringify(trace.metadata.decisionsRecorded), trace.metadata.causalChain ? 1 : 0, trace.compressed ? JSON.stringify(trace.compressed) : null ); trace.tools.forEach((tool, index) => { toolCallStmt.run( tool.id, trace.id, tool.tool, tool.arguments ? JSON.stringify(tool.arguments) : null, tool.timestamp, tool.result ? JSON.stringify(tool.result) : null, tool.error || null, tool.filesAffected ? JSON.stringify(tool.filesAffected) : null, tool.duration || null, index ); }); })(); logger.debug( `Saved trace ${trace.id} with ${trace.tools.length} tool calls` ); } catch (error) { logger.error(`Failed to save trace ${trace.id}:`, error); throw error; } } /** * Load a trace by ID */ getTrace(id) { const traceRow = this.db.prepare( ` SELECT * FROM traces WHERE id = ? ` ).get(id); if (!traceRow) { return null; } const toolRows = this.db.prepare( ` SELECT * FROM tool_calls WHERE trace_id = ? ORDER BY sequence_number ` ).all(id); return this.rowsToTrace(traceRow, toolRows); } /** * Load all traces */ getAllTraces() { const traceRows = this.db.prepare( ` SELECT * FROM traces ORDER BY start_time DESC ` ).all(); return traceRows.map((traceRow) => { const toolRows = this.db.prepare( ` SELECT * FROM tool_calls WHERE trace_id = ? ORDER BY sequence_number ` ).all(traceRow.id); return this.rowsToTrace(traceRow, toolRows); }); } /** * Load traces by type */ getTracesByType(type) { const traceRows = this.db.prepare( ` SELECT * FROM traces WHERE type = ? ORDER BY start_time DESC ` ).all(type); return traceRows.map((traceRow) => { const toolRows = this.db.prepare( ` SELECT * FROM tool_calls WHERE trace_id = ? ORDER BY sequence_number ` ).all(traceRow.id); return this.rowsToTrace(traceRow, toolRows); }); } /** * Load traces by frame */ getTracesByFrame(frameId) { const traceRows = this.db.prepare( ` SELECT * FROM traces WHERE frame_id = ? ORDER BY start_time DESC ` ).all(frameId); return traceRows.map((traceRow) => { const toolRows = this.db.prepare( ` SELECT * FROM tool_calls WHERE trace_id = ? ORDER BY sequence_number ` ).all(traceRow.id); return this.rowsToTrace(traceRow, toolRows); }); } /** * Load high-importance traces */ getHighImportanceTraces(minScore = 0.7) { const traceRows = this.db.prepare( ` SELECT * FROM traces WHERE score >= ? ORDER BY score DESC, start_time DESC ` ).all(minScore); return traceRows.map((traceRow) => { const toolRows = this.db.prepare( ` SELECT * FROM tool_calls WHERE trace_id = ? ORDER BY sequence_number ` ).all(traceRow.id); return this.rowsToTrace(traceRow, toolRows); }); } /** * Load error traces */ getErrorTraces() { const traceRows = this.db.prepare( ` SELECT * FROM traces WHERE type = ? OR errors_encountered != '[]' ORDER BY start_time DESC ` ).all(TraceType.ERROR_RECOVERY); return traceRows.map((traceRow) => { const toolRows = this.db.prepare( ` SELECT * FROM tool_calls WHERE trace_id = ? ORDER BY sequence_number ` ).all(traceRow.id); return this.rowsToTrace(traceRow, toolRows); }); } /** * Get trace statistics */ getStatistics() { const stats = this.db.prepare( ` SELECT COUNT(*) as total, AVG(score) as avg_score, AVG(( SELECT COUNT(*) FROM tool_calls WHERE trace_id = traces.id )) as avg_length, SUM(CASE WHEN type = ? OR errors_encountered != '[]' THEN 1 ELSE 0 END) * 100.0 / COUNT(*) as error_rate FROM traces ` ).get(TraceType.ERROR_RECOVERY); const typeStats = this.db.prepare( ` SELECT type, COUNT(*) as count FROM traces GROUP BY type ` ).all(); const tracesByType = {}; typeStats.forEach((row) => { tracesByType[row.type] = row.count; }); return { totalTraces: stats.total || 0, tracesByType, averageScore: stats.avg_score || 0, averageLength: stats.avg_length || 0, errorRate: stats.error_rate || 0 }; } /** * Delete old traces */ deleteOldTraces(olderThanMs) { const cutoff = Date.now() - olderThanMs; const result = this.db.prepare( ` DELETE FROM traces WHERE start_time < ? ` ).run(cutoff); return result.changes; } /** * Convert database rows to Trace object */ rowsToTrace(traceRow, toolRows) { const tools = toolRows.map((row) => ({ id: row.id, tool: row.tool, arguments: row.arguments ? JSON.parse(row.arguments) : void 0, timestamp: row.timestamp, result: row.result ? JSON.parse(row.result) : void 0, error: row.error || void 0, filesAffected: row.files_affected ? JSON.parse(row.files_affected) : void 0, duration: row.duration || void 0 })); const metadata = { startTime: traceRow.start_time, endTime: traceRow.end_time, frameId: traceRow.frame_id || void 0, userId: traceRow.user_id || void 0, filesModified: JSON.parse(traceRow.files_modified || "[]"), errorsEncountered: JSON.parse(traceRow.errors_encountered || "[]"), decisionsRecorded: JSON.parse(traceRow.decisions_recorded || "[]"), causalChain: traceRow.causal_chain === 1 }; const trace = { id: traceRow.id, type: traceRow.type, tools, score: traceRow.score, summary: traceRow.summary, metadata }; if (traceRow.compressed_data) { trace.compressed = JSON.parse(traceRow.compressed_data); } return trace; } } export { TraceStore }; //# sourceMappingURL=trace-store.js.map