@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
392 lines (391 loc) • 12.6 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 "../../../core/monitoring/logger.js";
import { FrameManager } from "../../../core/context/index.js";
import { sharedContextLayer } from "../../../core/context/shared-context-layer.js";
import { ContextRetriever } from "../../../core/retrieval/context-retriever.js";
import { sessionManager } from "../../../core/session/index.js";
import { ContextBudgetManager } from "./context-budget-manager.js";
class StackMemoryContextLoader {
frameManager;
contextRetriever;
budgetManager;
config;
constructor(config) {
this.config = {
maxTokens: 3200,
// Leave room for task description
lookbackDays: 30,
similarityThreshold: 0.7,
patternDetectionEnabled: true,
includeFailedAttempts: true,
crossSessionSearch: true,
...config
};
this.budgetManager = new ContextBudgetManager({
maxTokens: this.config.maxTokens,
priorityWeights: {
task: 0.15,
recentWork: 0.3,
patterns: 0.25,
decisions: 0.2,
dependencies: 0.1
}
});
logger.info("StackMemory context loader initialized", {
maxTokens: this.config.maxTokens,
lookbackDays: this.config.lookbackDays,
patternDetection: this.config.patternDetectionEnabled
});
}
async initialize() {
try {
await sessionManager.initialize();
await sharedContextLayer.initialize();
const session = await sessionManager.getOrCreateSession({});
if (session.database) {
this.frameManager = new FrameManager(
session.database,
session.projectId
);
this.contextRetriever = new ContextRetriever(session.database);
}
logger.info("Context loader initialized successfully");
} catch (error) {
logger.error("Failed to initialize context loader", error);
throw error;
}
}
/**
* Load context for Ralph loop initialization
*/
async loadInitialContext(request) {
logger.info("Loading initial context for Ralph loop", {
task: request.task.substring(0, 100),
usePatterns: request.usePatterns,
useSimilarTasks: request.useSimilarTasks
});
const sources = [];
let totalTokens = 0;
try {
if (request.useSimilarTasks) {
const similarTasks2 = await this.findSimilarTasks(request.task);
if (similarTasks2.length > 0) {
const tasksContext = await this.extractTaskContext(similarTasks2);
sources.push({
type: "similar_tasks",
weight: 0.3,
content: tasksContext,
tokens: this.budgetManager.estimateTokens(tasksContext)
});
totalTokens += sources[sources.length - 1].tokens;
}
}
if (request.usePatterns) {
const patterns2 = await this.extractRelevantPatterns(request.task);
if (patterns2.length > 0) {
const patternsContext = await this.formatPatterns(patterns2);
sources.push({
type: "historical_patterns",
weight: 0.25,
content: patternsContext,
tokens: this.budgetManager.estimateTokens(patternsContext)
});
totalTokens += sources[sources.length - 1].tokens;
}
}
const decisions = await this.loadRecentDecisions();
if (decisions.length > 0) {
const decisionsContext = this.formatDecisions(decisions);
sources.push({
type: "recent_decisions",
weight: 0.2,
content: decisionsContext,
tokens: this.budgetManager.estimateTokens(decisionsContext)
});
totalTokens += sources[sources.length - 1].tokens;
}
const projectContext = await this.loadProjectContext(request.task);
if (projectContext) {
sources.push({
type: "project_context",
weight: 0.15,
content: projectContext,
tokens: this.budgetManager.estimateTokens(projectContext)
});
totalTokens += sources[sources.length - 1].tokens;
}
const budgetedSources = this.budgetManager.allocateBudget({ sources });
const synthesizedContext = this.synthesizeContext(
budgetedSources.sources
);
logger.info("Context loaded successfully", {
totalSources: sources.length,
totalTokens,
budgetedTokens: budgetedSources.sources.reduce(
(sum, s) => sum + s.tokens,
0
)
});
return {
context: synthesizedContext,
sources: budgetedSources.sources,
metadata: {
totalTokens: budgetedSources.sources.reduce(
(sum, s) => sum + s.tokens,
0
),
sourcesCount: budgetedSources.sources.length,
patterns: request.usePatterns ? patterns : [],
similarTasks: request.useSimilarTasks ? similarTasks : []
}
};
} catch (error) {
logger.error("Failed to load context", error);
throw error;
}
}
/**
* Find similar tasks from StackMemory history
*/
async findSimilarTasks(taskDescription) {
if (!this.frameManager || !this.contextRetriever) {
return [];
}
try {
const searchResults = await this.contextRetriever.search(
taskDescription,
{
maxResults: 10,
types: ["task", "subtask"],
timeFilter: {
days: this.config.lookbackDays
}
}
);
const similarities = [];
for (const result of searchResults) {
const similarity = this.calculateTaskSimilarity(
taskDescription,
result.content
);
if (similarity >= this.config.similarityThreshold) {
similarities.push({
frameId: result.frameId,
task: result.content,
similarity,
outcome: await this.determineTaskOutcome(result.frameId),
createdAt: result.timestamp,
sessionId: result.sessionId || "unknown"
});
}
}
return similarities.sort((a, b) => {
const aScore = a.similarity * (a.outcome === "success" ? 1.2 : 1);
const bScore = b.similarity * (b.outcome === "success" ? 1.2 : 1);
return bScore - aScore;
}).slice(0, 5);
} catch (error) {
logger.error("Failed to find similar tasks", error);
return [];
}
}
/**
* Extract relevant patterns from historical data
*/
async extractRelevantPatterns(taskDescription) {
try {
const context = await sharedContextLayer.getSharedContext();
if (!context) return [];
const relevantPatterns = [];
for (const pattern of context.globalPatterns) {
const relevance = this.calculatePatternRelevance(
taskDescription,
pattern.pattern
);
if (relevance >= 0.5) {
relevantPatterns.push({
pattern: pattern.pattern,
type: pattern.type,
frequency: pattern.frequency,
lastSeen: pattern.lastSeen,
relevance,
resolution: pattern.resolution,
examples: await this.getPatternExamples(pattern.pattern)
});
}
}
return relevantPatterns.sort(
(a, b) => b.relevance * Math.log(b.frequency + 1) - a.relevance * Math.log(a.frequency + 1)
).slice(0, 8);
} catch (error) {
logger.error("Failed to extract patterns", error);
return [];
}
}
/**
* Load recent decisions that might be relevant
*/
async loadRecentDecisions() {
try {
const context = await sharedContextLayer.getSharedContext();
if (!context) return [];
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1e3;
return context.decisionLog.filter((d) => d.timestamp >= cutoff && d.outcome === "success").sort((a, b) => b.timestamp - a.timestamp).slice(0, 5);
} catch (error) {
logger.error("Failed to load recent decisions", error);
return [];
}
}
/**
* Load project-specific context
*/
async loadProjectContext(taskDescription) {
try {
if (!this.contextRetriever) return null;
const projectInfo = await this.contextRetriever.search(taskDescription, {
maxResults: 3,
types: ["task"],
projectSpecific: true
});
if (projectInfo.length === 0) return null;
const contextParts = [];
for (const info of projectInfo) {
contextParts.push(`Project context: ${info.content}`);
}
return contextParts.join("\n\n");
} catch (error) {
logger.error("Failed to load project context", error);
return null;
}
}
/**
* Calculate similarity between task descriptions
*/
calculateTaskSimilarity(task1, task2) {
const words1 = new Set(task1.toLowerCase().split(/\s+/));
const words2 = new Set(task2.toLowerCase().split(/\s+/));
const intersection = new Set([...words1].filter((x) => words2.has(x)));
const union = /* @__PURE__ */ new Set([...words1, ...words2]);
return intersection.size / union.size;
}
/**
* Calculate pattern relevance to current task
*/
calculatePatternRelevance(taskDescription, pattern) {
const taskWords = taskDescription.toLowerCase().split(/\s+/);
const patternWords = pattern.toLowerCase().split(/\s+/);
let matches = 0;
for (const word of taskWords) {
if (patternWords.some((p) => p.includes(word) || word.includes(p))) {
matches++;
}
}
return matches / taskWords.length;
}
/**
* Extract context from similar tasks
*/
async extractTaskContext(similarities) {
const contextParts = [];
contextParts.push("Similar tasks from history:");
for (const sim of similarities) {
contextParts.push(
`
Task: ${sim.task}
Outcome: ${sim.outcome}
Similarity: ${Math.round(sim.similarity * 100)}%
${sim.outcome === "success" ? "\u2705 Successfully completed" : "\u274C Had issues"}
`.trim()
);
}
return contextParts.join("\n\n");
}
/**
* Format patterns for context inclusion
*/
async formatPatterns(patterns2) {
const contextParts = [];
contextParts.push("Relevant patterns from experience:");
for (const pattern of patterns2) {
contextParts.push(
`
Pattern: ${pattern.pattern}
Type: ${pattern.type}
Frequency: ${pattern.frequency} occurrences
${pattern.resolution ? `Resolution: ${pattern.resolution}` : ""}
Relevance: ${Math.round(pattern.relevance * 100)}%
`.trim()
);
}
return contextParts.join("\n\n");
}
/**
* Format decisions for context inclusion
*/
formatDecisions(decisions) {
const contextParts = [];
contextParts.push("Recent successful decisions:");
for (const decision of decisions) {
contextParts.push(
`
Decision: ${decision.decision}
Reasoning: ${decision.reasoning}
Date: ${new Date(decision.timestamp).toLocaleDateString()}
`.trim()
);
}
return contextParts.join("\n\n");
}
/**
* Synthesize all context sources into coherent input
*/
synthesizeContext(sources) {
if (sources.length === 0) {
return "No relevant historical context found.";
}
const contextParts = [];
contextParts.push("Context from StackMemory:");
const sortedSources = sources.sort((a, b) => b.weight - a.weight);
for (const source of sortedSources) {
contextParts.push(
`
--- ${source.type.replace("_", " ").toUpperCase()} ---`
);
contextParts.push(source.content);
}
contextParts.push(
"\nUse this context to inform your approach to the current task."
);
return contextParts.join("\n");
}
/**
* Determine task outcome from frame history
*/
async determineTaskOutcome(frameId) {
try {
if (!this.frameManager) return "unknown";
const frame = await this.frameManager.getFrame(frameId);
if (!frame) return "unknown";
if (frame.state === "closed" && frame.outputs) {
return "success";
}
return frame.state === "closed" ? "failure" : "unknown";
} catch {
return "unknown";
}
}
/**
* Get examples of a specific pattern
*/
async getPatternExamples(_pattern) {
return [];
}
}
const stackMemoryContextLoader = new StackMemoryContextLoader();
export {
StackMemoryContextLoader,
stackMemoryContextLoader
};