Skip to content
Draft
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
75 changes: 47 additions & 28 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ import { createMemoryUpgrader } from "./src/memory-upgrader.js";
import { buildSmartMetadata, parseSmartMetadata, stringifySmartMetadata, toLifecycleMemory, } from "./src/smart-metadata.js";
import { computeTier1Patch, isSuppressed as isTier1Suppressed, TIER1_DEFAULT_BAD_RECALL_DECAY_MS, TIER1_DEFAULT_SUPPRESSION_DURATION_MS, } from "./src/auto-recall-tier1.js";
import { filterUserMdExclusiveRecallResults, isUserMdExclusiveMemory, } from "./src/workspace-boundary.js";
import { normalizeAdmissionControlConfig, resolveRejectedAuditFilePath, } from "./src/admission-control.js";
import { normalizeAdmissionControlConfig, createAdmissionController, resolveRejectedAuditFilePath, } from "./src/admission-control.js";
import { analyzeIntent, applyCategoryBoost } from "./src/intent-analyzer.js";
import { createOpenClawMemoryCapability } from "./src/openclaw-memory-capability.js";
import { CanonicalCorpusIndexer, parseCanonicalCorpusConfig, } from "./src/corpus-indexer.js";
Expand Down Expand Up @@ -1811,7 +1811,8 @@ function _initPluginState(api) {
// callback below closes over it.
const mdMirror = createMdMirrorWriter(api, config);
let smartExtractor = null;
if (config.smartExtraction !== false) {
let admissionController = null;
if (config.smartExtraction !== false || config.admissionControl.enabled === true) {
try {
const llmAuth = config.llm?.auth || "api-key";
const llmApiKey = llmAuth === "oauth"
Expand Down Expand Up @@ -1841,30 +1842,42 @@ function _initPluginState(api) {
log: (msg) => api.logger.debug(msg),
warnLog: (msg) => api.logger.warn(msg),
});
const noiseBank = new NoisePrototypeBank((msg) => api.logger.debug(msg));
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, {
user: "User",
extractMinMessages: config.extractMinMessages ?? 4,
extractMaxChars: config.extractMaxChars ?? 8000,
defaultScope: config.scopes?.default ?? "global",
workspaceBoundary: config.workspaceBoundary,
admissionControl: config.admissionControl,
onAdmissionRejected: admissionRejectionAuditWriter ?? undefined,
onPersisted: mdMirror ?? undefined,
log: (msg) => api.logger.info(msg),
debugLog: (msg) => api.logger.debug(msg),
noiseBank,
});
(isCliMode() ? api.logger.debug : api.logger.info)("memory-lancedb-pro: smart extraction enabled (LLM model: "
+ llmModel
+ ", timeoutMs: "
+ llmTimeoutMs
+ ", noise bank: ON)");
// Constructed independently of SmartExtractor so admission gating is
// available to other write paths (e.g. reflection-mapped rows, the
// regex fallback) even when smart extraction itself is disabled.
admissionController = createAdmissionController(store, llmClient, config.admissionControl, (msg) => api.logger.debug(msg));
if (config.smartExtraction !== false) {
const noiseBank = new NoisePrototypeBank((msg) => api.logger.debug(msg));
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, {
user: "User",
extractMinMessages: config.extractMinMessages ?? 4,
extractMaxChars: config.extractMaxChars ?? 8000,
defaultScope: config.scopes?.default ?? "global",
workspaceBoundary: config.workspaceBoundary,
admissionControl: config.admissionControl,
admissionController,
onAdmissionRejected: admissionRejectionAuditWriter ?? undefined,
onPersisted: mdMirror ?? undefined,
log: (msg) => api.logger.info(msg),
debugLog: (msg) => api.logger.debug(msg),
noiseBank,
});
(isCliMode() ? api.logger.debug : api.logger.info)("memory-lancedb-pro: smart extraction enabled (LLM model: "
+ llmModel
+ ", timeoutMs: "
+ llmTimeoutMs
+ ", noise bank: ON)");
}
}
catch (err) {
api.logger.warn(`memory-lancedb-pro: smart extraction init failed, falling back to regex: ${String(err)}`);
if (config.smartExtraction !== false) {
api.logger.warn(`memory-lancedb-pro: smart extraction init failed, falling back to regex: ${String(err)}`);
}
else {
api.logger.warn(`memory-lancedb-pro: standalone admission control init failed, admission gating unavailable: ${String(err)}`);
}
}
}
const extractionRateLimiter = createExtractionRateLimiter({
Expand Down Expand Up @@ -1900,6 +1913,7 @@ function _initPluginState(api) {
scopeManager,
migrator,
smartExtractor,
admissionController,
mdMirror,
extractionRateLimiter,
reflectionErrorStateBySession,
Expand Down Expand Up @@ -2020,7 +2034,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, admissionController, mdMirror, decayEngine, tierManager, extractionRateLimiter, reflectionErrorStateBySession, reflectionDerivedBySession, reflectionDerivedSuppressionBySession, reflectionByAgentCache, reflectionByAgentCacheGeneration, recallHistory, turnCounter, autoCaptureSeenTextCount, autoCapturePendingIngressTexts, autoCaptureRecentTexts, } = singleton;
warnForDisabledChannelPlugin(api.config, api.logger);
async function sleep(ms, signal) {
if (signal?.aborted) {
Expand Down Expand Up @@ -2261,7 +2275,7 @@ const memoryLanceDBProPlugin = {
const pendingRecall = new Map();
const logReg = isCliMode() ? api.logger.debug : api.logger.info;
if (isFirstRegistration) {
logReg(`memory-lancedb-pro@${pluginVersion}: plugin registered (db: ${resolvedDbPath}, model: ${config.embedding.model || "text-embedding-3-small"}, smartExtraction: ${smartExtractor ? 'ON' : 'OFF'})`);
logReg(`memory-lancedb-pro@${pluginVersion}: plugin registered (db: ${resolvedDbPath}, model: ${config.embedding.model || "text-embedding-3-small"}, smartExtraction: ${smartExtractor ? 'ON' : 'OFF'}, admissionControl: ${admissionController ? 'ON' : 'OFF'})`);
logReg(`memory-lancedb-pro: diagnostic build tag loaded (${DIAG_BUILD_TAG})`);
}
// Dual-memory model warning: help users understand the two-layer architecture
Expand Down Expand Up @@ -4066,10 +4080,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
11 changes: 11 additions & 0 deletions dist/src/admission-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,17 @@ async function scoreUtility(llm, mode, candidate, conversationText) {
reason: typeof response.reason === "string" ? response.reason.trim() : undefined,
};
}
/**
* Construct an AdmissionController independently of any extraction engine.
* Availability depends only on the admission config's `enabled` flag, so
* callers that never build a SmartExtractor (e.g. smartExtraction: false)
* can still obtain a working controller to gate other write paths.
*/
export function createAdmissionController(store, llm, config, debugLog = () => { }) {
return config?.enabled === true
? new AdmissionController(store, llm, config, debugLog)
: null;
}
export class AdmissionController {
store;
llm;
Expand Down
6 changes: 1 addition & 5 deletions dist/src/smart-extractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
*
*/
import { buildExtractionPrompt, buildDedupPrompt, buildMergePrompt, } from "./extraction-prompts.js";
import { AdmissionController, } from "./admission-control.js";
import { ALWAYS_MERGE_CATEGORIES, getStorageCategoryForMemoryCategory, MERGE_SUPPORTED_CATEGORIES, TEMPORAL_VERSIONED_CATEGORIES, normalizeCategory, } from "./memory-categories.js";
import { isMetaFrustrationNoise, isNoise } from "./noise-filter.js";
import { appendRelation, buildSmartMetadata, deriveFactKey, parseSmartMetadata, stringifySmartMetadata, parseSupportInfo, updateSupportStats, } from "./smart-metadata.js";
Expand Down Expand Up @@ -192,10 +191,7 @@ export class SmartExtractor {
config.admissionControl.auditMetadata !== false;
this.onAdmissionRejected = config.onAdmissionRejected;
this.onPersisted = config.onPersisted;
this.admissionController =
config.admissionControl?.enabled === true
? new AdmissionController(this.store, this.llm, config.admissionControl, this.debugLog)
: null;
this.admissionController = config.admissionController ?? null;
}
/**
* Notify the onPersisted sink (e.g. markdown mirror) after a successful
Expand Down
143 changes: 83 additions & 60 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ import {
} from "./src/workspace-boundary.js";
import {
normalizeAdmissionControlConfig,
createAdmissionController,
resolveRejectedAuditFilePath,
type AdmissionControlConfig,
type AdmissionRejectionAuditEntry,
type AdmissionController,
} from "./src/admission-control.js";
import { analyzeIntent, applyCategoryBoost } from "./src/intent-analyzer.js";
import { createOpenClawMemoryCapability } from "./src/openclaw-memory-capability.js";
Expand Down Expand Up @@ -2337,6 +2339,7 @@ interface PluginSingletonState {
scopeManager: ReturnType<typeof createScopeManager>;
migrator: ReturnType<typeof createMigrator>;
smartExtractor: SmartExtractor | null;
admissionController: AdmissionController | null;
mdMirror: MdMirrorWriter | null;
extractionRateLimiter: ReturnType<typeof createExtractionRateLimiter>;
// Session Maps — persist across scope refreshes instead of being recreated
Expand Down Expand Up @@ -2469,71 +2472,89 @@ function _initPluginState(api: OpenClawPluginApi): PluginSingletonState {
// callback below closes over it.
const mdMirror = createMdMirrorWriter(api, config);

let smartExtractor: SmartExtractor | null = null;
if (config.smartExtraction !== false) {
try {
let smartExtractor: SmartExtractor | null = null;
let admissionController: AdmissionController | null = null;
if (config.smartExtraction !== false || config.admissionControl.enabled === true) {
try {
const llmAuth = config.llm?.auth || "api-key";
const llmApiKey = llmAuth === "oauth"
? undefined
: config.llm?.apiKey
? resolveSecretCredential(api, config.llm.apiKey, "llm.apiKey")
: resolveFirstApiKey(api, config.embedding.apiKey);
const llmBaseURL = llmAuth === "oauth"
? (config.llm?.baseURL ? resolveEnvVars(config.llm.baseURL) : undefined)
: config.llm?.baseURL
? resolveEnvVars(config.llm.baseURL)
: config.embedding.baseURL;
const llmModel = config.llm?.model || "openai/gpt-oss-120b";
const llmOauthPath = llmAuth === "oauth"
? resolveOptionalPathWithEnv(api, config.llm?.oauthPath, ".memory-lancedb-pro/oauth.json")
: undefined;
const llmOauthProvider = llmAuth === "oauth" ? config.llm?.oauthProvider : undefined;
const llmTimeoutMs = resolveLlmTimeoutMs(config);

const llmClient = createLlmClient({
auth: llmAuth,
apiKey: llmApiKey,
model: llmModel,
baseURL: llmBaseURL,
oauthProvider: llmOauthProvider,
oauthPath: llmOauthPath,
timeoutMs: llmTimeoutMs,
log: (msg: string) => api.logger.debug(msg),
warnLog: (msg: string) => api.logger.warn(msg),
});

const noiseBank = new NoisePrototypeBank((msg: string) => api.logger.debug(msg));
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, {
user: "User",
extractMinMessages: config.extractMinMessages ?? 4,
extractMaxChars: config.extractMaxChars ?? 8000,
defaultScope: config.scopes?.default ?? "global",
workspaceBoundary: config.workspaceBoundary,
admissionControl: config.admissionControl,
onAdmissionRejected: admissionRejectionAuditWriter ?? undefined,
onPersisted: mdMirror ?? undefined,
log: (msg: string) => api.logger.info(msg),
debugLog: (msg: string) => api.logger.debug(msg),
noiseBank,
});

(isCliMode() ? api.logger.debug : api.logger.info)(
"memory-lancedb-pro: smart extraction enabled (LLM model: "
+ llmModel
+ ", timeoutMs: "
+ llmTimeoutMs
+ ", noise bank: ON)",
);
} catch (err) {
api.logger.warn(`memory-lancedb-pro: smart extraction init failed, falling back to regex: ${String(err)}`);
}
}
const llmBaseURL = llmAuth === "oauth"
? (config.llm?.baseURL ? resolveEnvVars(config.llm.baseURL) : undefined)
: config.llm?.baseURL
? resolveEnvVars(config.llm.baseURL)
: config.embedding.baseURL;
const llmModel = config.llm?.model || "openai/gpt-oss-120b";
const llmOauthPath = llmAuth === "oauth"
? resolveOptionalPathWithEnv(api, config.llm?.oauthPath, ".memory-lancedb-pro/oauth.json")
: undefined;
const llmOauthProvider = llmAuth === "oauth" ? config.llm?.oauthProvider : undefined;
const llmTimeoutMs = resolveLlmTimeoutMs(config);

const llmClient = createLlmClient({
auth: llmAuth,
apiKey: llmApiKey,
model: llmModel,
baseURL: llmBaseURL,
oauthProvider: llmOauthProvider,
oauthPath: llmOauthPath,
timeoutMs: llmTimeoutMs,
log: (msg: string) => api.logger.debug(msg),
warnLog: (msg: string) => api.logger.warn(msg),
});

// Constructed independently of SmartExtractor so admission gating is
// available to other write paths (e.g. reflection-mapped rows, the
// regex fallback) even when smart extraction itself is disabled.
admissionController = createAdmissionController(
store,
llmClient,
config.admissionControl,
(msg: string) => api.logger.debug(msg),
);

if (config.smartExtraction !== false) {
const noiseBank = new NoisePrototypeBank((msg: string) => api.logger.debug(msg));
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, {
user: "User",
extractMinMessages: config.extractMinMessages ?? 4,
extractMaxChars: config.extractMaxChars ?? 8000,
defaultScope: config.scopes?.default ?? "global",
workspaceBoundary: config.workspaceBoundary,
admissionControl: config.admissionControl,
admissionController,
onAdmissionRejected: admissionRejectionAuditWriter ?? undefined,
onPersisted: mdMirror ?? undefined,
log: (msg: string) => api.logger.info(msg),
debugLog: (msg: string) => api.logger.debug(msg),
noiseBank,
});

(isCliMode() ? api.logger.debug : api.logger.info)(
"memory-lancedb-pro: smart extraction enabled (LLM model: "
+ llmModel
+ ", timeoutMs: "
+ llmTimeoutMs
+ ", noise bank: ON)",
);
}
} catch (err) {
if (config.smartExtraction !== false) {
api.logger.warn(`memory-lancedb-pro: smart extraction init failed, falling back to regex: ${String(err)}`);
} else {
api.logger.warn(`memory-lancedb-pro: standalone admission control init failed, admission gating unavailable: ${String(err)}`);
}
}
}

const extractionRateLimiter = createExtractionRateLimiter({
maxExtractionsPerHour: config.extractionThrottle?.maxExtractionsPerHour,
Expand Down Expand Up @@ -2570,6 +2591,7 @@ function _initPluginState(api: OpenClawPluginApi): PluginSingletonState {
scopeManager,
migrator,
smartExtractor,
admissionController,
mdMirror,
extractionRateLimiter,
reflectionErrorStateBySession,
Expand Down Expand Up @@ -2721,6 +2743,7 @@ const memoryLanceDBProPlugin = {
scopeManager,
migrator,
smartExtractor,
admissionController,
mdMirror,
decayEngine,
tierManager,
Expand Down Expand Up @@ -3035,7 +3058,7 @@ const memoryLanceDBProPlugin = {
const logReg = isCliMode() ? api.logger.debug : api.logger.info;
if (isFirstRegistration) {
logReg(
`memory-lancedb-pro@${pluginVersion}: plugin registered (db: ${resolvedDbPath}, model: ${config.embedding.model || "text-embedding-3-small"}, smartExtraction: ${smartExtractor ? 'ON' : 'OFF'})`
`memory-lancedb-pro@${pluginVersion}: plugin registered (db: ${resolvedDbPath}, model: ${config.embedding.model || "text-embedding-3-small"}, smartExtraction: ${smartExtractor ? 'ON' : 'OFF'}, admissionControl: ${admissionController ? 'ON' : 'OFF'})`
);
logReg(`memory-lancedb-pro: diagnostic build tag loaded (${DIAG_BUILD_TAG})`);
}
Expand Down
Loading
Loading