@stackmemoryai/stackmemory
Version:
Lossless, project-scoped memory for AI coding tools. Durable context across sessions with 56 MCP tools, FTS5 search, conductor orchestrator, loop/watch monitoring, snapshot capture, pre-flight overlap checks, Claude/Codex/OpenCode wrappers, Linear sync, a
345 lines (344 loc) • 9.99 kB
JavaScript
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
};