fix(memory): skip duplicate entities and relations within a single batch (#4383)

create_entities and create_relations only de-duplicated against the existing
graph, so passing the same entity name (or identical relation) twice in one
call persisted duplicate records. This contradicts the documented behavior
("Ignores entities with existing names" / "Skips duplicate relations") and
breaks the implicit name-uniqueness invariant the rest of the manager relies
on (e.g. addObservations/deleteEntities key on name).

De-duplicate within the input batch as well, keeping the first occurrence.

Co-authored-by: JSap0914 <JSap0914@users.noreply.github.com>
Co-authored-by: olaservo <olahungerford@gmail.com>
This commit is contained in:
JSap0914 2026-09-03 10:38:22 +09:00 • committed by GitHub
commit 1ec570c27d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 6 deletions

View file

@ -59,6 +59,20 @@ describe('KnowledgeGraphManager', () => {
const newEntities = await manager.createEntities([]);
expect(newEntities).toHaveLength(0);
});
it('should ignore duplicate entity names within a single batch', async () => {
const entities: Entity[] = [
{ name: 'Alice', entityType: 'person', observations: ['first'] },
{ name: 'Alice', entityType: 'person', observations: ['second'] },
];
const newEntities = await manager.createEntities(entities);
expect(newEntities).toHaveLength(1);
const graph = await manager.readGraph();
expect(graph.entities).toHaveLength(1);
expect(graph.entities[0].name).toBe('Alice');
});
});
describe('createRelations', () => {
@ -135,6 +149,24 @@ describe('KnowledgeGraphManager', () => {
const newRelations = await manager.createRelations([]);
expect(newRelations).toHaveLength(0);
});
it('should skip duplicate relations within a single batch', async () => {
await manager.createEntities([
{ name: 'Alice', entityType: 'person', observations: [] },
{ name: 'Bob', entityType: 'person', observations: [] },
]);
const relations: Relation[] = [
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
];
const newRelations = await manager.createRelations(relations);
expect(newRelations).toHaveLength(1);
const graph = await manager.readGraph();
expect(graph.relations).toHaveLength(1);
});
});
describe('addObservations', () => {

View file

@ -181,7 +181,11 @@ export class KnowledgeGraphManager {
async createEntities(entities: Entity[]): Promise<Entity[]> {
const graph = await this.loadGraph();
const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
const newEntities = entities.filter((e, index) =>
!graph.entities.some(existingEntity => existingEntity.name === e.name) &&
// Also skip duplicates appearing earlier in this same batch
!entities.slice(0, index).some(earlier => earlier.name === e.name)
);
graph.entities.push(...newEntities);
await this.saveGraph(graph);
return newEntities;
@ -200,11 +204,15 @@ export class KnowledgeGraphManager {
}
});
const newRelations = relations.filter(r => !graph.relations.some(existingRelation =>
existingRelation.from === r.from &&
existingRelation.to === r.to &&
existingRelation.relationType === r.relationType
));
const isSameRelation = (a: Relation, b: Relation) =>
a.from === b.from &&
a.to === b.to &&
a.relationType === b.relationType;
const newRelations = relations.filter((r, index) =>
!graph.relations.some(existingRelation => isSameRelation(existingRelation, r)) &&
// Also skip duplicates appearing earlier in this same batch
!relations.slice(0, index).some(earlier => isSameRelation(earlier, r))
);
graph.relations.push(...newRelations);
await this.saveGraph(graph);
return newRelations;