Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 54 additions & 12 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { createReflectionEventId } from "./src/reflection-event-store.js";
import { buildReflectionMappedMetadata } from "./src/reflection-mapped-metadata.js";
import { createMemoryCLI } from "./cli.js";
import { isNoise } from "./src/noise-filter.js";
import { normalizeAutoCaptureText } from "./src/auto-capture-cleanup.js";
import { normalizeAutoCaptureText, buildConversationTurnsForExtraction, trimTurnsToUserCap, dedupePairWindow, } from "./src/auto-capture-cleanup.js";
// Import smart extraction & lifecycle components
import { SmartExtractor, createExtractionRateLimiter } from "./src/smart-extractor.js";
import { compressTexts, estimateConversationValue } from "./src/session-compressor.js";
Expand Down Expand Up @@ -1845,6 +1845,7 @@ function _initPluginState(api) {
noiseBank.init(embedder).catch((err) => api.logger.debug(`memory-lancedb-pro: noise bank init: ${String(err)}`));
const admissionRejectionAuditWriter = createAdmissionRejectionAuditWriter(config, resolvedDbPath, api);
smartExtractor = new SmartExtractor(store, embedder, llmClient, {
captureAssistantEligible: config.captureAssistant === true,
user: "User",
extractMinMessages: config.extractMinMessages ?? 4,
extractMaxChars: config.extractMaxChars ?? 8000,
Expand Down Expand Up @@ -1885,6 +1886,7 @@ function _initPluginState(api) {
const autoCaptureSeenTextCount = new Map();
const autoCapturePendingIngressTexts = new Map();
const autoCaptureRecentTexts = new Map();
const autoCaptureRecentPairTurns = new Map();
return {
config,
resolvedDbPath,
Expand Down Expand Up @@ -1912,6 +1914,7 @@ function _initPluginState(api) {
autoCaptureSeenTextCount,
autoCapturePendingIngressTexts,
autoCaptureRecentTexts,
autoCaptureRecentPairTurns,
};
}
export function isAgentOrSessionExcluded(agentId, sessionKey, patterns) {
Expand Down Expand Up @@ -2020,7 +2023,7 @@ const memoryLanceDBProPlugin = {
_registeredApisMap.delete(api); // dual-track rollback: Map un-claim
throw err;
}
const { config, resolvedDbPath, vectorDim, store, embedder, retriever, canonicalCorpusIndexer, dreamingEngine, dreamingScheduler, scopeManager, migrator, smartExtractor, mdMirror, decayEngine, tierManager, extractionRateLimiter, reflectionErrorStateBySession, reflectionDerivedBySession, reflectionDerivedSuppressionBySession, reflectionByAgentCache, reflectionByAgentCacheGeneration, recallHistory, turnCounter, autoCaptureSeenTextCount, autoCapturePendingIngressTexts, autoCaptureRecentTexts, } = singleton;
const { config, resolvedDbPath, vectorDim, store, embedder, retriever, canonicalCorpusIndexer, dreamingEngine, dreamingScheduler, scopeManager, migrator, smartExtractor, mdMirror, decayEngine, tierManager, extractionRateLimiter, reflectionErrorStateBySession, reflectionDerivedBySession, reflectionDerivedSuppressionBySession, reflectionByAgentCache, reflectionByAgentCacheGeneration, recallHistory, turnCounter, autoCaptureSeenTextCount, autoCapturePendingIngressTexts, autoCaptureRecentTexts, autoCaptureRecentPairTurns, } = singleton;
warnForDisabledChannelPlugin(api.config, api.logger);
async function sleep(ms, signal) {
if (signal?.aborted) {
Expand Down Expand Up @@ -2900,29 +2903,36 @@ const memoryLanceDBProPlugin = {
? config.scopes?.default ?? "global"
: scopeManager.getDefaultScope(agentId);
const sessionKey = ctx?.sessionKey || event.sessionKey || "unknown";
api.logger.debug(`memory-lancedb-pro: auto-capture agent_end payload for agent ${agentId} (sessionKey=${sessionKey}, captureAssistant=${config.captureAssistant === true}, ${summarizeAgentEndMessages(event.messages)})`);
api.logger.debug(`memory-lancedb-pro: auto-capture agent_end payload for agent ${agentId} (sessionKey=${sessionKey}, captureAssistant=${JSON.stringify(config.captureAssistant)}, ${summarizeAgentEndMessages(event.messages)})`);
// Extract text content from messages
const eligibleTexts = [];
const assistantContextTexts = [];
const conversationTurns = [];
let skippedAutoCaptureTexts = 0;
const captureAssistantValue = config.captureAssistant;
const captureAssistantEligible = captureAssistantValue === true;
const captureAssistantAsContext = captureAssistantValue === "context";
for (const msg of event.messages) {
if (!msg || typeof msg !== "object") {
continue;
}
const msgObj = msg;
const role = msgObj.role;
const captureAssistant = config.captureAssistant === true;
if (role !== "user" &&
!(captureAssistant && role === "assistant")) {
const isEligibleRole = role === "user" || (captureAssistantEligible && role === "assistant");
const isContextOnlyRole = captureAssistantAsContext && role === "assistant";
if (!isEligibleRole && !isContextOnlyRole) {
continue;
}
const targetTexts = isEligibleRole ? eligibleTexts : assistantContextTexts;
const content = msgObj.content;
if (typeof content === "string") {
const normalized = normalizeAutoCaptureText(role, content, shouldSkipReflectionMessage);
if (!normalized) {
skippedAutoCaptureTexts++;
}
else {
eligibleTexts.push(normalized);
targetTexts.push(normalized);
conversationTurns.push({ role: role, text: normalized });
}
continue;
}
Expand All @@ -2940,7 +2950,8 @@ const memoryLanceDBProPlugin = {
skippedAutoCaptureTexts++;
}
else {
eligibleTexts.push(normalized);
targetTexts.push(normalized);
conversationTurns.push({ role: role, text: normalized });
}
}
}
Expand Down Expand Up @@ -2978,6 +2989,24 @@ const memoryLanceDBProPlugin = {
pruneMapIfOver(autoCaptureRecentTexts, AUTO_CAPTURE_MAP_MAX_ENTRIES);
}
const minMessages = config.extractMinMessages ?? 4;
// Rolling PAIR window (operator spec: extractMinMessages counts
// user<->assistant pairs, and captureAssistant context rides the
// same window). This call's new pairs -- kept user turns with the
// assistant replies interleaved in true order -- extend what
// earlier calls buffered, bounded to extractMinMessages user turns
// (or this call's own new-user count when larger, so unextracted
// user turns are never trimmed out of their own transcript).
const thisCallPairTurns = buildConversationTurnsForExtraction({
messageLoopTurns: conversationTurns,
eligibleTexts,
newUserTexts: newTexts,
});
const priorPairTurns = autoCaptureRecentPairTurns.get(sessionKey) || [];
const pairWindowTurns = trimTurnsToUserCap(dedupePairWindow([...priorPairTurns, ...thisCallPairTurns]), Math.max(minMessages, thisCallPairTurns.filter((turn) => turn.role === "user").length));
if (thisCallPairTurns.length > 0) {
autoCaptureRecentPairTurns.set(sessionKey, pairWindowTurns);
pruneMapIfOver(autoCaptureRecentPairTurns, AUTO_CAPTURE_MAP_MAX_ENTRIES);
}
if (skippedAutoCaptureTexts > 0) {
api.logger.debug(`memory-lancedb-pro: auto-capture skipped ${skippedAutoCaptureTexts} injected/system text block(s) for agent ${agentId}`);
}
Expand Down Expand Up @@ -3033,10 +3062,17 @@ const memoryLanceDBProPlugin = {
if (cumulativeCount >= minMessages) {
api.logger.debug(`memory-lancedb-pro: auto-capture running smart extraction for agent ${agentId} (cumulative=${cumulativeCount} >= minMessages=${minMessages}, cleanTexts=${cleanTexts.length})`);
const conversationText = cleanTexts.join("\n");
// The pair window is the transcript; user turns the noise
// filter dropped stay out of it so they cannot become sources.
const noiseDroppedTexts = new Set(texts.filter((text) => !cleanTexts.includes(text)));
const finalConversationTurns = pairWindowTurns.filter((turn) => !(turn.role === "user" && noiseDroppedTexts.has(turn.text)));
const assistantWindowTexts = finalConversationTurns
.filter((turn) => turn.role === "assistant")
.map((turn) => turn.text);
// issue #417 Fix #10: prevent hook crash on LLM API errors / network timeouts
let stats = null;
try {
stats = await smartExtractor.extractAndPersist(conversationText, sessionKey, { scope: defaultScope, scopeFilter: accessibleScopes, agentId });
stats = await smartExtractor.extractAndPersist(conversationText, sessionKey, { scope: defaultScope, scopeFilter: accessibleScopes, agentId, assistantContextTexts: assistantWindowTexts, conversationTurns: finalConversationTurns });
}
catch (err) {
api.logger.error(`memory-lancedb-pro: smart-extract failed for agent ${agentId}: ${String(err)}`);
Expand All @@ -3056,6 +3092,7 @@ const memoryLanceDBProPlugin = {
// consumed history length there instead, so the next turn
// only sees the delta.
autoCaptureSeenTextCount.set(sessionKey, pendingIngressTexts.length > 0 ? 0 : eligibleTexts.length);
autoCaptureRecentPairTurns.delete(sessionKey);
return; // Smart extraction handled everything
}
if ((stats.boundarySkipped ?? 0) === 0) {
Expand Down Expand Up @@ -4066,10 +4103,15 @@ const memoryLanceDBProPlugin = {
const now = new Date(params.timestampMs ?? Date.now());
const dateStr = now.toISOString().split("T")[0];
const timeStr = now.toISOString().split("T")[1].split(".")[0];
// Session key/id stay out of `text`: it is the FTS index surface, and
// the `simple` tokenizer splits a key like
// `agent:main:cron:<uuid>:run:<uuid>` on its punctuation — so every session
// summary ends up indexed under `agent`, `main`, `cron`, `run`. A query
// mentioning any of those then BM25-matches every session summary in the
// store regardless of content. Both ids are already recorded structurally
// in metadata below, so provenance is unaffected.
const memoryText = [
`Session: ${dateStr} ${timeStr} UTC`,
`Session Key: ${params.sessionKey}`,
`Session ID: ${params.sessionId}`,
`Source: ${params.source}`,
"",
"Conversation Summary:",
Expand Down Expand Up @@ -4685,7 +4727,7 @@ export function parsePluginConfig(value) {
}
return s;
})(),
captureAssistant: cfg.captureAssistant === true,
captureAssistant: cfg.captureAssistant === "context" ? "context" : cfg.captureAssistant === true,
retrieval: typeof cfg.retrieval === "object" && cfg.retrieval !== null
? (() => {
const retrieval = { ...cfg.retrieval };
Expand Down
Loading