@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.
480 lines (479 loc) • 13.9 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 { logger } from "../monitoring/logger.js";
class ContextRetriever {
adapter;
strategies = /* @__PURE__ */ new Map();
queryCache = /* @__PURE__ */ new Map();
cacheMaxSize = 100;
cacheExpiryMs = 3e5;
// 5 minutes
constructor(adapter) {
this.adapter = adapter;
this.initializeStrategies();
}
initializeStrategies() {
this.strategies.set("keyword", {
name: "Keyword Search",
searchType: "text",
boost: {
name: 2,
digest_text: 1.5,
inputs: 1.2,
outputs: 1.2
},
fallbackStrategy: "semantic"
});
this.strategies.set("semantic", {
name: "Semantic Search",
searchType: "vector",
fallbackStrategy: "hybrid"
});
this.strategies.set("hybrid", {
name: "Hybrid Search",
searchType: "hybrid",
weights: { text: 0.6, vector: 0.4 },
boost: {
name: 2,
digest_text: 1.5
},
fallbackStrategy: "keyword"
});
this.strategies.set("recent", {
name: "Recent Activity",
searchType: "text",
boost: {
created_at: 3,
closed_at: 2
},
fallbackStrategy: "hybrid"
});
this.strategies.set("debug", {
name: "Debug Context",
searchType: "hybrid",
weights: { text: 0.8, vector: 0.2 },
boost: {
type: 2.5,
// Boost error frames
digest_text: 2,
outputs: 1.8
},
fallbackStrategy: "keyword"
});
}
async retrieveContext(query) {
const startTime = Date.now();
if (!query.text || query.text.trim().length === 0) {
logger.debug("Empty query provided, returning empty result");
return {
contexts: [],
totalMatches: 0,
retrievalTimeMs: Date.now() - startTime,
strategy: "empty_query",
queryAnalysis: {
intent: "general",
concepts: [],
complexity: "simple"
}
};
}
const cacheKey = this.generateCacheKey(query);
const cached = this.getCachedResult(cacheKey);
if (cached) {
logger.debug("Context retrieval cache hit");
return cached;
}
try {
logger.info("Starting LLM-driven context retrieval", {
query: query.text
});
const queryAnalysis = await this.analyzeQuery(query);
const strategy = this.selectStrategy(queryAnalysis, query);
logger.debug("Selected retrieval strategy", {
strategy: strategy.name,
analysis: queryAnalysis
});
const contexts = await this.executeRetrieval(
query,
strategy,
queryAnalysis
);
const rankedContexts = await this.rankAndFilter(
contexts,
query,
queryAnalysis
);
const result = {
contexts: rankedContexts,
totalMatches: contexts.length,
retrievalTimeMs: Date.now() - startTime,
strategy: strategy.name,
queryAnalysis
};
this.cacheResult(cacheKey, result);
logger.info("Context retrieval completed", {
resultsCount: rankedContexts.length,
timeMs: result.retrievalTimeMs,
strategy: strategy.name
});
return result;
} catch (error) {
logger.error("Context retrieval failed:", error);
return {
contexts: [],
totalMatches: 0,
retrievalTimeMs: Date.now() - startTime,
strategy: "fallback",
queryAnalysis: {
intent: "unknown",
concepts: [],
complexity: "simple"
}
};
}
}
async analyzeQuery(query) {
const text = query.text.toLowerCase().trim();
const words = text.split(/\s+/);
let intent = "general";
if (this.containsKeywords(text, [
"error",
"exception",
"fail",
"bug",
"issue",
"problem",
"debug"
])) {
intent = "debug";
} else if (this.containsKeywords(text, ["how", "what", "why", "when", "where"])) {
intent = "explanation";
} else if (this.containsKeywords(text, [
"implement",
"create",
"build",
"add",
"develop"
])) {
intent = "implementation";
} else if (this.containsKeywords(text, [
"recent",
"latest",
"last",
"current",
"happened"
])) {
intent = "recent_activity";
}
const concepts = this.extractConcepts(text);
let complexity = "simple";
if (words.length > 10 || concepts.length > 5) {
complexity = "complex";
} else if (words.length > 5 || concepts.length > 2) {
complexity = "moderate";
}
return { intent, concepts, complexity };
}
containsKeywords(text, keywords) {
return keywords.some(
(keyword) => text.toLowerCase().includes(keyword.toLowerCase())
);
}
extractConcepts(text) {
const technicalTerms = [
"database",
"sql",
"query",
"index",
"migration",
"adapter",
"frame",
"event",
"anchor",
"digest",
"context",
"search",
"vector",
"embedding",
"similarity",
"score",
"rank",
"performance",
"optimization",
"cache",
"pool",
"connection",
"error",
"exception",
"debug",
"trace",
"log",
"monitor"
];
const concepts = [];
const words = text.split(/\W+/).map((w) => w.toLowerCase());
for (const term of technicalTerms) {
if (words.includes(term)) {
concepts.push(term);
}
}
const bigrams = this.extractBigrams(words);
const technicalBigrams = [
"database adapter",
"query router",
"connection pool",
"vector search"
];
for (const bigram of bigrams) {
if (technicalBigrams.includes(bigram)) {
concepts.push(bigram);
}
}
return [...new Set(concepts)];
}
extractBigrams(words) {
const bigrams = [];
for (let i = 0; i < words.length - 1; i++) {
bigrams.push(`${words[i]} ${words[i + 1]}`);
}
return bigrams;
}
selectStrategy(analysis, query) {
if (query.type) {
return this.strategies.get(
query.type === "keyword" ? "keyword" : query.type === "semantic" ? "semantic" : "hybrid"
) || this.strategies.get("hybrid");
}
switch (analysis.intent) {
case "debug":
return this.strategies.get("debug");
case "recent_activity":
return this.strategies.get("recent");
case "explanation":
return analysis.complexity === "simple" ? this.strategies.get("keyword") : this.strategies.get("semantic");
case "implementation":
return this.strategies.get("hybrid");
default:
return analysis.complexity === "complex" ? this.strategies.get("semantic") : this.strategies.get("keyword");
}
}
async executeRetrieval(query, strategy, analysis) {
const searchOptions = {
query: query.text,
searchType: strategy.searchType,
limit: query.maxResults || 20,
scoreThreshold: query.scoreThreshold || 0.1,
boost: strategy.boost
};
if (query.frameTypes) {
searchOptions.fields = ["type", "name", "digest_text"];
}
let rawResults = [];
try {
if (strategy.searchType === "hybrid" && strategy.weights) {
const embedding = await this.generateEmbedding(query.text);
rawResults = await this.adapter.searchHybrid(
query.text,
embedding,
strategy.weights
);
} else {
rawResults = await this.adapter.search(searchOptions);
}
} catch (error) {
logger.warn(`Strategy ${strategy.name} failed, trying fallback:`, error);
if (strategy.fallbackStrategy) {
const fallbackStrategy = this.strategies.get(strategy.fallbackStrategy);
if (fallbackStrategy) {
return this.executeRetrieval(query, fallbackStrategy, analysis);
}
}
return [];
}
return rawResults.map((result) => ({
frame: result,
score: result.score,
relevanceReason: this.generateRelevanceReason(result, query, analysis),
retrievalMethod: strategy.searchType,
matchedFields: this.identifyMatchedFields(result, query)
}));
}
async generateEmbedding(text) {
const hash = this.simpleHash(text);
return Array.from(
{ length: 384 },
(_, i) => (hash + i) % 100 / 100 - 0.5
);
}
simpleHash(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
}
return Math.abs(hash);
}
generateRelevanceReason(frame, query, analysis) {
const reasons = [];
if (frame.name.toLowerCase().includes(query.text.toLowerCase())) {
reasons.push("Frame name matches query");
}
if (frame.digest_text?.toLowerCase().includes(query.text.toLowerCase())) {
reasons.push("Content contains query terms");
}
for (const concept of analysis.concepts) {
if (frame.digest_text?.toLowerCase().includes(concept.toLowerCase()) || frame.name.toLowerCase().includes(concept.toLowerCase())) {
reasons.push(`Related to ${concept}`);
}
}
if (analysis.intent === "debug" && frame.type.includes("error")) {
reasons.push("Error context for debugging");
}
return reasons.length > 0 ? reasons.join("; ") : "General semantic similarity";
}
identifyMatchedFields(frame, query) {
const matched = [];
const queryLower = query.text.toLowerCase();
if (frame.name.toLowerCase().includes(queryLower)) {
matched.push("name");
}
if (frame.digest_text?.toLowerCase().includes(queryLower)) {
matched.push("digest_text");
}
if (frame.type.toLowerCase().includes(queryLower)) {
matched.push("type");
}
return matched;
}
async rankAndFilter(contexts, query, analysis) {
let filtered = contexts;
if (query.timeRange) {
filtered = filtered.filter((ctx) => {
const frameTime = new Date(ctx.frame.created_at);
const start = query.timeRange?.start;
const end = query.timeRange?.end;
return (!start || frameTime >= start) && (!end || frameTime <= end);
});
}
if (query.frameTypes) {
filtered = filtered.filter(
(ctx) => query.frameTypes.includes(ctx.frame.type)
);
}
if (query.scoreThreshold) {
filtered = filtered.filter((ctx) => ctx.score >= query.scoreThreshold);
}
const ranked = filtered.map((ctx) => ({
...ctx,
score: this.calculateEnhancedScore(ctx, query, analysis)
}));
ranked.sort((a, b) => b.score - a.score);
const maxResults = query.maxResults || 20;
return ranked.slice(0, maxResults);
}
calculateEnhancedScore(context, query, analysis) {
let score = context.score;
const ageHours = (Date.now() - context.frame.created_at) / (1e3 * 60 * 60);
if (ageHours < 24) {
score *= 1.2;
} else if (ageHours < 168) {
score *= 1.1;
}
if (context.frame.closed_at) {
score *= 1.1;
}
if (analysis.intent === "debug" && context.frame.type.includes("error")) {
score *= 1.5;
}
if (context.matchedFields.includes("name")) {
score *= 1.3;
}
if (context.matchedFields.length > 1) {
score *= 1.1;
}
if (analysis.intent === "recent_activity" && ageHours > 168) {
score *= 0.5;
}
return score;
}
generateCacheKey(query) {
return JSON.stringify({
text: query.text,
type: query.type,
maxResults: query.maxResults,
frameTypes: query.frameTypes,
scoreThreshold: query.scoreThreshold
});
}
getCachedResult(cacheKey) {
const entry = this.queryCache.get(cacheKey);
if (!entry) return null;
return entry;
}
cacheResult(cacheKey, result) {
if (this.queryCache.size >= this.cacheMaxSize) {
const firstKey = this.queryCache.keys().next().value;
this.queryCache.delete(firstKey);
}
this.queryCache.set(cacheKey, result);
}
// Utility methods for integration
async findSimilarFrames(frameId, limit = 10) {
const frame = await this.adapter.getFrame(frameId);
if (!frame) {
throw new Error(`Frame not found: ${frameId}`);
}
const query = {
text: frame.digest_text || frame.name,
type: "semantic",
maxResults: limit,
scoreThreshold: 0.3
};
const result = await this.retrieveContext(query);
return result.contexts.filter((ctx) => ctx.frame.frame_id !== frameId);
}
async findContextForError(errorMessage, stackTrace) {
const query = {
text: `${errorMessage} ${stackTrace || ""}`.trim(),
type: "hybrid",
maxResults: 15,
frameTypes: ["error", "debug", "function"],
scoreThreshold: 0.2
};
const result = await this.retrieveContext(query);
return result.contexts;
}
async getRecentContext(hours = 24, frameTypes) {
const query = {
text: "recent activity context",
type: "keyword",
maxResults: 50,
timeRange: {
start: new Date(Date.now() - hours * 60 * 60 * 1e3)
},
frameTypes,
scoreThreshold: 0.1
};
const result = await this.retrieveContext(query);
return result.contexts;
}
// Analytics and insights
getRetrievalStats() {
return {
cacheSize: this.queryCache.size,
strategiesCount: this.strategies.size,
availableStrategies: Array.from(this.strategies.keys())
};
}
clearCache() {
this.queryCache.clear();
logger.info("Context retrieval cache cleared");
}
}
export {
ContextRetriever
};
//# sourceMappingURL=context-retriever.js.map