UNPKG

@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

358 lines (357 loc) 11 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { ConfigManager } from "../../../core/config/config-manager.js"; import { TraceDetector } from "../../../core/trace/trace-detector.js"; import { logger } from "../../../core/monitoring/logger.js"; import { v4 as uuidv4 } from "uuid"; class ToolScoringMiddleware { configManager; traceDetector; metrics = []; profileStats = /* @__PURE__ */ new Map(); db; constructor(configManager, traceDetector, db) { this.configManager = configManager || new ConfigManager(); this.traceDetector = traceDetector || new TraceDetector(); this.db = db; this.initializeDatabase(); this.loadMetrics(); } /** * Initialize database tables for metrics */ initializeDatabase() { if (!this.db) return; this.db.exec(` CREATE TABLE IF NOT EXISTS tool_scoring_metrics ( id TEXT PRIMARY KEY, profile_name TEXT NOT NULL, tool_name TEXT NOT NULL, score REAL NOT NULL, files_affected INTEGER DEFAULT 0, is_permanent BOOLEAN DEFAULT FALSE, reference_count INTEGER DEFAULT 0, timestamp INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); this.db.exec(` CREATE TABLE IF NOT EXISTS profile_usage_stats ( profile_name TEXT PRIMARY KEY, usage_count INTEGER DEFAULT 0, total_score REAL DEFAULT 0, avg_score REAL DEFAULT 0, high_score_tools TEXT, last_used INTEGER, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); this.db.exec(` CREATE INDEX IF NOT EXISTS idx_metrics_profile ON tool_scoring_metrics(profile_name); CREATE INDEX IF NOT EXISTS idx_metrics_tool ON tool_scoring_metrics(tool_name); CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON tool_scoring_metrics(timestamp); `); } /** * Load existing metrics from database */ loadMetrics() { if (!this.db) return; try { const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3; const stmt = this.db.prepare(` SELECT * FROM tool_scoring_metrics WHERE timestamp > ? ORDER BY timestamp DESC `); const rows = stmt.all(cutoff); this.metrics = rows.map((row) => ({ profileName: row.profile_name, toolName: row.tool_name, score: row.score, factors: { filesAffected: row.files_affected, isPermanent: row.is_permanent === 1, referenceCount: row.reference_count }, timestamp: row.timestamp })); const statsStmt = this.db.prepare(` SELECT * FROM profile_usage_stats `); const statsRows = statsStmt.all(); statsRows.forEach((row) => { this.profileStats.set(row.profile_name, { profileName: row.profile_name, usageCount: row.usage_count, totalScore: row.total_score, avgScore: row.avg_score, highScoreTools: row.high_score_tools ? JSON.parse(row.high_score_tools) : [], lastUsed: row.last_used }); }); logger.info("Loaded tool scoring metrics", { metricsCount: this.metrics.length, profilesCount: this.profileStats.size }); } catch (error) { logger.error("Failed to load metrics", error); } } /** * Score a tool call and track it */ async scoreToolCall(toolName, args, result, error) { const factors = this.extractScoringFactors(toolName, args, result); const score = this.configManager.calculateScore(toolName, factors); const config = this.configManager.getConfig(); const profileName = config.profile || "default"; const metric = { profileName, toolName, score, factors, timestamp: Date.now() }; this.metrics.push(metric); this.updateProfileStats(profileName, toolName, score); this.saveMetric(metric); if (score > 0.3) { const toolCall = { id: uuidv4(), tool: toolName, arguments: args, timestamp: Date.now(), result, error, filesAffected: factors.filesAffected > 0 ? this.getFilesFromArgs(args) : void 0 }; this.traceDetector.addToolCall(toolCall); } if (score > 0.8) { logger.warn("High-importance tool call", { tool: toolName, score, profile: profileName, factors }); } return score; } /** * Extract scoring factors from tool arguments and results */ extractScoringFactors(toolName, args, result) { let filesAffected = 0; let isPermanent = false; let referenceCount = 0; if (args.file || args.files || args.path || args.paths) { filesAffected = 1; if (args.files || args.paths) { filesAffected = Array.isArray(args.files || args.paths) ? (args.files || args.paths).length : 1; } } const permanentTools = [ "write", "edit", "create", "delete", "update", "add_decision", "create_task", "linear_update_task" ]; if (permanentTools.some((t) => toolName.includes(t))) { isPermanent = true; } if (result?.referenceCount !== void 0) { referenceCount = result.referenceCount; } else if (result?.references?.length) { referenceCount = result.references.length; } return { filesAffected, isPermanent, referenceCount }; } /** * Get file paths from arguments */ getFilesFromArgs(args) { const files = []; if (args.file) files.push(args.file); if (args.path) files.push(args.path); if (args.files && Array.isArray(args.files)) files.push(...args.files); if (args.paths && Array.isArray(args.paths)) files.push(...args.paths); return files; } /** * Update profile usage statistics */ updateProfileStats(profileName, toolName, score) { let stats = this.profileStats.get(profileName); if (!stats) { stats = { profileName, usageCount: 0, totalScore: 0, avgScore: 0, highScoreTools: [], lastUsed: Date.now() }; this.profileStats.set(profileName, stats); } stats.usageCount++; stats.totalScore += score; stats.avgScore = stats.totalScore / stats.usageCount; stats.lastUsed = Date.now(); if (score > 0.7 && !stats.highScoreTools.includes(toolName)) { stats.highScoreTools.push(toolName); if (stats.highScoreTools.length > 10) { stats.highScoreTools.shift(); } } this.saveProfileStats(stats); } /** * Save metric to database */ saveMetric(metric) { if (!this.db) return; try { const stmt = this.db.prepare(` INSERT INTO tool_scoring_metrics ( id, profile_name, tool_name, score, files_affected, is_permanent, reference_count, timestamp ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( uuidv4(), metric.profileName, metric.toolName, metric.score, metric.factors.filesAffected, metric.factors.isPermanent ? 1 : 0, metric.factors.referenceCount, metric.timestamp ); } catch (error) { logger.error("Failed to save metric", error); } } /** * Save profile stats to database */ saveProfileStats(stats) { if (!this.db) return; try { const stmt = this.db.prepare(` INSERT OR REPLACE INTO profile_usage_stats ( profile_name, usage_count, total_score, avg_score, high_score_tools, last_used ) VALUES (?, ?, ?, ?, ?, ?) `); stmt.run( stats.profileName, stats.usageCount, stats.totalScore, stats.avgScore, JSON.stringify(stats.highScoreTools), stats.lastUsed ); } catch (error) { logger.error("Failed to save profile stats", error); } } /** * Get profile effectiveness report */ getProfileReport(profileName) { if (profileName) { const stats = this.profileStats.get(profileName); if (!stats) { return { error: `Profile '${profileName}' not found or not used yet` }; } return { profile: profileName, usage: stats.usageCount, avgScore: stats.avgScore.toFixed(3), highScoreTools: stats.highScoreTools, lastUsed: new Date(stats.lastUsed).toISOString() }; } const profiles = Array.from(this.profileStats.values()).sort( (a, b) => b.avgScore - a.avgScore ); return { profileCount: profiles.length, mostEffective: profiles[0]?.profileName || "none", profiles: profiles.map((p) => ({ name: p.profileName, usage: p.usageCount, avgScore: p.avgScore.toFixed(3) })) }; } /** * Get tool scoring trends */ getToolTrends(toolName, hours = 24) { const cutoff = Date.now() - hours * 60 * 60 * 1e3; const relevantMetrics = this.metrics.filter( (m) => m.toolName === toolName && m.timestamp > cutoff ); if (relevantMetrics.length === 0) { return { tool: toolName, message: "No recent data" }; } const scores = relevantMetrics.map((m) => m.score); const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length; const maxScore = Math.max(...scores); const minScore = Math.min(...scores); return { tool: toolName, period: `${hours}h`, count: relevantMetrics.length, avgScore: avgScore.toFixed(3), maxScore: maxScore.toFixed(3), minScore: minScore.toFixed(3), trend: this.calculateTrend(relevantMetrics) }; } /** * Calculate score trend */ calculateTrend(metrics) { if (metrics.length < 2) return "stable"; const firstHalf = metrics.slice(0, Math.floor(metrics.length / 2)); const secondHalf = metrics.slice(Math.floor(metrics.length / 2)); const firstAvg = firstHalf.reduce((a, m) => a + m.score, 0) / firstHalf.length; const secondAvg = secondHalf.reduce((a, m) => a + m.score, 0) / secondHalf.length; const diff = secondAvg - firstAvg; if (diff > 0.1) return "increasing"; if (diff < -0.1) return "decreasing"; return "stable"; } /** * Clean old metrics */ cleanOldMetrics(daysToKeep = 30) { const cutoff = Date.now() - daysToKeep * 24 * 60 * 60 * 1e3; const oldCount = this.metrics.length; this.metrics = this.metrics.filter((m) => m.timestamp > cutoff); if (this.db) { try { const stmt = this.db.prepare(` DELETE FROM tool_scoring_metrics WHERE timestamp < ? `); stmt.run(cutoff); } catch (error) { logger.error("Failed to clean old metrics", error); } } return oldCount - this.metrics.length; } } export { ToolScoringMiddleware };