重建会话记录逻辑
This commit is contained in:
+120
-3
@@ -57,13 +57,33 @@ export class MemoryStore {
|
||||
return this.serializeWrite(async () => {
|
||||
const content = normalizeMemoryContent(draft.content);
|
||||
if (!content) {
|
||||
return { changed: false, entry: null as MemoryEntry | null };
|
||||
return {
|
||||
changed: false,
|
||||
detail: "content rejected by persistence policy",
|
||||
entry: null as MemoryEntry | null,
|
||||
similar: null as MemoryEntry | null,
|
||||
};
|
||||
}
|
||||
|
||||
const entries = await this.readEntries(scope, key);
|
||||
const existing = entries.find((entry) => entry.content === content);
|
||||
if (existing) {
|
||||
return { changed: false, entry: existing };
|
||||
return {
|
||||
changed: false,
|
||||
detail: "memory already existed",
|
||||
entry: existing,
|
||||
similar: existing,
|
||||
};
|
||||
}
|
||||
|
||||
const similar = findSimilarMemory(entries, content);
|
||||
if (similar) {
|
||||
return {
|
||||
changed: false,
|
||||
detail: "similar memory already exists",
|
||||
entry: similar,
|
||||
similar,
|
||||
};
|
||||
}
|
||||
|
||||
const entry: MemoryEntry = {
|
||||
@@ -80,7 +100,12 @@ export class MemoryStore {
|
||||
rootDir: this.baseDir,
|
||||
},
|
||||
);
|
||||
return { changed: true, entry };
|
||||
return {
|
||||
changed: true,
|
||||
detail: "memory stored",
|
||||
entry,
|
||||
similar: null as MemoryEntry | null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -105,6 +130,13 @@ export class MemoryStore {
|
||||
if (duplicate) {
|
||||
return { changed: false, detail: "replacement would duplicate an existing memory" };
|
||||
}
|
||||
const similar = findSimilarMemory(entries, content, entries[index]?.id);
|
||||
if (similar) {
|
||||
return {
|
||||
changed: false,
|
||||
detail: "replacement would overlap with a similar existing memory",
|
||||
};
|
||||
}
|
||||
entries[index] = {
|
||||
content,
|
||||
id: entries[index]?.id ?? toStableId(scope, key, content.toLowerCase()),
|
||||
@@ -214,6 +246,91 @@ const normalizeMemoryContent = (content: string) => {
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const findSimilarMemory = (
|
||||
entries: MemoryEntry[],
|
||||
content: string,
|
||||
excludeId?: string,
|
||||
) =>
|
||||
entries.find(
|
||||
(entry) => entry.id !== excludeId && areSimilarMemoryContents(entry.content, content),
|
||||
) ?? null;
|
||||
|
||||
const areSimilarMemoryContents = (left: string, right: string) => {
|
||||
const normalizedLeft = normalizeComparableMemory(left);
|
||||
const normalizedRight = normalizeComparableMemory(right);
|
||||
if (!normalizedLeft || !normalizedRight) {
|
||||
return false;
|
||||
}
|
||||
if (normalizedLeft === normalizedRight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const [shorter, longer] =
|
||||
normalizedLeft.length <= normalizedRight.length
|
||||
? [normalizedLeft, normalizedRight]
|
||||
: [normalizedRight, normalizedLeft];
|
||||
if (shorter.length >= 12 && longer.includes(shorter)) {
|
||||
return true;
|
||||
}
|
||||
if (shorter.length < 8) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
longestCommonSubsequenceLength(normalizedLeft, normalizedRight) / shorter.length >= 0.5
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
diceCoefficient(buildCharacterBigrams(normalizedLeft), buildCharacterBigrams(normalizedRight)) >=
|
||||
0.72
|
||||
);
|
||||
};
|
||||
|
||||
const normalizeComparableMemory = (content: string) =>
|
||||
normalizeMemoryContent(content)
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}]+/gu, "");
|
||||
|
||||
const buildCharacterBigrams = (content: string) => {
|
||||
const grams = new Set<string>();
|
||||
for (let index = 0; index < content.length - 1; index += 1) {
|
||||
grams.add(content.slice(index, index + 2));
|
||||
}
|
||||
return grams;
|
||||
};
|
||||
|
||||
const diceCoefficient = (left: Set<string>, right: Set<string>) => {
|
||||
if (left.size === 0 || right.size === 0) {
|
||||
return 0;
|
||||
}
|
||||
let overlap = 0;
|
||||
for (const item of left) {
|
||||
if (right.has(item)) {
|
||||
overlap += 1;
|
||||
}
|
||||
}
|
||||
return (2 * overlap) / (left.size + right.size);
|
||||
};
|
||||
|
||||
const longestCommonSubsequenceLength = (left: string, right: string) => {
|
||||
const previous = new Array(right.length + 1).fill(0);
|
||||
const current = new Array(right.length + 1).fill(0);
|
||||
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
||||
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
||||
current[rightIndex] =
|
||||
left[leftIndex - 1] === right[rightIndex - 1]
|
||||
? previous[rightIndex - 1] + 1
|
||||
: Math.max(previous[rightIndex], current[rightIndex - 1]);
|
||||
}
|
||||
for (let rightIndex = 0; rightIndex <= right.length; rightIndex += 1) {
|
||||
previous[rightIndex] = current[rightIndex];
|
||||
current[rightIndex] = 0;
|
||||
}
|
||||
}
|
||||
return previous[right.length];
|
||||
};
|
||||
|
||||
const parseMemoryMarkdown = (content: string): MemoryEntry[] =>
|
||||
content
|
||||
.split("\n")
|
||||
|
||||
Reference in New Issue
Block a user