Skip to content

Commit 961cbab

Browse files
committed
feat: route reflection mapped rows through the uniform dedup/merge pipeline
1 parent f6e63af commit 961cbab

11 files changed

Lines changed: 1369 additions & 133 deletions

dist/index.js

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { storeReflectionToLanceDB, loadAgentReflectionSlicesFromEntries, DEFAULT
3636
import { parseReflectionMetadata } from "./src/reflection-metadata.js";
3737
import { extractReflectionLearningGovernanceCandidates, extractInjectableReflectionMappedMemoryItems, isRecallUsed, } from "./src/reflection-slices.js";
3838
import { createReflectionEventId } from "./src/reflection-event-store.js";
39-
import { buildReflectionMappedMetadata, getReflectionMappedStorageCategory } from "./src/reflection-mapped-metadata.js";
39+
import { buildReflectionMappedMetadata, getReflectionMappedMemoryCategory, getReflectionMappedStorageCategory } from "./src/reflection-mapped-metadata.js";
4040
import { buildFallbackCandidate, gateRegexFallbackCapture } from "./src/autocapture-fallback-admission.js";
4141
import { gateMappedReflectionEntries, resolveMappedRowAdmissionController } from "./src/reflection-mapped-admission.js";
4242
import { createMemoryCLI } from "./cli.js";
@@ -4619,8 +4619,9 @@ const memoryLanceDBProPlugin = {
46194619
const MAX_MAPPED_ENTRIES = 100;
46204620
const mappedReflectionMemories = extractInjectableReflectionMappedMemoryItems(reflectionText);
46214621
const mappedEntries = [];
4622-
// Per-row embed + near-duplicate pre-check first, collecting the
4623-
// gate-eligible rows so the whole burst can share one admission call.
4622+
const mappedGatedItems = [];
4623+
// Per-row embed first, collecting the gate-eligible rows so the
4624+
// whole burst can share one admission call.
46244625
const gateEligible = [];
46254626
for (const mapped of mappedReflectionMemories) {
46264627
if (gateEligible.length >= MAX_MAPPED_ENTRIES) {
@@ -4635,28 +4636,25 @@ const memoryLanceDBProPlugin = {
46354636
api.logger.warn(`memory-reflection: mapped row embedding failed after retry, skipping row: ${String(embedErr)}`);
46364637
continue;
46374638
}
4638-
let existing = [];
4639-
let searchFailed = false;
4640-
try {
4641-
existing = await store.vectorSearch(vector, 1, 0.1, [targetScope]);
4642-
}
4643-
catch (err) {
4644-
api.logger.warn(`memory-reflection: mapped memory duplicate pre-check failed, skip store: ${String(err)}`);
4645-
searchFailed = true;
4646-
}
4647-
if (searchFailed) {
4648-
continue;
4649-
}
4650-
// Near-duplicate pre-check ahead of admission gating. This is the only dedup mapped
4651-
// rows get: a single vector-similarity threshold, direct skip, no LLM-mediated
4652-
// merge/contextualize/contradict decision. Extraction candidates own deduplicate()
4653-
// (src/smart-extractor.ts) is a genuinely different, richer pipeline (a 0.7
4654-
// pre-filter feeding an LLM decision, not a single hard cutoff) - deliberately not
4655-
// reused here yet. AdmissionController's "pass_to_dedup" decision for a mapped row
4656-
// is therefore always treated as "admit, subject to this cheaper pre-check" below,
4657-
// not "route through the same merge pipeline extraction candidates get".
4658-
if (existing.length > 0 && existing[0].score > 0.95) {
4659-
continue;
4639+
// Extractor-backed runs take the SAME dedup/merge pipeline
4640+
// extraction candidates get (persistGatedCandidates below), so no
4641+
// bespoke similarity cutoff runs here. The no-extractor fallback
4642+
// keeps the historical near-duplicate pre-check, downgraded from
4643+
// fail-closed to fail-open: a search blip stores the row (worst
4644+
// case the near-duplicate lands as a separate row — this path
4645+
// only pre-checks, it has no merge step) instead of silently
4646+
// dropping it.
4647+
if (!smartExtractor) {
4648+
let existing = [];
4649+
try {
4650+
existing = await store.vectorSearch(vector, 1, 0.1, [targetScope]);
4651+
}
4652+
catch (err) {
4653+
api.logger.warn(`memory-reflection: mapped memory duplicate pre-check failed, storing without pre-check: ${String(err)}`);
4654+
}
4655+
if (existing.length > 0 && existing[0].score > 0.95) {
4656+
continue;
4657+
}
46604658
}
46614659
gateEligible.push({ mapped, vector });
46624660
}
@@ -4709,14 +4707,61 @@ const memoryLanceDBProPlugin = {
47094707
baseMetadata.admission_audit = mappedGate.auditJson;
47104708
}
47114709
const metadata = JSON.stringify(baseMetadata);
4712-
mappedEntries.push({
4713-
text: mapped.text,
4714-
vector,
4715-
importance,
4716-
category: getReflectionMappedStorageCategory(mapped.mappedKind),
4717-
scope: targetScope,
4718-
metadata,
4710+
if (smartExtractor) {
4711+
// Uniform pipeline: judge (done above) -> dedup -> merge-writer,
4712+
// identical to extraction candidates. The entry builder keeps
4713+
// the reflection metadata on CREATE-shaped verdicts.
4714+
mappedGatedItems.push({
4715+
candidate: {
4716+
category: getReflectionMappedMemoryCategory(mapped.mappedKind),
4717+
abstract: mapped.text,
4718+
overview: `## ${mapped.heading}`,
4719+
content: mapped.text,
4720+
},
4721+
vector,
4722+
buildEntry: (v) => ({
4723+
text: mapped.text,
4724+
vector: v,
4725+
importance,
4726+
category: getReflectionMappedStorageCategory(mapped.mappedKind),
4727+
scope: targetScope,
4728+
metadata,
4729+
}),
4730+
});
4731+
}
4732+
else {
4733+
mappedEntries.push({
4734+
text: mapped.text,
4735+
vector,
4736+
importance,
4737+
category: getReflectionMappedStorageCategory(mapped.mappedKind),
4738+
scope: targetScope,
4739+
metadata,
4740+
});
4741+
}
4742+
}
4743+
if (smartExtractor && mappedGatedItems.length > 0) {
4744+
const gatedResult = await smartExtractor.persistGatedCandidates(mappedGatedItems, {
4745+
sessionKey,
4746+
targetScope,
4747+
scopeFilter: [targetScope],
4748+
agentId: ownerAgentId,
4749+
conversationText: conversation,
47194750
});
4751+
api.logger.info(`memory-reflection: mapped rows through uniform pipeline: ${gatedResult.createdEntries.length} created, ${gatedResult.stats.merged} merged, ${gatedResult.stats.skipped} skipped`);
4752+
if (mdMirror) {
4753+
for (const stored of gatedResult.createdEntries) {
4754+
let heading = "unknown";
4755+
try {
4756+
const storedMeta = stored.metadata ? JSON.parse(stored.metadata) : {};
4757+
heading = storedMeta._reflectionHeading ?? "unknown";
4758+
}
4759+
catch {
4760+
api.logger.warn(`memory-reflection: failed to parse stored metadata for entry ${stored.id}, using "unknown"`);
4761+
}
4762+
await mdMirror({ text: stored.text, category: stored.category, scope: stored.scope, timestamp: stored.timestamp }, { source: `reflection:${heading}`, agentId: sourceAgentId });
4763+
}
4764+
}
47204765
}
47214766
if (mappedEntries.length > 0) {
47224767
const storedEntries = await store.bulkStore(mappedEntries, ({ index, reason }) => {

0 commit comments

Comments
 (0)