@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.
378 lines (377 loc) • 11.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";
const DEFAULT_CONFIG = {
enabled: !!process.env.DIFFMEM_ENDPOINT,
endpoint: process.env.DIFFMEM_ENDPOINT || "http://localhost:3100",
autoFetchCategories: ["preference", "expertise", "pattern"],
autoLearnEnabled: true,
learningConfidenceThreshold: 0.7,
maxMemoriesPerSession: 50
};
class DiffMemHooks {
config;
fetchedMemories = [];
learningBuffer = [];
isConnected = false;
frameManager;
sessionStartTime = 0;
constructor(config = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
/**
* Register session hooks with the event emitter
*/
register(emitter, frameManager) {
this.frameManager = frameManager;
if (!this.config.enabled) {
logger.debug("DiffMem hooks disabled - skipping registration");
return;
}
emitter.registerHandler("session_start", this.onSessionStart.bind(this));
emitter.registerHandler("session_end", this.onSessionEnd.bind(this));
logger.info("DiffMem hooks registered", {
endpoint: this.config.endpoint,
autoFetchCategories: this.config.autoFetchCategories,
autoLearnEnabled: this.config.autoLearnEnabled
});
}
/**
* Handle session start - fetch user knowledge
*/
async onSessionStart(event) {
const sessionEvent = event;
this.sessionStartTime = Date.now();
try {
const status = await this.checkStatus();
this.isConnected = status.connected;
if (!this.isConnected) {
logger.debug("DiffMem not available - skipping memory fetch");
return;
}
const query = {
categories: this.config.autoFetchCategories,
limit: this.config.maxMemoriesPerSession,
minConfidence: this.config.learningConfidenceThreshold
};
this.fetchedMemories = await this.fetchMemories(query);
if (this.frameManager && this.fetchedMemories.length > 0) {
await this.injectAsAnchors(sessionEvent.data.sessionId);
}
logger.info("DiffMem session start completed", {
memoriesFetched: this.fetchedMemories.length,
sessionId: sessionEvent.data.sessionId
});
} catch (error) {
logger.warn("DiffMem session start failed", {
error: error instanceof Error ? error.message : String(error)
});
this.isConnected = false;
}
}
/**
* Handle session end - sync buffered learnings
*/
async onSessionEnd(event) {
const sessionEvent = event;
if (!this.config.autoLearnEnabled || this.learningBuffer.length === 0) {
logger.debug("No learnings to sync on session end");
return;
}
if (!this.isConnected) {
const status = await this.checkStatus();
if (!status.connected) {
logger.warn("DiffMem not available - learnings not synced", {
bufferedCount: this.learningBuffer.length
});
return;
}
this.isConnected = true;
}
try {
await this.syncLearnings();
const sessionDuration = Date.now() - this.sessionStartTime;
logger.info("DiffMem session end completed", {
learningSynced: this.learningBuffer.length,
sessionId: sessionEvent.data.sessionId,
sessionDurationMs: sessionDuration
});
this.learningBuffer = [];
} catch (error) {
logger.warn("DiffMem session end sync failed", {
error: error instanceof Error ? error.message : String(error),
bufferedCount: this.learningBuffer.length
});
}
}
/**
* Record a learning during session
*/
recordLearning(insight) {
if (!this.config.autoLearnEnabled) {
return;
}
if (insight.confidence < this.config.learningConfidenceThreshold) {
logger.debug("Learning below confidence threshold", {
confidence: insight.confidence,
threshold: this.config.learningConfidenceThreshold
});
return;
}
const learning = {
...insight,
timestamp: Date.now()
};
this.learningBuffer.push(learning);
logger.debug("Learning recorded", {
category: learning.category,
confidence: learning.confidence,
bufferSize: this.learningBuffer.length
});
}
/**
* Get fetched memories
*/
getUserKnowledge() {
return [...this.fetchedMemories];
}
/**
* Format memories for LLM context with token budget
*/
formatForContext(maxTokens = 2e3) {
if (this.fetchedMemories.length === 0) {
return "";
}
const sortedMemories = [...this.fetchedMemories].sort((a, b) => {
if (b.confidence !== a.confidence) {
return b.confidence - a.confidence;
}
return b.timestamp - a.timestamp;
});
const sections = /* @__PURE__ */ new Map();
for (const memory of sortedMemories) {
const category = memory.category;
if (!sections.has(category)) {
sections.set(category, []);
}
const categoryMemories = sections.get(category);
if (categoryMemories) {
categoryMemories.push(memory.content);
}
}
const lines = ["## User Knowledge"];
let estimatedTokens = 5;
const categoryLabels = {
preference: "Preferences",
expertise: "Expertise",
project_knowledge: "Project Knowledge",
pattern: "Patterns",
correction: "Corrections"
};
for (const [category, contents] of sections) {
const label = categoryLabels[category] || category;
const categoryHeader = `
### ${label}`;
const headerTokens = Math.ceil(categoryHeader.length / 4);
if (estimatedTokens + headerTokens > maxTokens) {
break;
}
lines.push(categoryHeader);
estimatedTokens += headerTokens;
for (const content of contents) {
const contentLine = `- ${content}`;
const contentTokens = Math.ceil(contentLine.length / 4);
if (estimatedTokens + contentTokens > maxTokens) {
lines.push("- (additional items truncated for token budget)");
break;
}
lines.push(contentLine);
estimatedTokens += contentTokens;
}
}
return lines.join("\n");
}
/**
* Get current connection status
*/
getStatus() {
return {
connected: this.isConnected,
memoriesLoaded: this.fetchedMemories.length,
learningsBuffered: this.learningBuffer.length
};
}
/**
* Update configuration
*/
updateConfig(config) {
this.config = { ...this.config, ...config };
logger.debug("DiffMem config updated", { config: this.config });
}
// Private methods
/**
* Check DiffMem service status
*/
async checkStatus() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3e3);
const response = await fetch(`${this.config.endpoint}/status`, {
method: "GET",
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
return { connected: false, memoryCount: 0, lastSync: null };
}
const status = await response.json();
return {
connected: true,
memoryCount: status.memoryCount || 0,
lastSync: status.lastSync || null,
version: status.version
};
} catch {
return { connected: false, memoryCount: 0, lastSync: null };
}
}
/**
* Fetch memories from DiffMem
*/
async fetchMemories(query) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5e3);
const response = await fetch(`${this.config.endpoint}/memories/query`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(query),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
logger.warn("DiffMem query failed", { status: response.status });
return [];
}
const data = await response.json();
return data.memories || [];
} catch (error) {
logger.debug("DiffMem fetch failed", {
error: error instanceof Error ? error.message : String(error)
});
return [];
}
}
/**
* Sync buffered learnings to DiffMem
*/
async syncLearnings() {
if (this.learningBuffer.length === 0) {
return;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 1e4);
try {
const response = await fetch(`${this.config.endpoint}/memories/learn`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ insights: this.learningBuffer }),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`Sync failed with status ${response.status}`);
}
logger.info("Learnings synced to DiffMem", {
count: this.learningBuffer.length
});
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
/**
* Inject fetched memories as frame anchors
*/
async injectAsAnchors(sessionId) {
if (!this.frameManager || this.fetchedMemories.length === 0) {
return;
}
try {
const byCategory = /* @__PURE__ */ new Map();
for (const memory of this.fetchedMemories) {
if (!byCategory.has(memory.category)) {
byCategory.set(memory.category, []);
}
const categoryList = byCategory.get(memory.category);
if (categoryList) {
categoryList.push(memory);
}
}
for (const [category, memories] of byCategory) {
const highConfidence = memories.filter((m) => m.confidence >= 0.8);
for (const memory of highConfidence.slice(0, 5)) {
const anchorType = this.categoryToAnchorType(category);
const priority = Math.round(memory.confidence * 10);
this.frameManager.addAnchor(anchorType, memory.content, priority, {
source: "diffmem",
category: memory.category,
memoryId: memory.id,
confidence: memory.confidence,
sessionId
});
}
}
logger.debug("Memories injected as anchors", {
totalMemories: this.fetchedMemories.length,
anchorsCreated: Math.min(
this.fetchedMemories.filter((m) => m.confidence >= 0.8).length,
25
)
});
} catch (error) {
logger.warn("Failed to inject memories as anchors", {
error: error instanceof Error ? error.message : String(error)
});
}
}
/**
* Map memory category to anchor type
*/
categoryToAnchorType(category) {
switch (category) {
case "preference":
return "CONSTRAINT";
case "expertise":
return "FACT";
case "project_knowledge":
return "FACT";
case "pattern":
return "INTERFACE_CONTRACT";
case "correction":
return "DECISION";
default:
return "FACT";
}
}
}
let instance = null;
function getDiffMemHooks(config) {
if (!instance) {
instance = new DiffMemHooks(config);
} else if (config) {
instance.updateConfig(config);
}
return instance;
}
function resetDiffMemHooks() {
instance = null;
}
export {
DiffMemHooks,
getDiffMemHooks,
resetDiffMemHooks
};
//# sourceMappingURL=diffmem-hooks.js.map