diff --git a/README.md b/README.md
index 51adce7..99e489b 100644
--- a/README.md
+++ b/README.md
@@ -9,19 +9,10 @@
A LanceDB-backed OpenClaw memory plugin that stores preferences, decisions, and project context, then auto-recalls them in future sessions.
[](https://github.com/openclaw/openclaw)
-[](https://github.com/openclaw/openclaw)
[](https://www.npmjs.com/package/memory-lancedb-pro)
[](https://lancedb.com)
[](LICENSE)
-
-
-
- ✅ Fully adapted for OpenClaw 2026.3+ new plugin architecture
- 🔄 Uses before_prompt_build hooks (replacing deprecated before_agent_start)
- 🩺 Run openclaw doctor --fix after upgrading
-
-
[English](README.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JA.md) | [한국어](README_KO.md) | [Français](README_FR.md) | [Español](README_ES.md) | [Deutsch](README_DE.md) | [Italiano](README_IT.md) | [Русский](README_RU.md) | [Português (Brasil)](README_PT-BR.md)
@@ -129,6 +120,31 @@ Add to your `openclaw.json`:
- `extractMinMessages: 2` → extraction triggers in normal two-turn chats
- `sessionMemory.enabled: false` → avoids polluting retrieval with session summaries on day one
+---
+
+## ⚠️ Dual-Memory Architecture (Important)
+
+When `memory-lancedb-pro` is active, your system has **two independent memory layers** that do **not** auto-sync:
+
+| Memory Layer | Storage | What it's for | Recallable? |
+|---|---|---|---|
+| **Plugin Memory** | LanceDB (vector store) | Semantic recall via `memory_recall` / auto-recall | ✅ Yes |
+| **Markdown Memory** | `MEMORY.md`, `memory/YYYY-MM-DD.md` | Startup context, human-readable journal | ❌ Not auto-recalled |
+
+**Key principle:**
+> A fact written into `memory/YYYY-MM-DD.md` is visible in startup context, but `memory_recall` **will not find it** unless it was also written via `memory_store` (or auto-captured by the plugin).
+
+**What this means for you:**
+- Need semantic recall? → Use `memory_store` or let auto-capture do it
+- `memory/YYYY-MM-DD.md` → treat as a **daily journal / log**, not a recall source
+- `MEMORY.md` → curated human-readable reference, not a recall source
+- Plugin memory → **primary recall source** for `memory_recall` and auto-recall
+
+**If you want your Markdown memories to be recallable**, use the import command:
+```bash
+npx memory-lancedb-pro memory-pro import-markdown
+```
+
Validate & restart:
```bash
@@ -620,19 +636,6 @@ Sometimes the model may echo the injected `` block.
-
-Auto-recall timeout tuning
-
-Auto-recall has a configurable timeout (default 5s) to prevent stalling agent startup. If you're behind a proxy or using a high-latency embedding API, increase it:
-
-```json
-{ "plugins": { "entries": { "memory-lancedb-pro": { "config": { "autoRecallTimeoutMs": 8000 } } } } }
-```
-
-If auto-recall consistently times out, check your embedding API latency first. The timeout only affects the automatic injection path — manual `memory_recall` tool calls are not affected.
-
-
-
Session Memory
diff --git a/cli.ts b/cli.ts
index 9920391..dd062ad 100644
--- a/cli.ts
+++ b/cli.ts
@@ -1036,6 +1036,131 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
}
});
+ /**
+ * import-markdown: Import memories from Markdown memory files into the plugin store.
+ * Targets MEMORY.md and memory/YYYY-MM-DD.md files found in OpenClaw workspaces.
+ */
+ memory
+ .command("import-markdown [workspace-glob]")
+ .description("Import memories from Markdown files (MEMORY.md, memory/YYYY-MM-DD.md) into the plugin store")
+ .option("--dry-run", "Show what would be imported without importing")
+ .option("--scope ", "Import into specific scope (default: global)")
+ .option(
+ "--openclaw-home ",
+ "OpenClaw home directory (default: ~/.openclaw)",
+ )
+ .action(async (workspaceGlob, options) => {
+ const openclawHome = options.openclawHome
+ ? path.resolve(options.openclawHome)
+ : path.join(homedir(), ".openclaw");
+
+ const workspaceDir = path.join(openclawHome, "workspace");
+ let imported = 0;
+ let skipped = 0;
+ let foundFiles = 0;
+
+ if (!context.embedder) {
+ console.error(
+ "import-markdown requires an embedder. Use via plugin CLI or ensure embedder is configured.",
+ );
+ process.exit(1);
+ }
+
+ // Scan workspace directories
+ let workspaceEntries: string[];
+ try {
+ const fsPromises = await import("node:fs/promises");
+ workspaceEntries = await fsPromises.readdir(workspaceDir, { withFileTypes: true });
+ } catch {
+ console.error(`Failed to read workspace directory: ${workspaceDir}`);
+ process.exit(1);
+ }
+
+ // Collect all markdown files to scan
+ const mdFiles: Array<{ filePath: string; scope: string }> = [];
+
+ for (const entry of workspaceEntries) {
+ if (!entry.isDirectory()) continue;
+ if (workspaceGlob && !entry.name.includes(workspaceGlob)) continue;
+
+ const workspacePath = path.join(workspaceDir, entry.name);
+
+ // MEMORY.md
+ const memoryMd = path.join(workspacePath, "MEMORY.md");
+ try {
+ const { stat } = await import("node:fs/promises");
+ await stat(memoryMd);
+ mdFiles.push({ filePath: memoryMd, scope: entry.name });
+ } catch { /* not found */ }
+
+ // memory/ directory
+ const memoryDir = path.join(workspacePath, "memory");
+ try {
+ const { stat } = await import("node:fs/promises");
+ const stats = await stat(memoryDir);
+ if (stats.isDirectory()) {
+ const { readdir } = await import("node:fs/promises");
+ const files = await readdir(memoryDir);
+ for (const f of files) {
+ if (f.endsWith(".md") && /^\d{4}-\d{2}-\d{2}/.test(f)) {
+ mdFiles.push({ filePath: path.join(memoryDir, f), scope: entry.name });
+ }
+ }
+ }
+ } catch { /* not found */ }
+ }
+
+ if (mdFiles.length === 0) {
+ console.log("No Markdown memory files found.");
+ return;
+ }
+
+ const targetScope = options.scope || "global";
+
+ // Parse each file for memory entries (lines starting with "- ")
+ for (const { filePath, scope } of mdFiles) {
+ foundFiles++;
+ const { readFile } = await import("node:fs/promises");
+ const content = await readFile(filePath, "utf-8");
+ const lines = content.split("\n");
+
+ for (const line of lines) {
+ // Skip non-memory lines
+ if (!line.startsWith("- ")) continue;
+ const text = line.slice(2).trim();
+ if (text.length < 5) { skipped++; continue; }
+
+ if (options.dryRun) {
+ console.log(` [dry-run] would import: ${text.slice(0, 80)}...`);
+ imported++;
+ continue;
+ }
+
+ try {
+ const vector = await context.embedder!.embedQuery(text);
+ await context.store.store({
+ text,
+ vector,
+ importance: 0.7,
+ category: "other",
+ scope: targetScope,
+ metadata: { importedFrom: filePath, sourceScope: scope },
+ });
+ imported++;
+ } catch (err) {
+ console.warn(` Failed to import: ${text.slice(0, 60)}... — ${err}`);
+ skipped++;
+ }
+ }
+ }
+
+ if (options.dryRun) {
+ console.log(`\nDRY RUN — found ${foundFiles} files, ${imported} entries would be imported, ${skipped} skipped`);
+ } else {
+ console.log(`\nImport complete: ${imported} imported, ${skipped} skipped (scanned ${foundFiles} files)`);
+ }
+ });
+
// Re-embed an existing LanceDB into the current target DB (A/B testing)
memory
.command("reembed")
diff --git a/index.ts b/index.ts
index 74cd7c6..5767acd 100644
--- a/index.ts
+++ b/index.ts
@@ -24,12 +24,6 @@ import { appendSelfImprovementEntry, ensureSelfImprovementLearningFiles } from "
import type { MdMirrorWriter } from "./src/tools.js";
import { shouldSkipRetrieval } from "./src/adaptive-retrieval.js";
import { parseClawteamScopes, applyClawteamScopes } from "./src/clawteam-scope.js";
-import {
- runCompaction,
- shouldRunCompaction,
- recordCompactionRun,
- type CompactionConfig,
-} from "./src/memory-compactor.js";
import { runWithReflectionTransientRetryOnce } from "./src/reflection-retry.js";
import { resolveReflectionSessionSearchDirs, stripResetSuffix } from "./src/session-recovery.js";
import {
@@ -45,11 +39,9 @@ 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 smart extraction & lifecycle components
-import { SmartExtractor, createExtractionRateLimiter } from "./src/smart-extractor.js";
-import { compressTexts, estimateConversationValue } from "./src/session-compressor.js";
+import { SmartExtractor } from "./src/smart-extractor.js";
import { NoisePrototypeBank } from "./src/noise-prototypes.js";
import { createLlmClient } from "./src/llm-client.js";
import { createDecayEngine, DEFAULT_DECAY_CONFIG } from "./src/decay-engine.js";
@@ -72,7 +64,6 @@ import {
type AdmissionControlConfig,
type AdmissionRejectionAuditEntry,
} from "./src/admission-control.js";
-import { analyzeIntent, applyCategoryBoost } from "./src/intent-analyzer.js";
// ============================================================================
// Configuration & Types
@@ -85,7 +76,6 @@ interface PluginConfig {
model?: string;
baseURL?: string;
dimensions?: number;
- omitDimensions?: boolean;
taskQuery?: string;
taskPassage?: string;
normalized?: boolean;
@@ -96,11 +86,10 @@ interface PluginConfig {
autoRecall?: boolean;
autoRecallMinLength?: number;
autoRecallMinRepeated?: number;
- autoRecallTimeoutMs?: number;
autoRecallMaxItems?: number;
autoRecallMaxChars?: number;
autoRecallPerItemMaxChars?: number;
- recallMode?: "full" | "summary" | "adaptive" | "off";
+ autoRecallTimeoutMs?: number;
captureAssistant?: boolean;
retrieval?: {
mode?: "hybrid" | "vector";
@@ -195,22 +184,6 @@ interface PluginConfig {
mdMirror?: { enabled?: boolean; dir?: string };
workspaceBoundary?: WorkspaceBoundaryConfig;
admissionControl?: AdmissionControlConfig;
- memoryCompaction?: {
- enabled?: boolean;
- minAgeDays?: number;
- similarityThreshold?: number;
- minClusterSize?: number;
- maxMemoriesToScan?: number;
- cooldownHours?: number;
- };
- sessionCompression?: {
- enabled?: boolean;
- minScoreToKeep?: number;
- };
- extractionThrottle?: {
- skipLowValue?: boolean;
- maxExtractionsPerHour?: number;
- };
}
type ReflectionThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
@@ -727,10 +700,65 @@ function shouldSkipReflectionMessage(role: string, text: string): boolean {
return false;
}
+const AUTO_CAPTURE_INBOUND_META_SENTINELS = [
+ "Conversation info (untrusted metadata):",
+ "Sender (untrusted metadata):",
+ "Thread starter (untrusted, for context):",
+ "Replied message (untrusted, for context):",
+ "Forwarded message context (untrusted metadata):",
+ "Chat history since last reply (untrusted, for context):",
+] as const;
+
+const AUTO_CAPTURE_SESSION_RESET_PREFIX =
+ "A new session was started via /new or /reset. Execute your Session Startup sequence now";
+const AUTO_CAPTURE_ADDRESSING_PREFIX_RE = /^(?:<@!?[0-9]+>|@[A-Za-z0-9_.-]+)\s*/;
const AUTO_CAPTURE_MAP_MAX_ENTRIES = 2000;
const AUTO_CAPTURE_EXPLICIT_REMEMBER_RE =
/^(?:请|請)?(?:记住|記住|记一下|記一下|别忘了|別忘了)[。.!??!]*$/u;
+function isAutoCaptureInboundMetaSentinelLine(line: string): boolean {
+ const trimmed = line.trim();
+ return AUTO_CAPTURE_INBOUND_META_SENTINELS.some((sentinel) => sentinel === trimmed);
+}
+
+function stripLeadingInboundMetadata(text: string): string {
+ if (!text || !AUTO_CAPTURE_INBOUND_META_SENTINELS.some((sentinel) => text.includes(sentinel))) {
+ return text;
+ }
+
+ const lines = text.split("\n");
+ let index = 0;
+ while (index < lines.length && lines[index].trim() === "") {
+ index++;
+ }
+
+ while (index < lines.length && isAutoCaptureInboundMetaSentinelLine(lines[index])) {
+ index++;
+ if (index < lines.length && lines[index].trim() === "```json") {
+ index++;
+ while (index < lines.length && lines[index].trim() !== "```") {
+ index++;
+ }
+ if (index < lines.length && lines[index].trim() === "```") {
+ index++;
+ }
+ } else {
+ // Sentinel line not followed by a ```json fenced block — unexpected format.
+ // Log and return original text to avoid lossy stripping.
+ _autoCaptureDebugLog(
+ `memory-lancedb-pro: stripLeadingInboundMetadata: sentinel line not followed by json fenced block at line ${index}, returning original text`,
+ );
+ return text;
+ }
+
+ while (index < lines.length && lines[index].trim() === "") {
+ index++;
+ }
+ }
+
+ return lines.slice(index).join("\n").trim();
+}
+
/**
* Prune a Map to stay within the given maximum number of entries.
* Deletes the oldest (earliest-inserted) keys when over the limit.
@@ -745,6 +773,28 @@ function pruneMapIfOver(map: Map, maxEntries: number): void {
}
}
+function stripAutoCaptureSessionResetPrefix(text: string): string {
+ const trimmed = text.trim();
+ if (!trimmed.startsWith(AUTO_CAPTURE_SESSION_RESET_PREFIX)) {
+ return trimmed;
+ }
+
+ const blankLineIndex = trimmed.indexOf("\n\n");
+ if (blankLineIndex >= 0) {
+ return trimmed.slice(blankLineIndex + 2).trim();
+ }
+
+ const lines = trimmed.split("\n");
+ if (lines.length <= 2) {
+ return "";
+ }
+ return lines.slice(2).join("\n").trim();
+}
+
+function stripAutoCaptureAddressingPrefix(text: string): string {
+ return text.replace(AUTO_CAPTURE_ADDRESSING_PREFIX_RE, "").trim();
+}
+
function isExplicitRememberCommand(text: string): boolean {
return AUTO_CAPTURE_EXPLICIT_REMEMBER_RE.test(text.trim());
}
@@ -774,6 +824,34 @@ function buildAutoCaptureConversationKeyFromSessionKey(sessionKey: string): stri
return suffix || null;
}
+function stripAutoCaptureInjectedPrefix(role: string, text: string): string {
+ if (role !== "user") {
+ return text.trim();
+ }
+
+ let normalized = text.trim();
+ normalized = normalized.replace(/^\s*[\s\S]*?<\/relevant-memories>\s*/i, "");
+ normalized = normalized.replace(
+ /^\[UNTRUSTED DATA[^\n]*\][\s\S]*?\[END UNTRUSTED DATA\]\s*/i,
+ "",
+ );
+ normalized = stripAutoCaptureSessionResetPrefix(normalized);
+ normalized = stripLeadingInboundMetadata(normalized);
+ normalized = stripAutoCaptureAddressingPrefix(normalized);
+ return normalized.trim();
+}
+
+/** Module-level debug logger for auto-capture helpers; set during plugin registration. */
+let _autoCaptureDebugLog: (msg: string) => void = () => { };
+
+function normalizeAutoCaptureText(role: unknown, text: string): string | null {
+ if (typeof role !== "string") return null;
+ const normalized = stripAutoCaptureInjectedPrefix(role, text);
+ if (!normalized) return null;
+ if (shouldSkipReflectionMessage(role, normalized)) return null;
+ return normalized;
+}
+
function redactSecrets(text: string): string {
const patterns: RegExp[] = [
/Bearer\s+[A-Za-z0-9\-._~+/]+=*/g,
@@ -1577,6 +1655,8 @@ const pluginVersion = getPluginVersion();
// Plugin Definition
// ============================================================================
+let _initialized = false;
+
const memoryLanceDBProPlugin = {
id: "memory-lancedb-pro",
name: "Memory (LanceDB Pro)",
@@ -1585,9 +1665,25 @@ const memoryLanceDBProPlugin = {
kind: "memory" as const,
register(api: OpenClawPluginApi) {
+
+ // Idempotent guard: skip re-init on repeated register() calls
+ if (_initialized) {
+ api.logger.debug("memory-lancedb-pro: register() called again — skipping re-init (idempotent)");
+ return;
+ }
+
// Parse and validate configuration
const config = parsePluginConfig(api.pluginConfig);
+ // Dual-memory model warning: help users understand the two-layer architecture
+ api.logger.info(
+ `[memory-lancedb-pro] memory_recall queries the plugin store (LanceDB), not MEMORY.md.\n` +
+ ` - Plugin memory (LanceDB) = primary recall source for semantic search\n` +
+ ` - MEMORY.md / memory/YYYY-MM-DD.md = startup context / journal only\n` +
+ ` - Use memory_store or auto-capture for recallable memories.\n` +
+ ` - Run: npx memory-lancedb-pro memory-pro import-markdown to migrate Markdown memories.`,
+ );
+
const resolvedDbPath = api.resolvePath(config.dbPath || getDefaultDbPath());
// Pre-flight: validate storage path (symlink resolution, mkdir, write check).
@@ -1723,12 +1819,6 @@ const memoryLanceDBProPlugin = {
}
}
- // Extraction rate limiter (Feature 7: Adaptive Extraction Throttling)
- // NOTE: This rate limiter is global — shared across all agents in multi-agent setups.
- const extractionRateLimiter = createExtractionRateLimiter({
- maxExtractionsPerHour: config.extractionThrottle?.maxExtractionsPerHour,
- });
-
async function sleep(ms: number): Promise {
await new Promise(resolve => setTimeout(resolve, ms));
}
@@ -1955,6 +2045,9 @@ const memoryLanceDBProPlugin = {
const autoCapturePendingIngressTexts = new Map();
const autoCaptureRecentTexts = new Map();
+ // Wire up the module-level debug logger for pure helper functions.
+ _autoCaptureDebugLog = (msg: string) => api.logger.debug(msg);
+
api.logger.info(
`memory-lancedb-pro@${pluginVersion}: plugin registered (db: ${resolvedDbPath}, model: ${config.embedding.model || "text-embedding-3-small"}, smartExtraction: ${smartExtractor ? 'ON' : 'OFF'})`
);
@@ -1965,7 +2058,7 @@ const memoryLanceDBProPlugin = {
ctx.channelId,
ctx.conversationId,
);
- const normalized = normalizeAutoCaptureText("user", event.content, shouldSkipReflectionMessage);
+ const normalized = normalizeAutoCaptureText("user", event.content);
if (conversationKey && normalized) {
const queue = autoCapturePendingIngressTexts.get(conversationKey) || [];
queue.push(normalized);
@@ -2019,128 +2112,6 @@ const memoryLanceDBProPlugin = {
}
);
- // ========================================================================
- // Memory Compaction (Progressive Summarization)
- // ========================================================================
-
- if (config.enableManagementTools) {
- api.registerTool({
- name: "memory_compact",
- description:
- "Consolidate semantically similar old memories into refined single entries " +
- "(progressive summarization). Reduces noise and improves retrieval quality over time. " +
- "Use dry_run:true first to preview the compaction plan without making changes.",
- inputSchema: {
- type: "object" as const,
- properties: {
- dry_run: {
- type: "boolean",
- description: "Preview clusters without writing changes. Default: false.",
- },
- min_age_days: {
- type: "number",
- description: "Only compact memories at least this many days old. Default: 7.",
- },
- similarity_threshold: {
- type: "number",
- description: "Cosine similarity threshold for clustering [0-1]. Default: 0.88.",
- },
- scopes: {
- type: "array",
- items: { type: "string" },
- description: "Scope filter. Omit to compact all scopes.",
- },
- },
- required: [],
- },
- execute: async (args: Record) => {
- const compactionCfg: CompactionConfig = {
- enabled: true,
- minAgeDays:
- typeof args.min_age_days === "number"
- ? args.min_age_days
- : (config.memoryCompaction?.minAgeDays ?? 7),
- similarityThreshold:
- typeof args.similarity_threshold === "number"
- ? Math.max(0, Math.min(1, args.similarity_threshold))
- : (config.memoryCompaction?.similarityThreshold ?? 0.88),
- minClusterSize: config.memoryCompaction?.minClusterSize ?? 2,
- maxMemoriesToScan: config.memoryCompaction?.maxMemoriesToScan ?? 200,
- dryRun: args.dry_run === true,
- cooldownHours: config.memoryCompaction?.cooldownHours ?? 24,
- };
- const scopes =
- Array.isArray(args.scopes) && args.scopes.length > 0
- ? (args.scopes as string[])
- : undefined;
-
- const result = await runCompaction(
- store,
- embedder,
- compactionCfg,
- scopes,
- api.logger,
- );
-
- return {
- content: [
- {
- type: "text",
- text: JSON.stringify(
- {
- scanned: result.scanned,
- clustersFound: result.clustersFound,
- memoriesDeleted: result.memoriesDeleted,
- memoriesCreated: result.memoriesCreated,
- dryRun: result.dryRun,
- summary: result.dryRun
- ? `Dry run: found ${result.clustersFound} cluster(s) in ${result.scanned} memories — no changes made.`
- : `Compacted ${result.memoriesDeleted} memories into ${result.memoriesCreated} consolidated entries.`,
- },
- null,
- 2,
- ),
- },
- ],
- };
- },
- });
- }
-
- // Auto-compaction at gateway_start (if enabled, respects cooldown)
- if (config.memoryCompaction?.enabled) {
- api.on("gateway_start", () => {
- const compactionStateFile = join(
- dirname(resolvedDbPath),
- ".compaction-state.json",
- );
- const compactionCfg: CompactionConfig = {
- enabled: true,
- minAgeDays: config.memoryCompaction!.minAgeDays ?? 7,
- similarityThreshold: config.memoryCompaction!.similarityThreshold ?? 0.88,
- minClusterSize: config.memoryCompaction!.minClusterSize ?? 2,
- maxMemoriesToScan: config.memoryCompaction!.maxMemoriesToScan ?? 200,
- dryRun: false,
- cooldownHours: config.memoryCompaction!.cooldownHours ?? 24,
- };
-
- shouldRunCompaction(compactionStateFile, compactionCfg.cooldownHours)
- .then(async (should) => {
- if (!should) return;
- await recordCompactionRun(compactionStateFile);
- const result = await runCompaction(store, embedder, compactionCfg, undefined, api.logger);
- if (result.clustersFound > 0) {
- api.logger.info(
- `memory-compactor [auto]: compacted ${result.memoriesDeleted} → ${result.memoriesCreated} entries`,
- );
- }
- })
- .catch((err) => {
- api.logger.warn(`memory-compactor [auto]: failed: ${String(err)}`);
- });
- });
- }
-
// ========================================================================
// Register CLI Commands
// ========================================================================
@@ -2194,7 +2165,6 @@ const memoryLanceDBProPlugin = {
// Auto-recall: inject relevant memories before agent starts
// Default is OFF to prevent the model from accidentally echoing injected context.
- // recallMode: "full" (default when autoRecall=true) | "summary" (L0 only) | "adaptive" (intent-based) | "off"
const recallMode = config.recallMode || "full";
if (config.autoRecall === true && recallMode !== "off") {
// Cache the most recent raw user message per session so the
@@ -2213,7 +2183,7 @@ const memoryLanceDBProPlugin = {
if (text) lastRawUserMessage.set(cacheKey, text);
});
- const AUTO_RECALL_TIMEOUT_MS = parsePositiveInt(config.autoRecallTimeoutMs) ?? 5_000; // configurable; default raised from 3s to 5s for remote embedding APIs behind proxies
+ const AUTO_RECALL_TIMEOUT_MS = config.autoRecallTimeoutMs ?? 3_000; // bounded timeout to prevent agent startup stall
api.on("before_prompt_build", async (event: any, ctx: any) => {
// Manually increment turn counter for this session
const sessionId = ctx?.sessionId || "default";
@@ -2260,14 +2230,6 @@ const memoryLanceDBProPlugin = {
const autoRecallPerItemMaxChars = clampInt(config.autoRecallPerItemMaxChars ?? 180, 32, 1000);
const retrieveLimit = clampInt(Math.max(autoRecallMaxItems * 2, autoRecallMaxItems), 1, 20);
- // Adaptive intent analysis (zero-LLM-cost pattern matching)
- const intent = recallMode === "adaptive" ? analyzeIntent(recallQuery) : undefined;
- if (intent) {
- api.logger.debug?.(
- `memory-lancedb-pro: adaptive recall intent=${intent.label} depth=${intent.depth} confidence=${intent.confidence} categories=[${intent.categories.join(",")}]`,
- );
- }
-
const results = filterUserMdExclusiveRecallResults(await retrieveWithRetry({
query: recallQuery,
limit: retrieveLimit,
@@ -2279,19 +2241,16 @@ const memoryLanceDBProPlugin = {
return;
}
- // Apply intent-based category boost for adaptive mode
- const rankedResults = intent ? applyCategoryBoost(results, intent) : results;
-
// Filter out redundant memories based on session history
const minRepeated = config.autoRecallMinRepeated ?? 8;
let dedupFilteredCount = 0;
// Only enable dedup logic when minRepeated > 0
- let finalResults = rankedResults;
+ let finalResults = results;
if (minRepeated > 0) {
const sessionHistory = recallHistory.get(sessionId) || new Map();
- const filteredResults = rankedResults.filter((r) => {
+ const filteredResults = results.filter((r) => {
const lastTurn = sessionHistory.get(r.entry.id) ?? -999;
const diff = currentTurn - lastTurn;
const isRedundant = diff < minRepeated;
@@ -2323,10 +2282,12 @@ const memoryLanceDBProPlugin = {
const meta = parseSmartMetadata(r.entry.metadata, r.entry);
if (meta.state !== "confirmed") {
stateFilteredCount++;
+ api.logger.debug(`memory-lancedb-pro: governance: filtered id=${r.entry.id} reason=state(${meta.state}) score=${r.score?.toFixed(3)} text=${r.entry.text.slice(0, 50)}`);
return false;
}
if (meta.memory_layer === "archive" || meta.memory_layer === "reflection") {
stateFilteredCount++;
+ api.logger.debug(`memory-lancedb-pro: governance: filtered id=${r.entry.id} reason=layer(${meta.memory_layer}) score=${r.score?.toFixed(3)} text=${r.entry.text.slice(0, 50)}`);
return false;
}
if (meta.suppressed_until_turn > 0 && currentTurn <= meta.suppressed_until_turn) {
@@ -2343,30 +2304,13 @@ const memoryLanceDBProPlugin = {
return;
}
- // Determine effective per-item char limit based on recall mode and intent depth
- const effectivePerItemMaxChars = (() => {
- if (recallMode === "summary") return Math.min(autoRecallPerItemMaxChars, 80); // L0 only
- if (!intent) return autoRecallPerItemMaxChars; // "full" mode
- // Adaptive mode: depth determines char budget
- switch (intent.depth) {
- case "l0": return Math.min(autoRecallPerItemMaxChars, 80);
- case "l1": return autoRecallPerItemMaxChars; // default budget
- case "full": return Math.min(autoRecallPerItemMaxChars * 3, 1000);
- }
- })();
-
const preBudgetCandidates = governanceEligible.map((r) => {
const metaObj = parseSmartMetadata(r.entry.metadata, r.entry);
const displayCategory = metaObj.memory_category || r.entry.category;
const displayTier = metaObj.tier || "";
const tierPrefix = displayTier ? `[${displayTier.charAt(0).toUpperCase()}]` : "";
- // Select content tier based on recallMode/intent depth
- const contentText = recallMode === "summary"
- ? (metaObj.l0_abstract || r.entry.text)
- : intent?.depth === "full"
- ? (r.entry.text) // full text for deep queries
- : (metaObj.l0_abstract || r.entry.text); // L0/L1 default
- const summary = sanitizeForContext(contentText).slice(0, effectivePerItemMaxChars);
+ const abstract = metaObj.l0_abstract || r.entry.text;
+ const summary = sanitizeForContext(abstract).slice(0, autoRecallPerItemMaxChars);
return {
id: r.entry.id,
prefix: `${tierPrefix}[${displayCategory}:${r.entry.scope}]`,
@@ -2516,14 +2460,6 @@ const memoryLanceDBProPlugin = {
// See: https://github.com/CortexReach/memory-lancedb-pro/issues/260
const backgroundRun = (async () => {
try {
- // Feature 7: Check extraction rate limit before any work
- if (extractionRateLimiter.isRateLimited()) {
- api.logger.debug(
- `memory-lancedb-pro: auto-capture skipped (rate limited: ${extractionRateLimiter.getRecentCount()} extractions in last hour)`,
- );
- return;
- }
-
// Determine agent ID and default scope
const agentId = resolveHookAgentId(ctx?.agentId, (event as any).sessionKey);
const accessibleScopes = resolveScopeFilter(scopeManager, agentId);
@@ -2557,7 +2493,7 @@ const memoryLanceDBProPlugin = {
const content = msgObj.content;
if (typeof content === "string") {
- const normalized = normalizeAutoCaptureText(role, content, shouldSkipReflectionMessage);
+ const normalized = normalizeAutoCaptureText(role, content);
if (!normalized) {
skippedAutoCaptureTexts++;
} else {
@@ -2577,7 +2513,7 @@ const memoryLanceDBProPlugin = {
typeof (block as Record).text === "string"
) {
const text = (block as Record).text as string;
- const normalized = normalizeAutoCaptureText(role, text, shouldSkipReflectionMessage);
+ const normalized = normalizeAutoCaptureText(role, text);
if (!normalized) {
skippedAutoCaptureTexts++;
} else {
@@ -2652,39 +2588,8 @@ const memoryLanceDBProPlugin = {
);
}
- // ----------------------------------------------------------------
- // Feature 7: Skip low-value conversations
- // ----------------------------------------------------------------
- if (config.extractionThrottle?.skipLowValue === true) {
- const conversationValue = estimateConversationValue(texts);
- if (conversationValue < 0.2) {
- api.logger.debug(
- `memory-lancedb-pro: auto-capture skipped for agent ${agentId} (low conversation value: ${conversationValue.toFixed(2)})`,
- );
- return;
- }
- }
-
- // ----------------------------------------------------------------
- // Feature 1: Session compression — prioritize high-signal texts
- // ----------------------------------------------------------------
- if (config.sessionCompression?.enabled === true && texts.length > 0) {
- const maxChars = config.extractMaxChars ?? 8000;
- const compressed = compressTexts(texts, maxChars, {
- minScoreToKeep: config.sessionCompression?.minScoreToKeep,
- });
- if (compressed.dropped > 0) {
- api.logger.debug(
- `memory-lancedb-pro: session compression for agent ${agentId}: dropped ${compressed.dropped}/${texts.length} texts (${compressed.totalChars} chars kept)`,
- );
- texts = compressed.texts;
- }
- }
-
// ----------------------------------------------------------------
// Smart Extraction (Phase 1: LLM-powered 6-category extraction)
- // Rate limiter charged AFTER successful extraction, not before,
- // so no-op sessions don't consume the hourly quota.
// ----------------------------------------------------------------
if (smartExtractor) {
// Pre-filter: embedding-based noise detection (language-agnostic)
@@ -2704,8 +2609,6 @@ const memoryLanceDBProPlugin = {
conversationText, sessionKey,
{ scope: defaultScope, scopeFilter: accessibleScopes },
);
- // Charge rate limiter only after successful extraction
- extractionRateLimiter.recordExtraction();
if (stats.created > 0 || stats.merged > 0) {
api.logger.info(
`memory-lancedb-pro: smart-extracted ${stats.created} created, ${stats.merged} merged, ${stats.skipped} skipped for agent ${agentId}`
@@ -2802,7 +2705,7 @@ const memoryLanceDBProPlugin = {
l2_content: text,
source_session: (event as any).sessionKey || "unknown",
source: "auto-capture",
- state: "pending",
+ state: "confirmed",
memory_layer: "working",
injected_count: 0,
bad_recall_count: 0,
@@ -3640,6 +3543,10 @@ const memoryLanceDBProPlugin = {
// Run initial backup after a short delay, then schedule daily
setTimeout(() => void runBackup(), 60_000); // 1 min after start
backupTimer = setInterval(() => void runBackup(), BACKUP_INTERVAL_MS);
+
+ // Mark init complete ONLY after all setup succeeds — so any error during
+ // init leaves the plugin recoverable (can retry on next register() call).
+ _initialized = true;
},
stop: async () => {
if (backupTimer) {
@@ -3736,10 +3643,6 @@ export function parsePluginConfig(value: unknown): PluginConfig {
// Accept number, numeric string, or env-var string (e.g. "${EMBED_DIM}").
// Also accept legacy top-level `dimensions` for convenience.
dimensions: parsePositiveInt(embedding.dimensions ?? cfg.dimensions),
- omitDimensions:
- typeof embedding.omitDimensions === "boolean"
- ? embedding.omitDimensions
- : undefined,
taskQuery:
typeof embedding.taskQuery === "string"
? embedding.taskQuery
@@ -3766,6 +3669,7 @@ export function parsePluginConfig(value: unknown): PluginConfig {
autoRecallMaxItems: parsePositiveInt(cfg.autoRecallMaxItems) ?? 3,
autoRecallMaxChars: parsePositiveInt(cfg.autoRecallMaxChars) ?? 600,
autoRecallPerItemMaxChars: parsePositiveInt(cfg.autoRecallPerItemMaxChars) ?? 180,
+ autoRecallTimeoutMs: parsePositiveInt(cfg.autoRecallTimeoutMs) ?? 3000,
captureAssistant: cfg.captureAssistant === true,
retrieval: typeof cfg.retrieval === "object" && cfg.retrieval !== null ? cfg.retrieval as any : undefined,
decay: typeof cfg.decay === "object" && cfg.decay !== null ? cfg.decay as any : undefined,
@@ -3861,47 +3765,9 @@ export function parsePluginConfig(value: unknown): PluginConfig {
}
: undefined,
admissionControl: normalizeAdmissionControlConfig(cfg.admissionControl),
- memoryCompaction: (() => {
- const raw =
- typeof cfg.memoryCompaction === "object" && cfg.memoryCompaction !== null
- ? (cfg.memoryCompaction as Record)
- : null;
- if (!raw) return undefined;
- return {
- enabled: raw.enabled === true,
- minAgeDays: parsePositiveInt(raw.minAgeDays) ?? 7,
- similarityThreshold:
- typeof raw.similarityThreshold === "number"
- ? Math.max(0, Math.min(1, raw.similarityThreshold))
- : 0.88,
- minClusterSize: parsePositiveInt(raw.minClusterSize) ?? 2,
- maxMemoriesToScan: parsePositiveInt(raw.maxMemoriesToScan) ?? 200,
- cooldownHours: parsePositiveInt(raw.cooldownHours) ?? 24,
- };
- })(),
- sessionCompression:
- typeof cfg.sessionCompression === "object" && cfg.sessionCompression !== null
- ? {
- enabled:
- (cfg.sessionCompression as Record).enabled === true,
- minScoreToKeep:
- typeof (cfg.sessionCompression as Record).minScoreToKeep === "number"
- ? ((cfg.sessionCompression as Record).minScoreToKeep as number)
- : 0.3,
- }
- : { enabled: false, minScoreToKeep: 0.3 },
- extractionThrottle:
- typeof cfg.extractionThrottle === "object" && cfg.extractionThrottle !== null
- ? {
- skipLowValue:
- (cfg.extractionThrottle as Record).skipLowValue === true,
- maxExtractionsPerHour:
- typeof (cfg.extractionThrottle as Record).maxExtractionsPerHour === "number"
- ? ((cfg.extractionThrottle as Record).maxExtractionsPerHour as number)
- : 30,
- }
- : { skipLowValue: false, maxExtractionsPerHour: 30 },
};
}
+export function _resetInitialized() { _initialized = false; }
+
export default memoryLanceDBProPlugin;