@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
761 lines (760 loc) • 22.4 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 { 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 FrameManager {
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("FrameManager 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 as { frames } — compat shim for ClearSurvival
*/
getStack() {
const frames = this.frameDb.getFramesByProject(this.projectId);
return { frames };
}
/**
* 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 {
FrameManager
};