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.

431 lines (430 loc) 13.2 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { logger } from "../monitoring/logger.js"; import { DatabaseError, ErrorCode } from "../errors/index.js"; import { EventEmitter } from "events"; class QueryRouter extends EventEmitter { tiers = /* @__PURE__ */ new Map(); metrics; decisionCache = /* @__PURE__ */ new Map(); cacheExpiration = 6e4; // 1 minute maxCacheSize = 1e3; constructor() { super(); this.metrics = { totalQueries: 0, queriesByTier: /* @__PURE__ */ new Map(), queriesByType: /* @__PURE__ */ new Map(), averageLatency: 0, latencyByTier: /* @__PURE__ */ new Map(), errorsByTier: /* @__PURE__ */ new Map(), cacheHitRate: 0, routingDecisions: 0 }; } /** * Register a storage tier with the router */ registerTier(tier) { this.tiers.set(tier.name, tier); logger.info( `Registered storage tier: ${tier.name} (priority: ${tier.priority})` ); this.emit("tierRegistered", tier); } /** * Remove a storage tier from the router */ unregisterTier(tierName) { const tier = this.tiers.get(tierName); if (tier) { this.tiers.delete(tierName); logger.info(`Unregistered storage tier: ${tierName}`); this.emit("tierUnregistered", tier); } } /** * Route a query to the most appropriate storage tier */ async route(operation, context, executor) { const startTime = Date.now(); this.metrics.totalQueries++; this.metrics.queriesByType.set( context.queryType, (this.metrics.queriesByType.get(context.queryType) || 0) + 1 ); try { const decision = await this.makeRoutingDecision(operation, context); try { const result = await this.executeOnTier(decision.primaryTier, executor); this.updateMetrics(decision.primaryTier.name, startTime, true); return result; } catch (error) { logger.warn( `Query failed on primary tier ${decision.primaryTier.name}:`, error ); this.updateMetrics(decision.primaryTier.name, startTime, false); for (const fallbackTier of decision.fallbackTiers) { try { logger.info(`Attempting fallback to tier: ${fallbackTier.name}`); const result = await this.executeOnTier(fallbackTier, executor); this.updateMetrics(fallbackTier.name, startTime, true); return result; } catch (fallbackError) { logger.warn( `Query failed on fallback tier ${fallbackTier.name}:`, fallbackError ); this.updateMetrics(fallbackTier.name, startTime, false); } } throw error; } } catch (error) { logger.error("Query routing failed:", error); this.emit("routingError", { operation, context, error }); throw error; } } /** * Make routing decision based on query context */ async makeRoutingDecision(operation, context) { const cacheKey = this.generateCacheKey(operation, context); const cached = this.decisionCache.get(cacheKey); if (cached && Date.now() - cached.estimatedLatency < this.cacheExpiration) { this.metrics.cacheHitRate = (this.metrics.cacheHitRate * this.metrics.routingDecisions + 1) / (this.metrics.routingDecisions + 1); return cached; } this.metrics.routingDecisions++; const evaluations = []; for (const tier of this.tiers.values()) { const score = await this.evaluateTier(tier, operation, context); const rationale = this.generateRationale(tier, operation, context, score); evaluations.push({ tier, score, rationale }); } evaluations.sort((a, b) => b.score - a.score); if (evaluations.length === 0) { throw new DatabaseError( "No storage tiers available for routing", ErrorCode.DB_CONNECTION_FAILED, { query: context.query, tiersConfigured: this.tiers.length } ); } const primaryEval = evaluations[0]; const fallbackTiers = evaluations.slice(1).map((evaluation) => evaluation.tier); const decision = { primaryTier: primaryEval.tier, fallbackTiers, rationale: primaryEval.rationale, confidence: primaryEval.score, estimatedLatency: this.estimateLatency( primaryEval.tier, operation, context ), cacheRecommendation: this.recommendCacheStrategy( primaryEval.tier, context ) }; this.cacheDecision(cacheKey, decision); logger.debug( `Routing decision: ${decision.primaryTier.name} (confidence: ${decision.confidence.toFixed(2)})` ); this.emit("routingDecision", { operation, context, decision }); return decision; } /** * Evaluate how well a tier fits the query requirements */ async evaluateTier(tier, operation, context) { let score = 0; let maxScore = 0; for (const rule of tier.config.routingRules) { maxScore += rule.weight; if (this.evaluateRule(rule, operation, context, tier)) { score += rule.weight; } } if (tier.config.preferredOperations.includes(context.queryType)) { score += 0.2; maxScore += 0.2; } if (context.requiredFeatures) { const supportedFeatures = context.requiredFeatures.filter( (feature) => tier.config.supportedFeatures.includes(feature) ); if (supportedFeatures.length === context.requiredFeatures.length) { score += 0.3; } maxScore += 0.3; } const currentLoad = await this.getCurrentLoad(tier); if (tier.config.maxThroughput && currentLoad < tier.config.maxThroughput * 0.8) { score += 0.1; } maxScore += 0.1; if (await this.isWithinCapacity(tier)) { score += 0.1; } maxScore += 0.1; return maxScore > 0 ? score / maxScore : 0; } /** * Evaluate a single routing rule */ evaluateRule(rule, _operation, context, _tier) { let actualValue; switch (rule.condition) { case "age": if (context.frames && context.frames.length > 0) { const avgAge = context.frames.reduce( (sum, frame) => sum + (Date.now() - frame.created_at), 0 ) / context.frames.length; actualValue = avgAge; } else if (context.timeRange) { actualValue = Date.now() - context.timeRange.end.getTime(); } else { return false; } break; case "query_type": actualValue = context.queryType; break; case "feature": actualValue = context.requiredFeatures || []; break; case "priority": actualValue = context.priority || "medium"; break; case "size": actualValue = context.frames ? context.frames.length : 0; break; default: return false; } return this.compareValues(actualValue, rule.operator, rule.value); } /** * Compare values based on operator */ compareValues(actual, operator, expected) { switch (operator) { case ">": return actual > expected; case "<": return actual < expected; case "=": case "==": return actual === expected; case "!=": return actual !== expected; case "in": return Array.isArray(expected) && expected.includes(actual); case "not_in": return Array.isArray(expected) && !expected.includes(actual); case "contains": return Array.isArray(actual) && actual.some((item) => expected.includes(item)); default: return false; } } /** * Execute query on specific tier */ async executeOnTier(tier, executor) { const startTime = Date.now(); try { const result = await executor(tier.adapter); const duration = Date.now() - startTime; logger.debug(`Query executed on tier ${tier.name} in ${duration}ms`); this.emit("queryExecuted", { tierName: tier.name, duration, success: true }); return result; } catch (error) { const duration = Date.now() - startTime; logger.error( `Query failed on tier ${tier.name} after ${duration}ms:`, error ); this.emit("queryExecuted", { tierName: tier.name, duration, success: false, error }); throw error; } } /** * Generate cache key for routing decisions */ generateCacheKey(operation, context) { const keyParts = [ operation, context.queryType, context.priority || "medium", (context.requiredFeatures || []).sort().join(","), context.timeRange ? `${context.timeRange.start.getTime()}-${context.timeRange.end.getTime()}` : "" ]; return keyParts.join("|"); } /** * Cache routing decision */ cacheDecision(key, decision) { if (this.decisionCache.size >= this.maxCacheSize) { const firstKey = this.decisionCache.keys().next().value; this.decisionCache.delete(firstKey); } this.decisionCache.set(key, decision); } /** * Estimate query latency for a tier */ estimateLatency(tier, operation, context) { const baseLatency = this.metrics.latencyByTier.get(tier.name) || tier.config.maxLatency || 100; let multiplier = 1; switch (context.queryType) { case "search": multiplier = 1.5; break; case "analytics": multiplier = 2; break; case "bulk": multiplier = 3; break; default: multiplier = 1; } return baseLatency * multiplier; } /** * Recommend cache strategy for the context */ recommendCacheStrategy(tier, context) { if (context.cacheStrategy && context.cacheStrategy !== "none") { return context.cacheStrategy; } if (tier.name === "hot" || tier.name === "memory") { return "read_write"; } else if (context.queryType === "read") { return "read"; } return "none"; } /** * Generate human-readable rationale for routing decision */ generateRationale(tier, operation, context, score) { const reasons = []; if (tier.config.preferredOperations.includes(context.queryType)) { reasons.push(`optimized for ${context.queryType} operations`); } if (context.requiredFeatures?.every( (feature) => tier.config.supportedFeatures.includes(feature) )) { reasons.push( `supports all required features (${context.requiredFeatures.join(", ")})` ); } if (score > 0.8) { reasons.push("high confidence match"); } else if (score > 0.6) { reasons.push("good match"); } else if (score > 0.4) { reasons.push("acceptable match"); } return reasons.length > 0 ? reasons.join(", ") : "default tier selection"; } /** * Get current load for a tier */ async getCurrentLoad(tier) { return this.metrics.queriesByTier.get(tier.name) || 0; } /** * Check if tier is within capacity limits */ async isWithinCapacity(tier) { try { const stats = await tier.adapter.getStats(); if (tier.config.maxFrames && stats.totalFrames >= tier.config.maxFrames) { return false; } if (tier.config.maxSizeMB && stats.diskUsage >= tier.config.maxSizeMB * 1024 * 1024) { return false; } return true; } catch (error) { logger.warn(`Failed to check capacity for tier ${tier.name}:`, error); return true; } } /** * Update routing metrics */ updateMetrics(tierName, startTime, success) { const duration = Date.now() - startTime; this.metrics.queriesByTier.set( tierName, (this.metrics.queriesByTier.get(tierName) || 0) + 1 ); if (success) { const currentAvg = this.metrics.latencyByTier.get(tierName) || 0; const count = this.metrics.queriesByTier.get(tierName) || 1; const newAvg = (currentAvg * (count - 1) + duration) / count; this.metrics.latencyByTier.set(tierName, newAvg); this.metrics.averageLatency = (this.metrics.averageLatency * (this.metrics.totalQueries - 1) + duration) / this.metrics.totalQueries; } else { this.metrics.errorsByTier.set( tierName, (this.metrics.errorsByTier.get(tierName) || 0) + 1 ); } } /** * Get current routing metrics */ getMetrics() { const cacheRequests = this.metrics.routingDecisions; const cacheHits = cacheRequests - this.decisionCache.size; this.metrics.cacheHitRate = cacheRequests > 0 ? cacheHits / cacheRequests : 0; return { ...this.metrics }; } /** * Get registered tiers */ getTiers() { return Array.from(this.tiers.values()).sort( (a, b) => b.priority - a.priority ); } /** * Clear routing decision cache */ clearCache() { this.decisionCache.clear(); logger.info("Routing decision cache cleared"); } /** * Get tier by name */ getTier(name) { return this.tiers.get(name); } } export { QueryRouter }; //# sourceMappingURL=query-router.js.map