@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
621 lines (620 loc) • 20.2 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 { v4 as uuidv4 } from "uuid";
import * as fs from "fs/promises";
import * as path from "path";
import { sessionManager } from "../session/session-manager.js";
import { logger } from "../monitoring/logger.js";
class SharedContextLayer {
static instance;
contextDir;
cache = /* @__PURE__ */ new Map();
MAX_CACHE_SIZE = 100;
CACHE_TTL = 5 * 60 * 1e3;
// 5 minutes
lastCacheClean = Date.now();
constructor() {
const homeDir = process.env["HOME"] || process.env["USERPROFILE"] || "";
this.contextDir = path.join(homeDir, ".stackmemory", "shared-context");
}
static getInstance() {
if (!SharedContextLayer.instance) {
SharedContextLayer.instance = new SharedContextLayer();
}
return SharedContextLayer.instance;
}
async initialize() {
await fs.mkdir(this.contextDir, { recursive: true });
await fs.mkdir(path.join(this.contextDir, "projects"), { recursive: true });
await fs.mkdir(path.join(this.contextDir, "patterns"), { recursive: true });
await fs.mkdir(path.join(this.contextDir, "decisions"), {
recursive: true
});
}
/**
* Get or create shared context for current project/branch
*/
async getSharedContext(options) {
const session = sessionManager.getCurrentSession();
const projectId = options?.projectId || session?.projectId || "global";
const branch = options?.branch || session?.branch;
const cacheKey = `${projectId}:${branch || "main"}`;
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.lastUpdated < this.CACHE_TTL) {
return cached;
}
}
const context = await this.loadProjectContext(projectId, branch);
if (options?.includeOtherBranches) {
const otherBranches = await this.loadOtherBranchContexts(
projectId,
branch
);
context.sessions.push(...otherBranches);
}
this.cache.set(cacheKey, context);
this.cleanCache();
return context;
}
/**
* Add current session's important frames to shared context
*/
async addToSharedContext(frames, options) {
const session = sessionManager.getCurrentSession();
if (!session) return;
const context = await this.getSharedContext();
const minScore = options?.minScore || 0.7;
const importantFrames = frames.filter((f) => {
const score = this.calculateFrameScore(f);
return score >= minScore;
});
const sessionContext = {
sessionId: session.sessionId,
runId: session.runId,
summary: this.generateSessionSummary(importantFrames),
keyFrames: importantFrames.map((f) => this.summarizeFrame(f)),
createdAt: session.startedAt,
lastActiveAt: Date.now(),
metadata: session.metadata
};
const existingIndex = context.sessions.findIndex(
(s) => s.sessionId === session.sessionId
);
if (existingIndex >= 0) {
context.sessions[existingIndex] = sessionContext;
} else {
context.sessions.push(sessionContext);
}
this.updatePatterns(context, importantFrames);
this.updateReferenceIndex(context, importantFrames);
await this.saveProjectContext(context);
}
/**
* Query shared context for relevant frames
*/
async querySharedContext(query) {
const context = await this.getSharedContext({ includeOtherBranches: true });
let results = [];
for (const session of context.sessions) {
if (query.sessionId && session.sessionId !== query.sessionId) continue;
if (!session.keyFrames || !Array.isArray(session.keyFrames)) continue;
const filtered = session.keyFrames.filter((f) => {
if (query.tags && !query.tags.some((tag) => f.tags.includes(tag)))
return false;
if (query.type && f.type !== query.type) return false;
if (query.minScore && f.score < query.minScore) return false;
return true;
});
results.push(...filtered);
}
results.sort((a, b) => {
const scoreWeight = 0.7;
const recencyWeight = 0.3;
const aScore = a.score * scoreWeight + (1 - (Date.now() - a.createdAt) / (30 * 24 * 60 * 60 * 1e3)) * recencyWeight;
const bScore = b.score * scoreWeight + (1 - (Date.now() - b.createdAt) / (30 * 24 * 60 * 60 * 1e3)) * recencyWeight;
return bScore - aScore;
});
if (query.limit) {
results = results.slice(0, query.limit);
}
const index = context.referenceIndex;
if (!index.recentlyAccessed) {
index.recentlyAccessed = [];
}
if (results.length > 0) {
const frameIds = results.map((r) => r.frameId);
index.recentlyAccessed = [
...frameIds,
...index.recentlyAccessed.filter((id) => !frameIds.includes(id))
].slice(0, 100);
await this.saveProjectContext(context);
}
return results;
}
/**
* Get relevant patterns from shared context
*/
async getPatterns(type) {
const context = await this.getSharedContext();
if (type) {
return context.globalPatterns.filter((p) => p.type === type);
}
return context.globalPatterns;
}
/**
* Add a decision to the shared context
*/
async addDecision(decision) {
const session = sessionManager.getCurrentSession();
if (!session) return;
const context = await this.getSharedContext();
const newDecision = {
id: uuidv4(),
timestamp: Date.now(),
sessionId: session.sessionId,
outcome: "pending",
...decision
};
context.decisionLog.push(newDecision);
if (context.decisionLog.length > 100) {
context.decisionLog = context.decisionLog.slice(-100);
}
await this.saveProjectContext(context);
}
/**
* Get recent decisions from shared context
*/
async getDecisions(limit = 10) {
const context = await this.getSharedContext();
return context.decisionLog.slice(-limit);
}
/**
* Automatic context discovery on CLI startup
*/
async autoDiscoverContext() {
const context = await this.getSharedContext({
includeOtherBranches: false
});
const recentPatterns = context.globalPatterns.filter((p) => Date.now() - p.lastSeen < 7 * 24 * 60 * 60 * 1e3).sort((a, b) => b.frequency - a.frequency).slice(0, 5);
const lastDecisions = context.decisionLog.slice(-5);
const suggestedFrames = await this.querySharedContext({
minScore: 0.8,
limit: 5
});
return {
hasSharedContext: context.sessions.length > 0,
sessionCount: context.sessions.length,
recentPatterns,
lastDecisions,
suggestedFrames
};
}
async loadProjectContext(projectId, branch) {
const contextFile = path.join(
this.contextDir,
"projects",
`${projectId}_${branch || "main"}.json`
);
try {
const data = await fs.readFile(contextFile, "utf-8");
const context = JSON.parse(data);
context.referenceIndex.byTag = new Map(
Object.entries(context.referenceIndex.byTag || {})
);
context.referenceIndex.byType = new Map(
Object.entries(context.referenceIndex.byType || {})
);
return context;
} catch {
return {
projectId,
branch,
lastUpdated: Date.now(),
sessions: [],
globalPatterns: [],
decisionLog: [],
referenceIndex: {
byTag: /* @__PURE__ */ new Map(),
byType: /* @__PURE__ */ new Map(),
byScore: [],
recentlyAccessed: []
}
};
}
}
async saveProjectContext(context) {
const contextFile = path.join(
this.contextDir,
"projects",
`${context.projectId}_${context.branch || "main"}.json`
);
const serializable = {
...context,
lastUpdated: Date.now(),
referenceIndex: {
...context.referenceIndex,
byTag: Object.fromEntries(context.referenceIndex.byTag),
byType: Object.fromEntries(context.referenceIndex.byType)
}
};
await fs.writeFile(contextFile, JSON.stringify(serializable, null, 2));
}
async loadOtherBranchContexts(projectId, currentBranch) {
const projectsDir = path.join(this.contextDir, "projects");
const files = await fs.readdir(projectsDir);
const sessions = [];
for (const file of files) {
if (file.startsWith(`${projectId}_`) && !file.includes(currentBranch || "main")) {
try {
const data = await fs.readFile(path.join(projectsDir, file), "utf-8");
const context = JSON.parse(data);
sessions.push(...context.sessions);
} catch {
}
}
}
return sessions;
}
calculateFrameScore(frame) {
let score = 0.5;
if (frame.type === "task" || frame.type === "review") score += 0.2;
if (frame.type === "debug" || frame.type === "write") score += 0.15;
if (frame.type === "error") score += 0.15;
const frameWithData = frame;
if (frameWithData.data) score += 0.2;
if (frame.outputs && Object.keys(frame.outputs).length > 0) score += 0.2;
if (frame.digest_text || frame.digest_json && Object.keys(frame.digest_json).length > 0)
score += 0.1;
if (frame.created_at) {
const age = Date.now() - frame.created_at;
const daysSinceCreation = age / (24 * 60 * 60 * 1e3);
score *= Math.max(0.3, 1 - daysSinceCreation / 30);
}
return Math.min(1, score);
}
summarizeFrame(frame) {
return {
frameId: frame.frame_id,
title: frame.name,
type: frame.type,
score: this.calculateFrameScore(frame),
tags: [],
summary: this.generateFrameSummary(frame),
createdAt: frame.created_at
};
}
generateFrameSummary(frame) {
const parts = [];
const frameWithData = frame;
if (frame.type) parts.push(`[${frame.type}]`);
if (frame.name) parts.push(frame.name);
if (frameWithData.title) parts.push(frameWithData.title);
if (frameWithData.data?.error)
parts.push(`Error: ${frameWithData.data.error}`);
if (frameWithData.data?.resolution)
parts.push(`Resolution: ${frameWithData.data.resolution}`);
return parts.join(" - ").slice(0, 200);
}
generateSessionSummary(frames) {
const types = [...new Set(frames.map((f) => f.type))];
return `Session with ${frames.length} key frames: ${types.join(", ")}`;
}
updatePatterns(context, frames) {
for (const frame of frames) {
const frameWithData = frame;
if (frameWithData.data?.error) {
this.addPattern(
context,
frameWithData.data.error,
"error",
frameWithData.data?.resolution
);
} else if (frame.type === "error" && frame.name) {
const errorText = frame.outputs?.error || frame.name;
const resolution = frame.outputs?.resolution;
if (errorText) {
this.addPattern(context, errorText, "error", resolution);
}
}
if (frame.type === "decision" && frameWithData.data?.decision) {
this.addPattern(context, frameWithData.data.decision, "decision");
} else if (frame.digest_json?.decision) {
this.addPattern(context, frame.digest_json.decision, "decision");
}
}
}
addPattern(context, pattern, type, resolution) {
const existing = context.globalPatterns.find(
(p) => p.pattern === pattern && p.type === type
);
if (existing) {
existing.frequency++;
existing.lastSeen = Date.now();
if (resolution) existing.resolution = resolution;
} else {
context.globalPatterns.push({
pattern,
type,
frequency: 1,
lastSeen: Date.now(),
resolution
});
}
if (context.globalPatterns.length > 100) {
context.globalPatterns.sort((a, b) => b.frequency - a.frequency);
context.globalPatterns = context.globalPatterns.slice(0, 100);
}
}
updateReferenceIndex(context, frames) {
for (const frame of frames) {
const summary = this.summarizeFrame(frame);
for (const tag of summary.tags) {
if (!context.referenceIndex.byTag.has(tag)) {
context.referenceIndex.byTag.set(tag, []);
}
context.referenceIndex.byTag.get(tag).push(frame.frameId);
}
if (!context.referenceIndex.byType.has(frame.type)) {
context.referenceIndex.byType.set(frame.type, []);
}
context.referenceIndex.byType.get(frame.type).push(frame.frameId);
const scoreIndex = context.referenceIndex.byScore;
const insertIndex = scoreIndex.findIndex((id) => {
const otherFrame = context.sessions.flatMap((s) => s.keyFrames).find((f) => f.frameId === id);
return otherFrame && otherFrame.score < summary.score;
});
if (insertIndex >= 0) {
scoreIndex.splice(insertIndex, 0, frame.frameId);
} else {
scoreIndex.push(frame.frameId);
}
context.referenceIndex.byScore = scoreIndex.slice(0, 1e3);
}
}
cleanCache() {
if (Date.now() - this.lastCacheClean < 6e4) return;
if (this.cache.size > this.MAX_CACHE_SIZE) {
const entries = Array.from(this.cache.entries()).sort(
(a, b) => b[1].lastUpdated - a[1].lastUpdated
);
this.cache = new Map(entries.slice(0, this.MAX_CACHE_SIZE / 2));
}
this.lastCacheClean = Date.now();
}
}
const sharedContextLayer = SharedContextLayer.getInstance();
class ContextBridge {
static instance;
frameManager = null;
syncTimer = null;
lastSyncTime = 0;
options = {
autoSync: true,
syncInterval: 6e4,
// 1 minute
minFrameScore: 0.5,
// Include frames with score above 0.5
importantTags: ["decision", "error", "milestone", "learning"]
};
constructor() {
}
static getInstance() {
if (!ContextBridge.instance) {
ContextBridge.instance = new ContextBridge();
}
return ContextBridge.instance;
}
/**
* Initialize the bridge with a frame manager
*/
async initialize(frameManager, options) {
this.frameManager = frameManager;
this.options = { ...this.options, ...options };
await this.loadSharedContext();
if (this.options.autoSync) {
this.startAutoSync();
}
logger.info("Context bridge initialized", {
autoSync: this.options.autoSync,
syncInterval: this.options.syncInterval
});
}
/**
* Load relevant shared context into current session
*/
async loadSharedContext() {
try {
const session = sessionManager.getCurrentSession();
if (!session) return;
const discovery = await sharedContextLayer.autoDiscoverContext();
if (!discovery.hasSharedContext) {
logger.info("No shared context available to load");
return;
}
if (discovery.recentPatterns.length > 0) {
logger.info("Loaded recent patterns from shared context", {
patternCount: discovery.recentPatterns.length
});
}
if (discovery.lastDecisions.length > 0) {
logger.info("Loaded recent decisions from shared context", {
decisionCount: discovery.lastDecisions.length
});
}
if (discovery.suggestedFrames.length > 0) {
const metadata = {
suggestedFrames: discovery.suggestedFrames,
loadedAt: Date.now()
};
if (this.frameManager) {
await this.frameManager.addContext(
"shared-context-suggestions",
metadata
);
}
logger.info("Loaded suggested frames from shared context", {
frameCount: discovery.suggestedFrames.length
});
}
} catch (error) {
logger.error("Failed to load shared context", error);
}
}
/**
* Sync current session's important frames to shared context
*/
async syncToSharedContext() {
try {
if (!this.frameManager) return;
const session = sessionManager.getCurrentSession();
if (!session) return;
const activeFrames = this.frameManager.getActiveFramePath().filter(Boolean);
const recentFrames = await this.frameManager.getRecentFrames(100);
const allFrames = [...activeFrames, ...recentFrames].filter(Boolean);
const importantFrames = this.filterImportantFrames(allFrames);
if (importantFrames.length === 0) {
logger.debug("No important frames to sync");
return;
}
await sharedContextLayer.addToSharedContext(importantFrames, {
minScore: this.options.minFrameScore,
tags: this.options.importantTags
});
this.lastSyncTime = Date.now();
logger.info("Synced frames to shared context", {
frameCount: importantFrames.length,
sessionId: session.sessionId
});
} catch (error) {
logger.error("Failed to sync to shared context", error);
}
}
/**
* Query shared context for relevant frames
*/
async querySharedFrames(query) {
try {
const results = await sharedContextLayer.querySharedContext({
...query,
minScore: this.options.minFrameScore
});
logger.info("Queried shared context", {
query,
resultCount: results.length
});
return results;
} catch (error) {
logger.error("Failed to query shared context", error);
return [];
}
}
/**
* Add a decision to shared context
*/
async addDecision(decision, reasoning) {
try {
await sharedContextLayer.addDecision({
decision,
reasoning,
outcome: "pending"
});
logger.info("Added decision to shared context", { decision });
} catch (error) {
logger.error("Failed to add decision", error);
}
}
/**
* Start automatic synchronization
*/
startAutoSync() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
}
this.syncTimer = setInterval(() => {
this.syncToSharedContext().catch((error) => {
logger.error("Auto-sync failed", error);
});
}, this.options.syncInterval);
this.setupEventListeners();
}
/**
* Stop automatic synchronization
*/
stopAutoSync() {
if (this.syncTimer) {
clearInterval(this.syncTimer);
this.syncTimer = null;
}
}
/**
* Filter frames that are important enough to share
*/
filterImportantFrames(frames) {
return frames.filter((frame) => {
const hasImportantTag = this.options.importantTags.some(
(tag) => frame.metadata?.tags?.includes(tag)
);
const isImportantType = [
"task",
"milestone",
"error",
"resolution",
"decision"
].includes(frame.type);
const markedImportant = frame.metadata?.importance === "high";
return hasImportantTag || isImportantType || markedImportant;
});
}
/**
* Setup event listeners for automatic syncing
*/
setupEventListeners() {
if (!this.frameManager) return;
const originalClose = this.frameManager.closeFrame.bind(this.frameManager);
this.frameManager.closeFrame = async (frameId, metadata) => {
const result = await originalClose(frameId, metadata);
const frame = await this.frameManager.getFrame(frameId);
if (frame && this.filterImportantFrames([frame]).length > 0) {
await this.syncToSharedContext();
}
return result;
};
const originalMilestone = this.frameManager.createFrame.bind(
this.frameManager
);
this.frameManager.createFrame = async (params) => {
const result = await originalMilestone(params);
if (params.type === "milestone") {
await this.syncToSharedContext();
}
return result;
};
}
/**
* Get sync statistics
*/
getSyncStats() {
return {
lastSyncTime: this.lastSyncTime,
autoSyncEnabled: this.options.autoSync,
syncInterval: this.options.syncInterval
};
}
/**
* Manual trigger for immediate sync
*/
async forceSyncNow() {
logger.info("Force sync triggered");
await this.syncToSharedContext();
}
}
const contextBridge = ContextBridge.getInstance();
export {
ContextBridge,
SharedContextLayer,
contextBridge,
sharedContextLayer
};