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
47 changes: 41 additions & 6 deletions src/memory/__tests__/knowledge-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,20 @@ describe('KnowledgeGraphManager', () => {
expect(result.entities[0].name).toBe('Alice');
});

it('should include relations between matched entities', async () => {
it('should include relations where at least one endpoint matches', async () => {
const result = await manager.searchNodes('Acme');
expect(result.entities).toHaveLength(2); // Alice and Acme Corp
expect(result.relations).toHaveLength(1); // Only Alice -> Acme Corp relation
// Both relations included: Alice → Acme Corp (Alice matched) and Bob → Acme Corp (Acme Corp matched)
expect(result.relations).toHaveLength(2);
});

it('should include outgoing relations to unmatched entities', async () => {
const result = await manager.searchNodes('Alice');
expect(result.entities).toHaveLength(1);
// Alice → Acme Corp relation included because Alice is the source
expect(result.relations).toHaveLength(1);
expect(result.relations[0].from).toBe('Alice');
expect(result.relations[0].to).toBe('Acme Corp');
});

it('should return empty graph for no matches', async () => {
Expand Down Expand Up @@ -336,16 +346,41 @@ describe('KnowledgeGraphManager', () => {
expect(result.entities.map(e => e.name)).toContain('Bob');
});

it('should include relations between opened nodes', async () => {
it('should include all relations connected to opened nodes', async () => {
const result = await manager.openNodes(['Alice', 'Bob']);
// Alice → Bob (both endpoints opened) and Bob → Charlie (Bob is opened)
expect(result.relations).toHaveLength(2);
expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob')).toBe(true);
expect(result.relations.some(r => r.from === 'Bob' && r.to === 'Charlie')).toBe(true);
});

it('should include relations connected to opened nodes', async () => {
const result = await manager.openNodes(['Bob']);
// Bob has two relations: Alice → Bob and Bob → Charlie
expect(result.relations).toHaveLength(2);
expect(result.relations.some(r => r.from === 'Alice' && r.to === 'Bob')).toBe(true);
expect(result.relations.some(r => r.from === 'Bob' && r.to === 'Charlie')).toBe(true);
});

it('should include outgoing relations to nodes not in the open set', async () => {
// This is the core bug fix for #3137: open_nodes should return
// relations FROM the opened node, even if the target is not opened
const result = await manager.openNodes(['Alice']);
expect(result.entities).toHaveLength(1);
expect(result.entities[0].name).toBe('Alice');
// Alice → Bob relation is included because Alice is opened
expect(result.relations).toHaveLength(1);
expect(result.relations[0].from).toBe('Alice');
expect(result.relations[0].to).toBe('Bob');
});

it('should exclude relations to unopened nodes', async () => {
const result = await manager.openNodes(['Bob']);
expect(result.relations).toHaveLength(0);
it('should include incoming relations from nodes not in the open set', async () => {
const result = await manager.openNodes(['Charlie']);
expect(result.entities).toHaveLength(1);
// Bob → Charlie relation is included because Charlie is opened
expect(result.relations).toHaveLength(1);
expect(result.relations[0].from).toBe('Bob');
expect(result.relations[0].to).toBe('Charlie');
});

it('should handle opening non-existent nodes', async () => {
Expand Down
12 changes: 8 additions & 4 deletions src/memory/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,10 @@ export class KnowledgeGraphManager {
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));

// Filter relations to only include those between filtered entities
// Include relations where at least one endpoint matches the search results.
// This lets callers discover connections to nodes outside the result set.
const filteredRelations = graph.relations.filter(r =>
filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to)
);

const filteredGraph: KnowledgeGraph = {
Expand All @@ -219,9 +220,12 @@ export class KnowledgeGraphManager {
// Create a Set of filtered entity names for quick lookup
const filteredEntityNames = new Set(filteredEntities.map(e => e.name));

// Filter relations to only include those between filtered entities
// Include relations where at least one endpoint is in the requested set.
// Previously this required BOTH endpoints, which meant relations from a
// requested node to an unrequested node were silently dropped — making it
// impossible to discover a node's connections without reading the full graph.
const filteredRelations = graph.relations.filter(r =>
filteredEntityNames.has(r.from) && filteredEntityNames.has(r.to)
filteredEntityNames.has(r.from) || filteredEntityNames.has(r.to)
);

const filteredGraph: KnowledgeGraph = {
Expand Down