@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
94 lines (91 loc) • 3.08 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 { frameLifecycleHooks } from "./frame-lifecycle-hooks.js";
const ENRICHMENT_PROMPT = `You are a context enrichment engine. Given a frame digest and its parent context, do two things:
1. Rewrite the digest to be fully self-contained \u2014 resolve all pronouns ("it", "that", "the project") using parent context.
2. Extract entity-relation-value triples from the content.
Respond in JSON only:
{"enrichedDigest":"...","entities":[{"name":"...","relation":"...","value":"...","context":"..."}]}`;
function buildEnrichmentInput(frame, parents) {
const parentCtx = parents.map((p) => `[${p.name}]: ${p.digest_text ?? ""}`).join("\n");
return `Parent context:
${parentCtx}
Current frame "${frame.name}":
${frame.digest_text ?? ""}`;
}
async function enrichFrame(frame, parents, apiKey) {
try {
const Anthropic = (await import("@anthropic-ai/sdk")).default;
const client = new Anthropic({ apiKey });
const input = buildEnrichmentInput(frame, parents);
const response = await client.messages.create({
model: "claude-3-5-haiku-latest",
max_tokens: 1024,
system: ENRICHMENT_PROMPT,
messages: [{ role: "user", content: input }]
});
const text = response.content[0].type === "text" ? response.content[0].text : "";
return JSON.parse(text);
} catch (err) {
logger.warn("Frame enrichment LLM call failed", {
error: err instanceof Error ? err.message : String(err),
frameId: frame.frame_id
});
return void 0;
}
}
let unregister = null;
function registerEnrichmentHook(config, deps) {
if (unregister) unregister();
if (!config.enabled) return () => {
};
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey) {
logger.warn("Enrichment enabled but ANTHROPIC_API_KEY not set");
return () => {
};
}
unregister = frameLifecycleHooks.onFrameClosed(
"frame-enrichment",
async (data) => {
if (!data.frame.digest_text) return;
const parents = await deps.getParentFrames(
data.frame.frame_id,
config.lookbackDepth
);
const result = await enrichFrame(data.frame, parents, apiKey);
if (!result) return;
await deps.updateDigest(data.frame.frame_id, result.enrichedDigest);
if (config.extractEntities) {
for (const entity of result.entities) {
try {
deps.recordEntity(
data.frame.project_id,
entity.name,
entity.relation,
entity.value,
entity.context,
data.frame.frame_id
);
} catch {
}
}
}
},
-10
// low priority — runs after other hooks
);
return () => {
if (unregister) {
unregister();
unregister = null;
}
};
}
export {
enrichFrame,
registerEnrichmentHook
};