UNPKG

@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.

315 lines (314 loc) 8.67 kB
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"; import { FrameError, ErrorCode } from "../errors/index.js"; import { FrameQueryMode } from "../session/index.js"; class FrameStack { constructor(frameDb, projectId, runId) { this.frameDb = frameDb; this.projectId = projectId; this.runId = runId; } activeStack = []; queryMode = FrameQueryMode.PROJECT_ACTIVE; /** * Initialize stack by loading active frames */ async initialize() { try { const activeFrames = this.frameDb.getFramesByProject( this.projectId, "active" ); this.activeStack = this.buildStackFromFrames(activeFrames); logger.info("Frame stack initialized", { stackDepth: this.activeStack.length, projectId: this.projectId }); } catch (error) { logger.error("Failed to initialize frame stack", { error: error instanceof Error ? error.message : String(error), projectId: this.projectId, runId: this.runId }); throw new FrameError( "Failed to initialize frame stack", ErrorCode.FRAME_INIT_FAILED, { projectId: this.projectId, runId: this.runId, originalError: error instanceof Error ? error.message : String(error) } ); } } /** * Push new frame onto stack */ pushFrame(frameId) { if (this.activeStack.includes(frameId)) { logger.warn("Frame already on stack", { frameId }); return; } this.activeStack.push(frameId); logger.debug("Pushed frame to stack", { frameId, stackDepth: this.activeStack.length }); } /** * Pop frame from stack */ popFrame(frameId) { if (this.activeStack.length === 0) { return void 0; } let poppedFrameId; if (frameId) { const index = this.activeStack.indexOf(frameId); if (index === -1) { logger.warn("Frame not found on stack", { frameId }); return void 0; } const removed = this.activeStack.splice(index); poppedFrameId = removed[0]; if (removed.length > 1) { logger.info("Popped multiple frames due to stack unwinding", { targetFrame: frameId, removedFrames: removed }); } } else { poppedFrameId = this.activeStack.pop(); } if (poppedFrameId) { logger.debug("Popped frame from stack", { frameId: poppedFrameId, stackDepth: this.activeStack.length }); } return poppedFrameId; } /** * Get current (top) frame ID */ getCurrentFrameId() { return this.activeStack[this.activeStack.length - 1]; } /** * Get stack depth */ getDepth() { return this.activeStack.length; } /** * Get complete stack */ getStack() { return [...this.activeStack]; } /** * Get stack as frame objects */ getStackFrames() { return this.activeStack.map((frameId) => this.frameDb.getFrame(frameId)).filter((f) => f !== void 0); } /** * Get frame context for the hot stack */ getHotStackContext(maxEvents = 20) { return this.activeStack.map((frameId) => this.buildFrameContext(frameId, maxEvents)).filter((ctx) => ctx !== null); } /** * Check if frame is on stack */ isFrameActive(frameId) { return this.activeStack.includes(frameId); } /** * Get parent frame ID for current frame */ getParentFrameId() { if (this.activeStack.length < 2) { return void 0; } return this.activeStack[this.activeStack.length - 2]; } /** * Get frame depth on stack (0-based) */ getFrameStackDepth(frameId) { return this.activeStack.indexOf(frameId); } /** * Clear entire stack */ clear() { const previousDepth = this.activeStack.length; this.activeStack = []; logger.info("Cleared frame stack", { previousDepth }); } /** * Set query mode and reinitialize stack */ setQueryMode(mode) { this.queryMode = mode; this.initialize().catch((error) => { logger.warn("Failed to reinitialize stack with new query mode", { mode, error }); }); } /** * Remove a specific frame from the stack without popping frames above it */ removeFrame(frameId) { const index = this.activeStack.indexOf(frameId); if (index === -1) { return false; } this.activeStack.splice(index, 1); logger.debug("Removed frame from stack", { frameId, stackDepth: this.activeStack.length }); return true; } /** * Validate stack consistency */ validateStack() { const errors = []; for (const frameId of this.activeStack) { const frame = this.frameDb.getFrame(frameId); if (!frame) { errors.push(`Frame not found in database: ${frameId}`); continue; } if (frame.state !== "active") { errors.push( `Frame on stack is not active: ${frameId} (state: ${frame.state})` ); } if (frame.project_id !== this.projectId) { errors.push(`Frame belongs to different project: ${frameId}`); } } for (let i = 1; i < this.activeStack.length; i++) { const currentFrameId = this.activeStack[i]; const expectedParentId = this.activeStack[i - 1]; const currentFrame = this.frameDb.getFrame(currentFrameId); if (currentFrame?.parent_frame_id !== expectedParentId) { errors.push( `Frame parent mismatch: ${currentFrameId} parent should be ${expectedParentId} but is ${currentFrame?.parent_frame_id}` ); } } return { isValid: errors.length === 0, errors }; } /** * Build frame context for a specific frame */ buildFrameContext(frameId, maxEvents) { try { const frame = this.frameDb.getFrame(frameId); if (!frame) { logger.warn("Frame not found for context building", { frameId }); return null; } const anchors = this.frameDb.getFrameAnchors(frameId); const recentEvents = this.frameDb.getFrameEvents(frameId, maxEvents); const activeArtifacts = this.extractActiveArtifacts(recentEvents); return { frameId, header: { goal: frame.name, constraints: this.extractConstraints(frame.inputs), definitions: frame.inputs.definitions }, anchors, recentEvents, activeArtifacts }; } catch (error) { logger.warn("Failed to build frame context", { frameId, error }); return null; } } /** * Extract constraints from frame inputs */ extractConstraints(inputs) { const constraints = []; if (inputs.constraints && Array.isArray(inputs.constraints)) { constraints.push(...inputs.constraints); } return constraints; } /** * Extract active artifacts from events */ extractActiveArtifacts(events) { const artifacts = []; for (const event of events) { const payload = event.payload; if (event.event_type === "artifact" && payload?.path) { artifacts.push(payload.path); } } return [...new Set(artifacts)]; } /** * Build stack order from database frames */ buildStackFromFrames(frames) { if (frames.length === 0) { return []; } const parentMap = /* @__PURE__ */ new Map(); const frameMap = /* @__PURE__ */ new Map(); for (const frame of frames) { frameMap.set(frame.frame_id, frame); if (frame.parent_frame_id) { parentMap.set(frame.frame_id, frame.parent_frame_id); } } const rootFrames = frames.filter( (f) => !f.parent_frame_id || !frameMap.has(f.parent_frame_id) ); if (rootFrames.length === 0) { logger.warn("No root frames found in active set"); return []; } if (rootFrames.length > 1) { logger.warn("Multiple root frames found, using most recent", { rootFrames: rootFrames.map((f) => f.frame_id) }); } const stack = []; let currentFrame = rootFrames.sort( (a, b) => a.created_at - b.created_at )[0]; while (currentFrame) { stack.push(currentFrame.frame_id); const parentId = currentFrame.frame_id; const childFrame = frames.find((f) => f.parent_frame_id === parentId); if (childFrame) { currentFrame = childFrame; } else { break; } } return stack; } } export { FrameStack }; //# sourceMappingURL=frame-stack.js.map