@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.
274 lines (273 loc) • 8.85 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 {
createPrivacyFilter
} from "./privacy-filter.js";
import { logger } from "../monitoring/logger.js";
const DEFAULT_UNIFIED_CONTEXT_CONFIG = {
totalTokenBudget: 8e3,
userKnowledgeBudget: 0.2,
// 20%
taskContextBudget: 0.7,
// 70%
systemContextBudget: 0.1,
// 10%
privacyMode: "standard"
};
function estimateTokens(content) {
if (!content) return 0;
return Math.ceil(content.length / 4);
}
function truncateToTokenBudget(content, tokenBudget) {
if (!content) return "";
const estimatedTokens = estimateTokens(content);
if (estimatedTokens <= tokenBudget) {
return content;
}
const charLimit = tokenBudget * 4;
const truncated = content.substring(0, charLimit);
const lastSpace = truncated.lastIndexOf(" ");
if (lastSpace > charLimit * 0.8) {
return truncated.substring(0, lastSpace) + "...";
}
return truncated + "...";
}
class UnifiedContextAssembler {
stackMemoryRetrieval;
diffMemHooks;
config;
privacyFilter;
constructor(stackMemoryRetrieval, diffMemHooks, config = {}) {
this.stackMemoryRetrieval = stackMemoryRetrieval;
this.diffMemHooks = diffMemHooks;
this.config = { ...DEFAULT_UNIFIED_CONTEXT_CONFIG, ...config };
this.privacyFilter = createPrivacyFilter(this.config.privacyMode);
const totalAllocation = this.config.userKnowledgeBudget + this.config.taskContextBudget + this.config.systemContextBudget;
if (Math.abs(totalAllocation - 1) > 1e-3) {
logger.warn("Budget allocations do not sum to 1.0", {
userKnowledge: this.config.userKnowledgeBudget,
taskContext: this.config.taskContextBudget,
systemContext: this.config.systemContextBudget,
total: totalAllocation
});
}
}
/**
* Assemble unified context from all sources
*/
async assemble(query) {
const startTime = Date.now();
let totalPrivacyFiltered = 0;
const userKnowledgeBudget = Math.floor(
this.config.totalTokenBudget * this.config.userKnowledgeBudget
);
const taskContextBudget = Math.floor(
this.config.totalTokenBudget * this.config.taskContextBudget
);
const systemContextBudget = Math.floor(
this.config.totalTokenBudget * this.config.systemContextBudget
);
const {
content: userKnowledge,
memories: diffMemMemories,
available: diffMemAvailable
} = await this.gatherUserKnowledge(query, userKnowledgeBudget);
const userKnowledgeFiltered = this.privacyFilter.filter(userKnowledge);
totalPrivacyFiltered += userKnowledgeFiltered.redactedCount;
const filteredUserKnowledge = truncateToTokenBudget(
userKnowledgeFiltered.filtered,
userKnowledgeBudget
);
const { content: taskContext, frameCount } = await this.gatherTaskContext(
query,
taskContextBudget
);
const taskContextFiltered = this.privacyFilter.filter(taskContext);
totalPrivacyFiltered += taskContextFiltered.redactedCount;
const filteredTaskContext = truncateToTokenBudget(
taskContextFiltered.filtered,
taskContextBudget
);
const systemContext = this.gatherSystemContext(systemContextBudget);
const systemContextFiltered = this.privacyFilter.filter(systemContext);
totalPrivacyFiltered += systemContextFiltered.redactedCount;
const filteredSystemContext = truncateToTokenBudget(
systemContextFiltered.filtered,
systemContextBudget
);
const combined = this.combineContextSections(
filteredUserKnowledge,
filteredTaskContext,
filteredSystemContext
);
const tokenUsage = {
userKnowledge: estimateTokens(filteredUserKnowledge),
taskContext: estimateTokens(filteredTaskContext),
systemContext: estimateTokens(filteredSystemContext),
total: estimateTokens(combined),
budget: this.config.totalTokenBudget
};
const metadata = {
diffMemAvailable,
diffMemMemories,
stackMemoryFrames: frameCount,
privacyFiltered: totalPrivacyFiltered
};
logger.info("Unified context assembled", {
query: query.substring(0, 50),
tokenUsage,
metadata,
assemblyTimeMs: Date.now() - startTime
});
return {
userKnowledge: filteredUserKnowledge,
taskContext: filteredTaskContext,
systemContext: filteredSystemContext,
combined,
tokenUsage,
metadata
};
}
/**
* Gather user knowledge from DiffMem
*/
async gatherUserKnowledge(query, tokenBudget) {
if (!this.diffMemHooks) {
return { content: "", memories: 0, available: false };
}
try {
const status = await this.diffMemHooks.getStatus();
if (!status.connected) {
logger.debug("DiffMem not connected");
return { content: "", memories: 0, available: false };
}
const memories = await this.diffMemHooks.getRelevantMemories(query, 10);
if (memories.length === 0) {
return { content: "", memories: 0, available: true };
}
const sections = ["## User Knowledge"];
const byCategory = /* @__PURE__ */ new Map();
for (const memory of memories) {
const existing = byCategory.get(memory.category) || [];
existing.push(memory);
byCategory.set(memory.category, existing);
}
for (const [category, categoryMemories] of byCategory) {
sections.push(`
### ${this.formatCategory(category)}`);
for (const memory of categoryMemories) {
const confidence = memory.confidence >= 0.8 ? "(high confidence)" : memory.confidence >= 0.5 ? "" : "(tentative)";
sections.push(`- ${memory.content} ${confidence}`);
}
}
const content = sections.join("\n");
return {
content: truncateToTokenBudget(content, tokenBudget),
memories: memories.length,
available: true
};
} catch (error) {
logger.warn("Failed to gather user knowledge from DiffMem", { error });
return { content: "", memories: 0, available: false };
}
}
/**
* Format category name for display
*/
formatCategory(category) {
return category.split("_").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
}
/**
* Gather task context from StackMemory
*/
async gatherTaskContext(query, tokenBudget) {
try {
const retrievedContext = await this.stackMemoryRetrieval.retrieveContext(
query,
{
tokenBudget
}
);
return {
content: retrievedContext.context,
frameCount: retrievedContext.frames.length
};
} catch (error) {
logger.warn("Failed to gather task context from StackMemory", { error });
return { content: "", frameCount: 0 };
}
}
/**
* Gather system context (environment, timestamps, etc.)
*/
gatherSystemContext(tokenBudget) {
const sections = ["## System Context"];
sections.push(`
**Current Time**: ${(/* @__PURE__ */ new Date()).toISOString()}`);
const nodeEnv = process.env.NODE_ENV || "development";
sections.push(`**Environment**: ${nodeEnv}`);
const projectId = process.env.STACKMEMORY_PROJECT_ID;
if (projectId) {
sections.push(`**Project**: ${projectId}`);
}
const sessionId = process.env.STACKMEMORY_SESSION_ID;
if (sessionId) {
sections.push(`**Session**: ${sessionId.substring(0, 8)}...`);
}
const content = sections.join("\n");
return truncateToTokenBudget(content, tokenBudget);
}
/**
* Combine all context sections into a single string
*/
combineContextSections(userKnowledge, taskContext, systemContext) {
const sections = [];
if (taskContext) {
sections.push(taskContext);
}
if (userKnowledge) {
sections.push(userKnowledge);
}
if (systemContext) {
sections.push(systemContext);
}
return sections.join("\n\n---\n\n");
}
/**
* Update privacy mode
*/
setPrivacyMode(mode) {
this.config.privacyMode = mode;
this.privacyFilter.setMode(mode);
}
/**
* Get current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Update configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
if (config.privacyMode) {
this.privacyFilter.setMode(config.privacyMode);
}
}
}
function createUnifiedContextAssembler(stackMemoryRetrieval, diffMemHooks = null, config = {}) {
return new UnifiedContextAssembler(
stackMemoryRetrieval,
diffMemHooks,
config
);
}
export {
DEFAULT_UNIFIED_CONTEXT_CONFIG,
UnifiedContextAssembler,
createUnifiedContextAssembler
};
//# sourceMappingURL=unified-context-assembler.js.map