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.

755 lines (754 loc) 22.3 kB
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 { logger } from "../monitoring/logger.js"; import { trace } from "../trace/index.js"; import { FrameError, SystemError, ErrorCode } from "../errors/index.js"; import { FrameQueryMode } from "../session/index.js"; import { frameLifecycleHooks } from "./frame-lifecycle-hooks.js"; const MAX_FRAME_DEPTH = 100; const DEFAULT_MAX_DEPTH = 100; import { FrameDatabase } from "./frame-database.js"; import { FrameStack } from "./frame-stack.js"; import { FrameDigestGenerator } from "./frame-digest.js"; import { FrameRecovery } from "./frame-recovery.js"; class RefactoredFrameManager { frameDb; frameStack; digestGenerator; frameRecovery; db; currentRunId; sessionId; projectId; queryMode = FrameQueryMode.PROJECT_ACTIVE; config; maxFrameDepth = DEFAULT_MAX_DEPTH; lastRecoveryReport = null; constructor(db, projectId, config) { this.projectId = projectId; this.db = db; this.config = { projectId, runId: config?.runId || uuidv4(), sessionId: config?.sessionId || uuidv4(), maxStackDepth: config?.maxStackDepth || 50 }; this.maxFrameDepth = config?.maxStackDepth || DEFAULT_MAX_DEPTH; this.currentRunId = this.config.runId; this.sessionId = this.config.sessionId; this.frameDb = new FrameDatabase(db); this.frameStack = new FrameStack( this.frameDb, projectId, this.currentRunId ); this.digestGenerator = new FrameDigestGenerator(this.frameDb); this.frameRecovery = new FrameRecovery(db); this.frameRecovery.setCurrentRunId(this.currentRunId); this.frameDb.initSchema(); logger.info("RefactoredFrameManager initialized", { projectId: this.projectId, runId: this.currentRunId, sessionId: this.sessionId }); } /** * Initialize the frame manager */ async initialize() { try { this.lastRecoveryReport = await this.frameRecovery.recoverOnStartup(); if (!this.lastRecoveryReport.recovered) { logger.warn("Recovery completed with issues", { errors: this.lastRecoveryReport.errors, orphansFound: this.lastRecoveryReport.orphanedFrames.detected, integrityPassed: this.lastRecoveryReport.integrityCheck.passed }); } await this.frameStack.initialize(); logger.info("Frame manager initialization completed", { stackDepth: this.frameStack.getDepth(), recoveryRan: true, orphansClosed: this.lastRecoveryReport.orphanedFrames.closed }); } catch (error) { throw new SystemError( "Failed to initialize frame manager", ErrorCode.SYSTEM_INIT_FAILED, { projectId: this.projectId }, error instanceof Error ? error : void 0 ); } } createFrame(typeOrOptions, name, inputs, parentFrameId) { return trace.traceSync( "function", "FrameManager.createFrame", { typeOrOptions, name }, () => this._createFrame(typeOrOptions, name, inputs, parentFrameId) ); } _createFrame(typeOrOptions, name, inputs, parentFrameId) { let frameOptions; if (typeof typeOrOptions === "string") { frameOptions = { type: typeOrOptions, name, inputs: inputs || {}, parentFrameId }; } else { frameOptions = typeOrOptions; } if (!frameOptions.name || frameOptions.name.trim().length === 0) { throw new FrameError( "Frame name is required", ErrorCode.FRAME_INVALID_INPUT, { frameOptions } ); } if (this.frameStack.getDepth() >= this.config.maxStackDepth) { throw new FrameError( `Maximum stack depth reached: ${this.config.maxStackDepth}`, ErrorCode.FRAME_STACK_OVERFLOW, { currentDepth: this.frameStack.getDepth() } ); } const resolvedParentId = frameOptions.parentFrameId || this.frameStack.getCurrentFrameId(); let depth = 0; if (resolvedParentId) { const parentFrame = this.frameDb.getFrame(resolvedParentId); depth = parentFrame ? parentFrame.depth + 1 : 0; } if (depth > this.maxFrameDepth) { throw new FrameError( `Maximum frame depth exceeded: ${depth} > ${this.maxFrameDepth}`, ErrorCode.FRAME_STACK_OVERFLOW, { currentDepth: depth, maxDepth: this.maxFrameDepth, frameName: frameOptions.name, parentFrameId: resolvedParentId } ); } if (resolvedParentId) { const cycle = this.detectCycle(uuidv4(), resolvedParentId); if (cycle) { throw new FrameError( `Circular reference detected in frame hierarchy`, ErrorCode.FRAME_CYCLE_DETECTED, { parentFrameId: resolvedParentId, cycle, frameName: frameOptions.name } ); } } const frameId = uuidv4(); const frame = { frame_id: frameId, run_id: this.currentRunId, project_id: this.projectId, parent_frame_id: resolvedParentId, depth, type: frameOptions.type, name: frameOptions.name, state: "active", inputs: frameOptions.inputs || {}, outputs: {}, digest_json: {} }; const createdFrame = this.frameDb.insertFrame(frame); this.frameStack.pushFrame(frameId); logger.info("Created frame", { frameId, name: frameOptions.name, type: frameOptions.type, parentFrameId: resolvedParentId, stackDepth: this.frameStack.getDepth() }); return frameId; } /** * Close a frame and generate digest */ closeFrame(frameId, outputs) { trace.traceSync( "function", "FrameManager.closeFrame", { frameId, outputs }, () => this._closeFrame(frameId, outputs) ); } _closeFrame(frameId, outputs) { const targetFrameId = frameId || this.frameStack.getCurrentFrameId(); if (!targetFrameId) { throw new FrameError( "No active frame to close", ErrorCode.FRAME_INVALID_STATE, { operation: "closeFrame", stackDepth: this.frameStack.getDepth() } ); } const frame = this.frameDb.getFrame(targetFrameId); if (!frame) { throw new FrameError( `Frame not found: ${targetFrameId}`, ErrorCode.FRAME_NOT_FOUND, { frameId: targetFrameId, operation: "closeFrame", runId: this.currentRunId } ); } if (frame.state === "closed") { logger.warn("Attempted to close already closed frame", { frameId: targetFrameId }); return; } const digest = this.digestGenerator.generateDigest(targetFrameId); const finalOutputs = { ...outputs, ...digest.structured }; this.frameDb.updateFrame(targetFrameId, { state: "closed", outputs: finalOutputs, digest_text: digest.text, digest_json: digest.structured, closed_at: Math.floor(Date.now() / 1e3) }); this.frameStack.popFrame(targetFrameId); this.closeChildFrames(targetFrameId); const events = this.frameDb.getFrameEvents(targetFrameId); const anchors = this.frameDb.getFrameAnchors(targetFrameId); frameLifecycleHooks.triggerClose({ frame: { ...frame, state: "closed" }, events, anchors }).catch(() => { }); logger.info("Closed frame", { frameId: targetFrameId, name: frame.name, duration: Math.floor(Date.now() / 1e3) - frame.created_at, digestLength: digest.text.length, stackDepth: this.frameStack.getDepth() }); } /** * Add an event to the current frame */ addEvent(eventType, payload, frameId) { return trace.traceSync( "function", "FrameManager.addEvent", { eventType, frameId }, () => this._addEvent(eventType, payload, frameId) ); } _addEvent(eventType, payload, frameId) { const targetFrameId = frameId || this.frameStack.getCurrentFrameId(); if (!targetFrameId) { throw new FrameError( "No active frame for event", ErrorCode.FRAME_INVALID_STATE, { eventType, operation: "addEvent" } ); } const eventId = uuidv4(); const sequence = this.frameDb.getNextEventSequence(targetFrameId); const event = { event_id: eventId, frame_id: targetFrameId, run_id: this.currentRunId, seq: sequence, event_type: eventType, payload }; const createdEvent = this.frameDb.insertEvent(event); logger.debug("Added event", { eventId, frameId: targetFrameId, eventType, sequence }); return eventId; } /** * Add an anchor (important fact) to current frame */ addAnchor(type, text, priority = 5, metadata = {}, frameId) { return trace.traceSync( "function", "FrameManager.addAnchor", { type, frameId }, () => this._addAnchor(type, text, priority, metadata, frameId) ); } _addAnchor(type, text, priority, metadata, frameId) { const targetFrameId = frameId || this.frameStack.getCurrentFrameId(); if (!targetFrameId) { throw new FrameError( "No active frame for anchor", ErrorCode.FRAME_INVALID_STATE, { anchorType: type, operation: "addAnchor" } ); } const anchorId = uuidv4(); const anchor = { anchor_id: anchorId, frame_id: targetFrameId, type, text, priority, metadata }; const createdAnchor = this.frameDb.insertAnchor(anchor); logger.debug("Added anchor", { anchorId, frameId: targetFrameId, type, priority }); return anchorId; } /** * Get hot stack context */ getHotStackContext(maxEvents = 20) { return this.frameStack.getHotStackContext(maxEvents); } /** * Get active frame path (root to current) */ getActiveFramePath() { return this.frameStack.getStackFrames(); } /** * Get current frame ID */ getCurrentFrameId() { return this.frameStack.getCurrentFrameId(); } /** * Get stack depth */ getStackDepth() { return this.frameStack.getDepth(); } /** * Get frame by ID */ getFrame(frameId) { return this.frameDb.getFrame(frameId); } /** * Get frame events */ getFrameEvents(frameId, limit) { return this.frameDb.getFrameEvents(frameId, limit); } /** * Get frame anchors */ getFrameAnchors(frameId) { return this.frameDb.getFrameAnchors(frameId); } /** * Generate digest for a frame */ generateDigest(frameId) { return this.digestGenerator.generateDigest(frameId); } /** * Validate stack consistency */ validateStack() { return this.frameStack.validateStack(); } /** * Get database statistics */ getStatistics() { return this.frameDb.getStatistics(); } /** * Get the last recovery report from initialization */ getRecoveryReport() { return this.lastRecoveryReport; } /** * Manually trigger recovery (e.g., after detecting issues) */ async runRecovery() { this.lastRecoveryReport = await this.frameRecovery.recoverOnStartup(); return this.lastRecoveryReport; } /** * Validate project data integrity */ validateProjectIntegrity() { return this.frameRecovery.validateProjectIntegrity(this.projectId); } /** * Close all child frames recursively with depth limit to prevent stack overflow */ closeChildFrames(parentFrameId, depth = 0) { if (depth > this.maxFrameDepth) { logger.warn("closeChildFrames depth limit exceeded", { parentFrameId, depth, maxDepth: this.maxFrameDepth }); return; } try { const activeFrames = this.frameDb.getFramesByProject( this.projectId, "active" ); const childFrames = activeFrames.filter( (f) => f.parent_frame_id === parentFrameId ); for (const childFrame of childFrames) { if (this.frameStack.isFrameActive(childFrame.frame_id)) { this.closeChildFrames(childFrame.frame_id, depth + 1); this.closeFrameDirectly(childFrame.frame_id); } } } catch (error) { logger.warn("Failed to close child frames", { parentFrameId, error }); } } /** * Close a frame directly without triggering child frame closure * Used by closeChildFrames to avoid double recursion */ closeFrameDirectly(frameId) { const frame = this.frameDb.getFrame(frameId); if (!frame || frame.state === "closed") return; const digest = this.digestGenerator.generateDigest(frameId); this.frameDb.updateFrame(frameId, { state: "closed", outputs: digest.structured, digest_text: digest.text, digest_json: digest.structured, closed_at: Math.floor(Date.now() / 1e3) }); this.frameStack.popFrame(frameId); logger.debug("Closed child frame directly", { frameId }); } /** * Extract active artifacts from frame events */ getActiveArtifacts(frameId) { const events = this.frameDb.getFrameEvents(frameId); const artifacts = []; for (const event of events) { if (event.event_type === "artifact" && event.payload.path) { artifacts.push(event.payload.path); } } return [...new Set(artifacts)]; } /** * Extract constraints from frame inputs */ extractConstraints(inputs) { const constraints = []; if (inputs.constraints && Array.isArray(inputs.constraints)) { constraints.push(...inputs.constraints); } if (inputs.requirements && Array.isArray(inputs.requirements)) { constraints.push(...inputs.requirements); } if (inputs.limitations && Array.isArray(inputs.limitations)) { constraints.push(...inputs.limitations); } return constraints; } /** * Detect if setting a parent frame would create a cycle in the frame hierarchy. * Returns the cycle path if detected, or null if no cycle. * @param childFrameId - The frame that would be the child * @param parentFrameId - The proposed parent frame * @returns Array of frame IDs forming the cycle, or null if no cycle */ detectCycle(childFrameId, parentFrameId) { const visited = /* @__PURE__ */ new Set(); const path = []; let currentId = parentFrameId; while (currentId) { if (visited.has(currentId)) { const cycleStart = path.indexOf(currentId); return path.slice(cycleStart).concat(currentId); } if (currentId === childFrameId) { return path.concat([currentId, childFrameId]); } visited.add(currentId); path.push(currentId); const frame = this.frameDb.getFrame(currentId); if (!frame) { break; } currentId = frame.parent_frame_id; if (path.length > this.maxFrameDepth) { throw new FrameError( `Frame hierarchy traversal exceeded maximum depth during cycle detection`, ErrorCode.FRAME_STACK_OVERFLOW, { depth: path.length, maxDepth: this.maxFrameDepth, path } ); } } return null; } /** * Update parent frame of an existing frame (with cycle detection) * @param frameId - The frame to update * @param newParentFrameId - The new parent frame ID (null to make it a root frame) */ updateParentFrame(frameId, newParentFrameId) { const frame = this.frameDb.getFrame(frameId); if (!frame) { throw new FrameError( `Frame not found: ${frameId}`, ErrorCode.FRAME_NOT_FOUND, { frameId } ); } if (newParentFrameId) { const newParentFrame = this.frameDb.getFrame(newParentFrameId); if (!newParentFrame) { throw new FrameError( `Parent frame not found: ${newParentFrameId}`, ErrorCode.FRAME_NOT_FOUND, { frameId, newParentFrameId } ); } const cycle = this.detectCycle(frameId, newParentFrameId); if (cycle) { throw new FrameError( `Cannot set parent: would create circular reference`, ErrorCode.FRAME_CYCLE_DETECTED, { frameId, newParentFrameId, cycle, currentParentId: frame.parent_frame_id } ); } const newDepth2 = newParentFrame.depth + 1; if (newDepth2 > this.maxFrameDepth) { throw new FrameError( `Cannot set parent: would exceed maximum frame depth`, ErrorCode.FRAME_STACK_OVERFLOW, { frameId, newParentFrameId, newDepth: newDepth2, maxDepth: this.maxFrameDepth } ); } } let newDepth = 0; if (newParentFrameId) { const newParentFrame = this.frameDb.getFrame(newParentFrameId); if (newParentFrame) { newDepth = newParentFrame.depth + 1; } } this.frameDb.updateFrame(frameId, { parent_frame_id: newParentFrameId, depth: newDepth }); logger.info("Updated parent frame", { frameId, oldParentId: frame.parent_frame_id, newParentId: newParentFrameId }); } /** * Validate the entire frame hierarchy for cycles and depth violations * @returns Validation result with any detected issues */ validateFrameHierarchy() { const errors = []; const warnings = []; const allFrames = this.frameDb.getFramesByProject(this.projectId); for (const frame of allFrames) { if (frame.depth > this.maxFrameDepth) { errors.push( `Frame ${frame.frame_id} exceeds max depth: ${frame.depth} > ${this.maxFrameDepth}` ); } if (frame.depth > this.maxFrameDepth * 0.8) { warnings.push( `Frame ${frame.frame_id} is deep in hierarchy: ${frame.depth}/${this.maxFrameDepth}` ); } } const rootFrames = allFrames.filter((f) => !f.parent_frame_id); const visited = /* @__PURE__ */ new Set(); const visiting = /* @__PURE__ */ new Set(); const checkForCycle = (frameId) => { if (visiting.has(frameId)) { errors.push(`Cycle detected involving frame ${frameId}`); return true; } if (visited.has(frameId)) { return false; } visiting.add(frameId); const children = allFrames.filter((f) => f.parent_frame_id === frameId); for (const child of children) { if (checkForCycle(child.frame_id)) { return true; } } visiting.delete(frameId); visited.add(frameId); return false; }; for (const root of rootFrames) { checkForCycle(root.frame_id); } return { isValid: errors.length === 0, errors, warnings }; } /** * Set query mode for frame retrieval */ setQueryMode(mode) { this.queryMode = mode; this.frameStack.setQueryMode(mode); } /** * Get recent frames for context sharing */ async getRecentFrames(limit = 100) { try { const frames = this.frameDb.getFramesByProject(this.projectId); return frames.sort((a, b) => (b.created_at || 0) - (a.created_at || 0)).slice(0, limit).map((frame) => ({ ...frame, // Add compatibility fields frameId: frame.frame_id, runId: frame.run_id, projectId: frame.project_id, parentFrameId: frame.parent_frame_id, title: frame.name, timestamp: frame.created_at, metadata: { tags: this.extractTagsFromFrame(frame), importance: this.calculateFrameImportance(frame) }, data: { inputs: frame.inputs, outputs: frame.outputs, digest: frame.digest_json } })); } catch (error) { logger.error("Failed to get recent frames", error); return []; } } /** * Add context metadata to the current frame */ async addContext(key, value) { const currentFrameId = this.frameStack.getCurrentFrameId(); if (!currentFrameId) return; try { const frame = this.frameDb.getFrame(currentFrameId); if (!frame) return; const metadata = frame.outputs || {}; metadata[key] = value; this.frameDb.updateFrame(currentFrameId, { outputs: metadata }); } catch (error) { logger.warn("Failed to add context to frame", { error, key }); } } /** * Delete a frame completely from the database (used in handoffs) */ deleteFrame(frameId) { try { this.frameStack.removeFrame(frameId); this.frameDb.deleteFrame(frameId); logger.debug("Deleted frame completely", { frameId }); } catch (error) { logger.error("Failed to delete frame", { frameId, error }); throw error; } } /** * Extract tags from frame for categorization */ extractTagsFromFrame(frame) { const tags = []; if (frame.type) tags.push(frame.type); if (frame.name) { const nameLower = frame.name.toLowerCase(); if (nameLower.includes("error")) tags.push("error"); if (nameLower.includes("fix")) tags.push("resolution"); if (nameLower.includes("decision")) tags.push("decision"); if (nameLower.includes("milestone")) tags.push("milestone"); } try { if (frame.digest_json && typeof frame.digest_json === "object") { const digest = frame.digest_json; if (Array.isArray(digest.tags)) { tags.push(...digest.tags); } } } catch { } return [...new Set(tags)]; } /** * Calculate frame importance for prioritization */ calculateFrameImportance(frame) { if (frame.type === "milestone" || frame.name?.includes("decision")) { return "high"; } if (frame.type === "error" || frame.type === "resolution") { return "medium"; } if (frame.closed_at && frame.created_at) { const duration = frame.closed_at - frame.created_at; if (duration > 300) return "medium"; } return "low"; } } export { RefactoredFrameManager }; //# sourceMappingURL=refactored-frame-manager.js.map