twining_prune_graph
Remove disconnected entities from your knowledge graph to clean up stale data. Optionally filter by entity type for targeted pruning.
Instructions
Remove orphaned knowledge graph entities that have no relations. Use this to clean up stale or disconnected entities. Optionally filter by entity type to only prune certain kinds.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| entity_types | No | Only prune orphans of these types (e.g., ["concept", "file"]). If omitted, prunes all orphan types. | |
| dry_run | No | If true, report orphans without removing them (default: false) |
Implementation Reference
- src/engine/graph.ts:248-296 (handler)The GraphEngine.prune() method implements the core logic for the twining_prune_graph tool. It finds orphaned entities (entities with no relations), optionally filters by entity type, and either reports them (dry run) or removes them via GraphStore.removeEntities().
async prune(entityTypes?: string[], dryRun = false): Promise<{ pruned: Array<{ id: string; name: string; type: string }>; total_orphans_found: number; total_removed: number; removed_relations: number; dry_run: boolean; }> { const entities = await this.graphStore.getEntities(); const relations = await this.graphStore.getRelations(); // Find all entity IDs that participate in at least one relation const connectedIds = new Set<string>(); for (const r of relations) { connectedIds.add(r.source); connectedIds.add(r.target); } // Find orphans let orphans = entities.filter((e) => !connectedIds.has(e.id)); const total_orphans_found = orphans.length; // Filter by type if requested if (entityTypes && entityTypes.length > 0) { const typeSet = new Set(entityTypes); orphans = orphans.filter((e) => typeSet.has(e.type)); } if (orphans.length === 0 || dryRun) { return { pruned: orphans.map((e) => ({ id: e.id, name: e.name, type: e.type })), total_orphans_found, total_removed: 0, removed_relations: 0, dry_run: dryRun, }; } const orphanIds = new Set(orphans.map((e) => e.id)); const { removedEntities, removedRelations } = await this.graphStore.removeEntities(orphanIds); return { pruned: orphans.map((e) => ({ id: e.id, name: e.name, type: e.type })), total_orphans_found, total_removed: removedEntities, removed_relations: removedRelations, dry_run: false, }; } - src/tools/graph-tools.ts:184-222 (handler)The tool handler for twining_prune_graph registered with the MCP server. Accepts optional entity_types and dry_run parameters, and calls engine.prune().
// twining_prune_graph — Remove orphaned graph entities server.registerTool( "twining_prune_graph", { description: "Remove orphaned knowledge graph entities that have no relations. Use this to clean up stale or disconnected entities. Optionally filter by entity type to only prune certain kinds.", inputSchema: { entity_types: z .array(z.string()) .optional() .describe( 'Only prune orphans of these types (e.g., ["concept", "file"]). If omitted, prunes all orphan types.', ), dry_run: z .boolean() .optional() .describe( "If true, report orphans without removing them (default: false)", ), }, }, async (args) => { try { const result = await engine.prune( args.entity_types, args.dry_run ?? false, ); return toolResult(result); } catch (e) { if (e instanceof TwiningError) { return toolError(e.message, e.code); } return toolError( e instanceof Error ? e.message : "Unknown error", "INTERNAL_ERROR", ); } }, ); - src/tools/graph-tools.ts:190-203 (schema)Input schema for twining_prune_graph: optional entity_types array and dry_run boolean.
inputSchema: { entity_types: z .array(z.string()) .optional() .describe( 'Only prune orphans of these types (e.g., ["concept", "file"]). If omitted, prunes all orphan types.', ), dry_run: z .boolean() .optional() .describe( "If true, report orphans without removing them (default: false)", ), }, - src/tools/graph-tools.ts:184-222 (registration)Registration of twining_prune_graph via server.registerTool() inside the registerGraphTools function.
// twining_prune_graph — Remove orphaned graph entities server.registerTool( "twining_prune_graph", { description: "Remove orphaned knowledge graph entities that have no relations. Use this to clean up stale or disconnected entities. Optionally filter by entity type to only prune certain kinds.", inputSchema: { entity_types: z .array(z.string()) .optional() .describe( 'Only prune orphans of these types (e.g., ["concept", "file"]). If omitted, prunes all orphan types.', ), dry_run: z .boolean() .optional() .describe( "If true, report orphans without removing them (default: false)", ), }, }, async (args) => { try { const result = await engine.prune( args.entity_types, args.dry_run ?? false, ); return toolResult(result); } catch (e) { if (e instanceof TwiningError) { return toolError(e.message, e.code); } return toolError( e instanceof Error ? e.message : "Unknown error", "INTERNAL_ERROR", ); } }, ); - src/storage/graph-store.ts:205-255 (helper)GraphStore.removeEntities() performs the actual deletion of orphan entities and their relations from storage files with file locking.
async removeEntities( entityIds: Set<string>, ): Promise<{ removedEntities: number; removedRelations: number }> { this.ensureFiles(); let removedEntities = 0; let removedRelations = 0; // Lock both files — entities first, then relations const releaseEntities = await lockfile.lock( this.entitiesPath, LOCK_OPTIONS, ); try { const entities = JSON.parse( fs.readFileSync(this.entitiesPath, "utf-8"), ) as Entity[]; const before = entities.length; const kept = entities.filter((e) => !entityIds.has(e.id)); removedEntities = before - kept.length; fs.writeFileSync( this.entitiesPath, JSON.stringify(kept, null, 2), ); } finally { await releaseEntities(); } const releaseRelations = await lockfile.lock( this.relationsPath, LOCK_OPTIONS, ); try { const relations = JSON.parse( fs.readFileSync(this.relationsPath, "utf-8"), ) as Relation[]; const before = relations.length; const kept = relations.filter( (r) => !entityIds.has(r.source) && !entityIds.has(r.target), ); removedRelations = before - kept.length; fs.writeFileSync( this.relationsPath, JSON.stringify(kept, null, 2), ); } finally { await releaseRelations(); } return { removedEntities, removedRelations }; }