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