Skip to content
Open
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
8 changes: 8 additions & 0 deletions cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ interface CLIContext {
llmClient?: LlmClient;
pluginId?: string;
pluginConfig?: Record<string, unknown>;
// Called synchronously after a delete or delete-bulk command actually removes
// memories, so the host plugin can invalidate any in-process read caches (e.g.
// reflection slice caches) keyed on the affected scope(s) before they go stale.
onMemoriesDeleted?: (info: { scopeFilter?: string[] }) => void;
oauthTestHooks?: {
openUrl?: (url: string) => void | Promise<void>;
authorizeUrl?: (url: string) => void | Promise<void>;
Expand Down Expand Up @@ -1472,6 +1476,7 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
const deleted = await context.store.delete(id, scopeFilter);

if (deleted) {
context.onMemoriesDeleted?.({ scopeFilter });
console.log(`Memory ${id} deleted successfully.`);
printReadConsistencyHint(context.store);
} else {
Expand Down Expand Up @@ -1517,6 +1522,9 @@ export function registerMemoryCLI(program: Command, context: CLIContext): void {
console.log(`Would delete from ${stats.totalCount} memories in matching scopes.`);
} else {
const deletedCount = await context.store.bulkDelete(options.scope, beforeTimestamp);
if (deletedCount > 0) {
context.onMemoriesDeleted?.({ scopeFilter: options.scope });
}
console.log(`Deleted ${deletedCount} memories.`);
printReadConsistencyHint(context.store);
}
Expand Down
4 changes: 4 additions & 0 deletions dist/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@ export function registerMemoryCLI(program, context) {
}
const deleted = await context.store.delete(id, scopeFilter);
if (deleted) {
context.onMemoriesDeleted?.({ scopeFilter });
console.log(`Memory ${id} deleted successfully.`);
printReadConsistencyHint(context.store);
}
Expand Down Expand Up @@ -1240,6 +1241,9 @@ export function registerMemoryCLI(program, context) {
}
else {
const deletedCount = await context.store.bulkDelete(options.scope, beforeTimestamp);
if (deletedCount > 0) {
context.onMemoriesDeleted?.({ scopeFilter: options.scope });
}
console.log(`Deleted ${deletedCount} memories.`);
printReadConsistencyHint(context.store);
}
Expand Down
42 changes: 40 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ const DEFAULT_REFLECTION_ERROR_SCAN_MAX_CHARS = 8_000;
const DEFAULT_SERIAL_GUARD_COOLDOWN_MS = 120_000;
const DEFAULT_REFLECTION_EMPTY_EVENT_GUARD_TTL_MS = 120_000;
const DEFAULT_REFLECTION_EMPTY_EVENT_GUARD_MAX_ENTRIES = 200;
const DEFAULT_REFLECTION_CACHE_TTL_MS = 15_000;
// After /new or /reset, the just-closed session may have generated fresh
// derived deltas. Keep those out of the immediately opened prompt window.
const DEFAULT_REFLECTION_BOUNDARY_DERIVED_SUPPRESSION_MS = 120_000;
Expand Down Expand Up @@ -2171,7 +2172,7 @@ const memoryLanceDBProPlugin = {
: "<NO_SCOPE_FILTER>";
const cacheKey = `${agentId}::${scopeKey}`;
const cached = reflectionByAgentCache.get(cacheKey);
if (cached && Date.now() - cached.updatedAt < 15_000)
if (cached && Date.now() - cached.updatedAt < DEFAULT_REFLECTION_CACHE_TTL_MS)
return cached;
// Prefer reflection-category rows to avoid full-table reads on bypass callers.
// Fall back to an uncategorized scan only when the category query produced no
Expand Down Expand Up @@ -2204,6 +2205,41 @@ const memoryLanceDBProPlugin = {
reflectionByAgentCache.set(cacheKey, next);
return next;
};
// Fast-path invalidation for SAME-PROCESS deletes only: CLI delete/delete-bulk
// commands run as a short-lived, separate process from the long-running Gateway
// in typical deployments, so this callback firing there does not reach (and
// cannot invalidate) the Gateway process own in-memory caches. It only has an
// effect when a delete genuinely happens inside this same plugin instance.
//
// The actual cross-process staleness bound comes from two other layers:
// - DEFAULT_REFLECTION_CACHE_TTL_MS bounds how long either cache below can
// serve stale content after ANY delete, same-process or not (see the read
// sites in loadAgentReflectionSlices and the derived-focus injector).
// - readConsistencyInterval (store config) bounds how long the underlying
// LanceDB table handle can serve stale rows to a fresh query in the first
// place, which is what a TTL-expired cache re-populates from.
//
// reflectionByAgentCache is keyed "<agentId>::scopes:<sorted,scopes>" (or
// "<agentId>::<NO_SCOPE_FILTER>"); drop any entry whose scope set intersects
// the deleted scopes, plus every no-scope-filter entry (it spans all scopes).
// reflectionDerivedBySession has no cheap scope-to-session mapping, so it is
// cleared in full rather than left to expire on its own TTL.
const invalidateReflectionCachesAfterDelete = (deletedScopes) => {
const deletedSet = new Set(deletedScopes ?? []);
for (const cacheKey of reflectionByAgentCache.keys()) {
const sepIdx = cacheKey.indexOf("::");
const scopePart = sepIdx === -1 ? "" : cacheKey.slice(sepIdx + 2);
if (scopePart === "<NO_SCOPE_FILTER>" || deletedSet.size === 0) {
reflectionByAgentCache.delete(cacheKey);
continue;
}
const cachedScopes = scopePart.startsWith("scopes:") ? scopePart.slice("scopes:".length).split(",") : [];
if (cachedScopes.some((s) => deletedSet.has(s))) {
reflectionByAgentCache.delete(cacheKey);
}
}
reflectionDerivedBySession.clear();
};
const pendingRecall = new Map();
const logReg = isCliMode() ? api.logger.debug : api.logger.info;
if (isFirstRegistration) {
Expand Down Expand Up @@ -2346,6 +2382,7 @@ const memoryLanceDBProPlugin = {
store,
retriever,
scopeManager,
onMemoriesDeleted: ({ scopeFilter }) => invalidateReflectionCachesAfterDelete(scopeFilter),
migrator,
embedder,
llmClient: smartExtractor ? (() => {
Expand Down Expand Up @@ -3514,7 +3551,8 @@ const memoryLanceDBProPlugin = {
reflectionDerivedSuppressionBySession.delete(sessionKey);
const scopes = resolveScopeFilter(scopeManager, agentId);
const derivedCache = sessionKey ? reflectionDerivedBySession.get(sessionKey) : null;
const derivedLines = derivedCache?.derived?.length
const derivedCacheFresh = derivedCache && Date.now() - derivedCache.updatedAt < DEFAULT_REFLECTION_CACHE_TTL_MS;
const derivedLines = derivedCacheFresh && derivedCache.derived.length
? derivedCache.derived
: (await loadAgentReflectionSlices(agentId, scopes)).derived;
if (derivedLines.length > 0) {
Expand Down
43 changes: 41 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,7 @@ const DEFAULT_REFLECTION_ERROR_SCAN_MAX_CHARS = 8_000;
const DEFAULT_SERIAL_GUARD_COOLDOWN_MS = 120_000;
const DEFAULT_REFLECTION_EMPTY_EVENT_GUARD_TTL_MS = 120_000;
const DEFAULT_REFLECTION_EMPTY_EVENT_GUARD_MAX_ENTRIES = 200;
const DEFAULT_REFLECTION_CACHE_TTL_MS = 15_000;
// After /new or /reset, the just-closed session may have generated fresh
// derived deltas. Keep those out of the immediately opened prompt window.
const DEFAULT_REFLECTION_BOUNDARY_DERIVED_SUPPRESSION_MS = 120_000;
Expand Down Expand Up @@ -2931,7 +2932,7 @@ const memoryLanceDBProPlugin = {
: "<NO_SCOPE_FILTER>";
const cacheKey = `${agentId}::${scopeKey}`;
const cached = reflectionByAgentCache.get(cacheKey);
if (cached && Date.now() - cached.updatedAt < 15_000) return cached;
if (cached && Date.now() - cached.updatedAt < DEFAULT_REFLECTION_CACHE_TTL_MS) return cached;

// Prefer reflection-category rows to avoid full-table reads on bypass callers.
// Fall back to an uncategorized scan only when the category query produced no
Expand Down Expand Up @@ -2964,6 +2965,42 @@ const memoryLanceDBProPlugin = {
return next;
};

// Fast-path invalidation for SAME-PROCESS deletes only: CLI delete/delete-bulk
// commands run as a short-lived, separate process from the long-running Gateway
// in typical deployments, so this callback firing there does not reach (and
// cannot invalidate) the Gateway process own in-memory caches. It only has an
// effect when a delete genuinely happens inside this same plugin instance.
//
// The actual cross-process staleness bound comes from two other layers:
// - DEFAULT_REFLECTION_CACHE_TTL_MS bounds how long either cache below can
// serve stale content after ANY delete, same-process or not (see the read
// sites in loadAgentReflectionSlices and the derived-focus injector).
// - readConsistencyInterval (store config) bounds how long the underlying
// LanceDB table handle can serve stale rows to a fresh query in the first
// place, which is what a TTL-expired cache re-populates from.
//
// reflectionByAgentCache is keyed "<agentId>::scopes:<sorted,scopes>" (or
// "<agentId>::<NO_SCOPE_FILTER>"); drop any entry whose scope set intersects
// the deleted scopes, plus every no-scope-filter entry (it spans all scopes).
// reflectionDerivedBySession has no cheap scope-to-session mapping, so it is
// cleared in full rather than left to expire on its own TTL.
const invalidateReflectionCachesAfterDelete = (deletedScopes: string[] | undefined) => {
const deletedSet = new Set(deletedScopes ?? []);
for (const cacheKey of reflectionByAgentCache.keys()) {
const sepIdx = cacheKey.indexOf("::");
const scopePart = sepIdx === -1 ? "" : cacheKey.slice(sepIdx + 2);
if (scopePart === "<NO_SCOPE_FILTER>" || deletedSet.size === 0) {
reflectionByAgentCache.delete(cacheKey);
continue;
}
const cachedScopes = scopePart.startsWith("scopes:") ? scopePart.slice("scopes:".length).split(",") : [];
if (cachedScopes.some((s) => deletedSet.has(s))) {
reflectionByAgentCache.delete(cacheKey);
}
}
reflectionDerivedBySession.clear();
};

// ========================================================================
// Proposal A Phase 1: Recall Usage Tracking Hooks
// ========================================================================
Expand Down Expand Up @@ -3155,6 +3192,7 @@ const memoryLanceDBProPlugin = {
store,
retriever,
scopeManager,
onMemoriesDeleted: ({ scopeFilter }) => invalidateReflectionCachesAfterDelete(scopeFilter),
migrator,
embedder,
llmClient: smartExtractor ? (() => {
Expand Down Expand Up @@ -4546,7 +4584,8 @@ const memoryLanceDBProPlugin = {
if (suppression) reflectionDerivedSuppressionBySession.delete(sessionKey);
const scopes = resolveScopeFilter(scopeManager, agentId);
const derivedCache = sessionKey ? reflectionDerivedBySession.get(sessionKey) : null;
const derivedLines = derivedCache?.derived?.length
const derivedCacheFresh = derivedCache && Date.now() - derivedCache.updatedAt < DEFAULT_REFLECTION_CACHE_TTL_MS;
const derivedLines = derivedCacheFresh && derivedCache.derived.length
? derivedCache.derived
: (await loadAgentReflectionSlices(agentId, scopes)).derived;
if (derivedLines.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"skills/**/*.md"
],
"scripts": {
"test": "node test/embedder-error-hints.test.mjs && node --test test/embedder-max-input-chars.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/extraction-prompt-structural-noise.test.mjs && node test/i18n-memory-triggers.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node --test test/per-agent-auto-recall.test.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node --test test/smart-extractor-noise-gating.test.mjs && node test/memory-capability-runtime.test.mjs && node --test test/startup-health-diagnostics.test.mjs && node test/corpus-indexer.test.mjs && node --test test/regex-fallback-bulk-store.test.mjs && node test/plugin-manifest-regression.mjs && node --test test/dreaming-engine.test.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-update-metadata-refresh.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs && node test/is-latest-auto-supersede.test.mjs && node --test test/temporal-awareness.test.mjs && node --test test/command-reflection-guard.test.mjs && node --test test/tier1-counters.test.mjs && node --test test/startup-check-timeout.test.mjs && node --test test/memory-subsession-prompt-hooks.test.mjs && node --test test/read-consistency-interval.test.mjs && node --test test/reflection-distiller-hook-skip.test.mjs && node --test test/register-scope-dedup.test.mjs && node --test test/raw-run-distiller-hooks.test.mjs",
"test": "node test/embedder-error-hints.test.mjs && node --test test/embedder-max-input-chars.test.mjs && node test/cjk-recursion-regression.test.mjs && node test/extraction-prompt-structural-noise.test.mjs && node test/i18n-memory-triggers.test.mjs && node test/migrate-legacy-schema.test.mjs && node --test test/config-session-strategy-migration.test.mjs && node --test test/scope-access-undefined.test.mjs && node --test test/reflection-bypass-hook.test.mjs && node --test test/smart-extractor-scope-filter.test.mjs && node --test test/store-empty-scope-filter.test.mjs && node --test test/recall-text-cleanup.test.mjs && node test/update-consistency-lancedb.test.mjs && node --test test/strip-envelope-metadata.test.mjs && node test/cli-smoke.mjs && node test/functional-e2e.mjs && node --test test/per-agent-auto-recall.test.mjs && node test/retriever-rerank-regression.mjs && node test/smart-memory-lifecycle.mjs && node test/smart-extractor-branches.mjs && node --test test/smart-extractor-noise-gating.test.mjs && node test/memory-capability-runtime.test.mjs && node --test test/startup-health-diagnostics.test.mjs && node test/corpus-indexer.test.mjs && node --test test/regex-fallback-bulk-store.test.mjs && node test/plugin-manifest-regression.mjs && node --test test/dreaming-engine.test.mjs && node --test test/session-summary-before-reset.test.mjs && node --test test/sync-plugin-version.test.mjs && node test/smart-metadata-v2.mjs && node test/vector-search-cosine.test.mjs && node test/context-support-e2e.mjs && node test/temporal-facts.test.mjs && node test/memory-update-supersede.test.mjs && node test/memory-update-metadata-refresh.test.mjs && node test/memory-upgrader-diagnostics.test.mjs && node --test test/llm-api-key-client.test.mjs && node --test test/llm-oauth-client.test.mjs && node --test test/cli-oauth-login.test.mjs && node --test test/workflow-fork-guards.test.mjs && node --test test/clawteam-scope.test.mjs && node --test test/cross-process-lock.test.mjs && node --test test/preference-slots.test.mjs && node test/is-latest-auto-supersede.test.mjs && node --test test/temporal-awareness.test.mjs && node --test test/command-reflection-guard.test.mjs && node --test test/tier1-counters.test.mjs && node --test test/startup-check-timeout.test.mjs && node --test test/memory-subsession-prompt-hooks.test.mjs && node --test test/read-consistency-interval.test.mjs && node --test test/reflection-distiller-hook-skip.test.mjs && node --test test/register-scope-dedup.test.mjs && node --test test/raw-run-distiller-hooks.test.mjs && node --test test/delete-invalidate-reflection-caches.test.mjs",
"test:cli-smoke": "node scripts/run-ci-tests.mjs --group cli-smoke",
"test:core-regression": "node scripts/run-ci-tests.mjs --group core-regression",
"test:storage-and-schema": "node scripts/run-ci-tests.mjs --group storage-and-schema",
Expand Down
2 changes: 2 additions & 0 deletions scripts/ci-test-manifest.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ export const CI_TEST_MANIFEST = [
{ group: "core-regression", runner: "node", file: "test/register-scope-dedup.test.mjs", args: ["--test"] },
// Reflection distiller sub-run must request raw-run semantics (skip foreign before_prompt_build hooks)
{ group: "core-regression", runner: "node", file: "test/raw-run-distiller-hooks.test.mjs", args: ["--test"] },
// Delete/delete-bulk must synchronously invalidate in-process reflection read caches
{ group: "core-regression", runner: "node", file: "test/delete-invalidate-reflection-caches.test.mjs", args: ["--test"] },
];

export function getEntriesForGroup(group) {
Expand Down
Loading
Loading