diff --git a/src/main/libs/coworkMemoryExtractor.test.ts b/src/main/libs/coworkMemoryExtractor.test.ts new file mode 100644 index 00000000..18668301 --- /dev/null +++ b/src/main/libs/coworkMemoryExtractor.test.ts @@ -0,0 +1,46 @@ +import { expect,test } from 'vitest'; + +import { extractTurnMemoryChanges } from './coworkMemoryExtractor'; + +const assistantText = '好的,已了解。'; + +test('keeps the highest-confidence implicit memories when more than the cap qualify', () => { + // Order matters: two lower-confidence candidates appear BEFORE the highest one. + // preference (0.88) -> ownership (0.9) -> profile (0.93) + const userText = '我喜欢深色主题。我养了一只猫。我叫张三。'; + + const changes = extractTurnMemoryChanges({ + userText, + assistantText, + guardLevel: 'relaxed', + }); + + const implicitAdds = changes.filter((c) => !c.isExplicit && c.action === 'add'); + + // Cap is 2 per turn. + expect(implicitAdds).toHaveLength(2); + + // The profile fact (0.93) must survive even though it appears last, and the + // lowest-confidence preference (0.88) must be the one dropped. + const kept = implicitAdds.map((c) => c.text); + expect(kept).toContain('我叫张三'); + expect(kept).toContain('我养了一只猫'); + expect(kept).not.toContain('我喜欢深色主题'); + + // Sorted by confidence descending. + expect(implicitAdds[0].confidence).toBeGreaterThanOrEqual(implicitAdds[1].confidence); +}); + +test('deduplicates identical implicit candidates within a single turn', () => { + const userText = '我叫张三。我叫张三。'; + + const changes = extractTurnMemoryChanges({ + userText, + assistantText, + guardLevel: 'relaxed', + }); + + const implicitAdds = changes.filter((c) => !c.isExplicit && c.action === 'add'); + expect(implicitAdds).toHaveLength(1); + expect(implicitAdds[0].text).toBe('我叫张三'); +}); diff --git a/src/main/libs/coworkMemoryExtractor.ts b/src/main/libs/coworkMemoryExtractor.ts index 00f8c6f7..c96189e9 100644 --- a/src/main/libs/coworkMemoryExtractor.ts +++ b/src/main/libs/coworkMemoryExtractor.ts @@ -172,11 +172,15 @@ function extractImplicit(options: ExtractTurnMemoryOptions): ExtractedMemoryChan isExplicit: false, reason, }); - - if (result.length >= maxImplicitAdds) break; } - return result; + // Keep the highest-confidence candidates rather than the first ones seen, so a + // low-value preference appearing early cannot crowd out a high-value profile + // fact appearing later in the same turn. Array.prototype.sort is stable, so + // candidates that tie on confidence preserve their original order. + return result + .sort((a, b) => b.confidence - a.confidence) + .slice(0, maxImplicitAdds); } export function extractTurnMemoryChanges(options: ExtractTurnMemoryOptions): ExtractedMemoryChange[] {