Graph Prune
graph_pruneRemove entities and edges with confidence below thresholds. Preview changes before destructive execution.
Instructions
Remove entities and edges that have decayed below threshold. DESTRUCTIVE — always preview first. Requires user confirmation before execute mode.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| mode | No | preview (default) or execute | preview |
| node_threshold | No | Prune nodes below this confidence (default: 0.1) | |
| edge_threshold | No | Prune edges below this weight (default: 0.05) | |
| include_orphans | No | Also prune orphaned nodes (default: true) | |
| max_age_days | No | Max age for orphan pruning (default: 30) |
Implementation Reference
- src/mcp-server/index.ts:568-594 (handler)MCP tool registration and handler for graph_prune. Delegates to client.prune() with mode (preview/execute), node_threshold, edge_threshold, include_orphans, and max_age_days parameters.
// ─── Tool: graph_prune ─── server.registerTool("graph_prune", { title: "Graph Prune", description: "Remove entities and edges that have decayed below threshold. DESTRUCTIVE — always preview first. Requires user confirmation before execute mode.", inputSchema: { mode: z.enum(["preview", "execute"]).optional().default("preview").describe("preview (default) or execute"), node_threshold: z.number().optional().default(0.1).describe("Prune nodes below this confidence (default: 0.1)"), edge_threshold: z.number().optional().default(0.05).describe("Prune edges below this weight (default: 0.05)"), include_orphans: z.boolean().optional().default(true).describe("Also prune orphaned nodes (default: true)"), max_age_days: z.number().optional().default(30).describe("Max age for orphan pruning (default: 30)"), }, annotations: { destructiveHint: true }, }, async (args) => { try { const result = await client.prune(currentTenant(), args.mode ?? "preview", { node_threshold: args.node_threshold, edge_threshold: args.edge_threshold, include_orphans: args.include_orphans, max_age_days: args.max_age_days, }); return toolResult(result); } catch (err) { return toolError(`graph_prune failed: ${err instanceof Error ? err.message : String(err)}`); } }); - src/mcp-server/index.ts:574-580 (schema)Input schema for graph_prune tool: mode (preview/execute), node_threshold, edge_threshold, include_orphans, max_age_days.
inputSchema: { mode: z.enum(["preview", "execute"]).optional().default("preview").describe("preview (default) or execute"), node_threshold: z.number().optional().default(0.1).describe("Prune nodes below this confidence (default: 0.1)"), edge_threshold: z.number().optional().default(0.05).describe("Prune edges below this weight (default: 0.05)"), include_orphans: z.boolean().optional().default(true).describe("Also prune orphaned nodes (default: true)"), max_age_days: z.number().optional().default(30).describe("Max age for orphan pruning (default: 30)"), }, - src/shared/neo4j-client.ts:1175-1284 (handler)Core prune() method in Neo4jClient. Finds nodes below confidence threshold with weak/no edges, optionally finds old orphans, and finds edges below weight threshold. In preview mode returns counts; in execute mode deletes edges then nodes via DETACH DELETE.
async prune( tenantId: string, mode: "preview" | "execute" = "preview", options: { node_threshold?: number; edge_threshold?: number; include_orphans?: boolean; max_age_days?: number; } = {}, ): Promise<{ mode: string; nodes_pruned: number; edges_pruned: number; details: Array<{ action: string; id?: string; type?: string; from?: string; to?: string }>; }> { const config = getConfig(); const nodeThreshold = options.node_threshold ?? config.decay.prune_node_threshold; const edgeThreshold = options.edge_threshold ?? config.decay.prune_edge_threshold; const includeOrphans = options.include_orphans ?? true; const maxAgeDays = options.max_age_days ?? config.decay.prune_orphan_days; // Find pruneable nodes (tenant-scoped) const nodeRows = await this.run( ` MATCH (n:Entity {tenant_id: $tenantId}) WHERE n.confidence < $nodeThreshold OPTIONAL MATCH (n)-[r]-(other:Entity {tenant_id: $tenantId}) WITH n, labels(n) AS labels, max(r.weight) AS maxEdge WHERE maxEdge IS NULL OR maxEdge < $edgeThreshold RETURN n.id AS id, n.name AS name, [l IN labels WHERE l <> 'Entity'][0] AS type, n.confidence AS confidence `, { tenantId, nodeThreshold, edgeThreshold }, ); // Find orphans if requested let orphanRows: Row[] = []; if (includeOrphans) { orphanRows = await this.run( ` MATCH (n:Entity {tenant_id: $tenantId}) WHERE NOT (n)-[]-() AND n.last_seen < datetime() - duration({days: $maxAgeDays}) AND n.confidence >= $nodeThreshold RETURN n.id AS id, n.name AS name, [l IN labels(n) WHERE l <> 'Entity'][0] AS type, n.confidence AS confidence `, { tenantId, maxAgeDays, nodeThreshold }, ); } // Find pruneable edges (both endpoints in tenant) const edgeRows = await this.run( ` MATCH (a:Entity {tenant_id: $tenantId})-[r]->(b:Entity {tenant_id: $tenantId}) WHERE r.weight < $edgeThreshold RETURN a.id AS fromId, b.id AS toId, type(r) AS relType, r.weight AS weight `, { tenantId, edgeThreshold }, ); const allNodeRows = [...nodeRows, ...orphanRows]; if (mode === "preview") { const details: Array<{ action: string; id?: string; type?: string; from?: string; to?: string }> = []; for (const row of allNodeRows) { details.push({ action: "would_delete_node", id: String(row["id"]), type: String(row["type"]) }); } for (const row of edgeRows) { details.push({ action: "would_delete_edge", from: String(row["fromId"]), to: String(row["toId"]), type: String(row["relType"]) }); } return { mode: "preview", nodes_pruned: allNodeRows.length, edges_pruned: edgeRows.length, details }; } // Execute mode — actually delete (tenant-scoped) const details: Array<{ action: string; id?: string; type?: string; from?: string; to?: string }> = []; // Delete edges first const edgeDeleteRows = await this.run( ` MATCH (a:Entity {tenant_id: $tenantId})-[r]->(b:Entity {tenant_id: $tenantId}) WHERE r.weight < $edgeThreshold DELETE r RETURN count(r) AS deleted `, { tenantId, edgeThreshold }, ); // Delete nodes (tenant-scoped) const nodeIds = allNodeRows.map((r) => String(r["id"])); let nodesDeleted = 0; if (nodeIds.length > 0) { const nodeDeleteRows = await this.run( ` MATCH (n:Entity {tenant_id: $tenantId}) WHERE n.id IN $nodeIds DETACH DELETE n RETURN count(n) AS deleted `, { tenantId, nodeIds }, ); nodesDeleted = Number(nodeDeleteRows[0]?.["deleted"] ?? 0); } const edgesDeleted = Number(edgeDeleteRows[0]?.["deleted"] ?? 0); return { mode: "executed", nodes_pruned: nodesDeleted, edges_pruned: edgesDeleted, details }; } - src/mcp-server/index.ts:570-594 (registration)Registration of the graph_prune tool via the MCP server.
server.registerTool("graph_prune", { title: "Graph Prune", description: "Remove entities and edges that have decayed below threshold. DESTRUCTIVE — always preview first. Requires user confirmation before execute mode.", inputSchema: { mode: z.enum(["preview", "execute"]).optional().default("preview").describe("preview (default) or execute"), node_threshold: z.number().optional().default(0.1).describe("Prune nodes below this confidence (default: 0.1)"), edge_threshold: z.number().optional().default(0.05).describe("Prune edges below this weight (default: 0.05)"), include_orphans: z.boolean().optional().default(true).describe("Also prune orphaned nodes (default: true)"), max_age_days: z.number().optional().default(30).describe("Max age for orphan pruning (default: 30)"), }, annotations: { destructiveHint: true }, }, async (args) => { try { const result = await client.prune(currentTenant(), args.mode ?? "preview", { node_threshold: args.node_threshold, edge_threshold: args.edge_threshold, include_orphans: args.include_orphans, max_age_days: args.max_age_days, }); return toolResult(result); } catch (err) { return toolError(`graph_prune failed: ${err instanceof Error ? err.message : String(err)}`); } }); - src/shared/config.ts:26-77 (helper)Default configuration values for prune thresholds used when not overridden by tool arguments.
prune_node_threshold: number; prune_edge_threshold: number; prune_orphan_days: number; }; query: { default_max_hops: number; default_min_weight: number; default_limit: number; cypher_timeout_ms: number; }; affinity: { hop_1_multiplier: number; hop_2_multiplier: number; }; dream: { cooldown_hours: number; max_transcripts_per_run: number; chunk_size_lines: number; }; } const DEFAULTS: GraphMemoryConfig = { neo4j: { uri: process.env.NEO4J_URI ?? "", user: process.env.NEO4J_USER ?? "neo4j", password: process.env.NEO4J_PASSWORD ?? "", database: process.env.NEO4J_DATABASE ?? "neo4j", }, weights: { explicit_statement: 0.7, inferred: 0.3, from_memory_file: 0.5, boost_on_confirm: 0.15, boost_on_mention: 0.05, weaken_on_correct: 0.3, project_context_boost: 0.1, }, decay: { rates: { Person: 0.998, Project: 0.995, Preference: 0.999, Concept: 0.999, Decision: 0.997, Fact: 0.996, Event: 0.993, Object: 0.996, }, edge_rate: 0.997, prune_node_threshold: 0.1, prune_edge_threshold: 0.05, prune_orphan_days: 30,