mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +08:00
refactor: Replace knowledge graph with session clustering system
Remove legacy knowledge graph and evolution tracking features, introduce new session clustering model for Core Memory. Changes: - Remove knowledge_graph, knowledge_graph_edges, evolution_history tables - Add session_clusters, cluster_members, cluster_relations tables - Add session_metadata_cache for metadata caching - Add new interfaces: SessionCluster, ClusterMember, ClusterRelation, SessionMetadataCache - Add CRUD methods for cluster management - Add session metadata upsert and search methods - Remove extractKnowledgeGraph, getKnowledgeGraph, trackEvolution methods - Remove API routes for /knowledge-graph, /evolution, /graph - Add database migration to clean up old tables - Update generateClusterId() for CLST-* ID format Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -71,9 +71,6 @@ async function importAction(text: string): Promise<void> {
|
|||||||
|
|
||||||
console.log(chalk.green(`✓ Created memory: ${memory.id}`));
|
console.log(chalk.green(`✓ Created memory: ${memory.id}`));
|
||||||
|
|
||||||
// Extract knowledge graph
|
|
||||||
store.extractKnowledgeGraph(memory.id);
|
|
||||||
|
|
||||||
// Notify dashboard
|
// Notify dashboard
|
||||||
notifyRefreshRequired('memory').catch(() => { /* ignore */ });
|
notifyRefreshRequired('memory').catch(() => { /* ignore */ });
|
||||||
|
|
||||||
|
|||||||
@@ -20,43 +20,44 @@ export interface CoreMemory {
|
|||||||
metadata?: string; // JSON string
|
metadata?: string; // JSON string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KnowledgeGraphNode {
|
export interface SessionCluster {
|
||||||
memory_id: string;
|
id: string; // Format: CLST-YYYYMMDD-HHMMSS
|
||||||
node_id: string;
|
name: string;
|
||||||
node_type: string; // file, function, module, concept
|
description?: string;
|
||||||
node_label: string;
|
intent?: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
status: 'active' | 'archived' | 'merged';
|
||||||
|
metadata?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KnowledgeGraphEdge {
|
export interface ClusterMember {
|
||||||
memory_id: string;
|
cluster_id: string;
|
||||||
edge_source: string;
|
session_id: string;
|
||||||
edge_target: string;
|
session_type: 'core_memory' | 'workflow' | 'cli_history' | 'native';
|
||||||
edge_type: string; // depends_on, implements, uses, relates_to
|
sequence_order: number;
|
||||||
|
added_at: string;
|
||||||
|
relevance_score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EvolutionVersion {
|
export interface ClusterRelation {
|
||||||
memory_id: string;
|
source_cluster_id: string;
|
||||||
version: number;
|
target_cluster_id: string;
|
||||||
content: string;
|
relation_type: 'depends_on' | 'extends' | 'conflicts_with' | 'related_to';
|
||||||
timestamp: string;
|
created_at: string;
|
||||||
diff_stats?: {
|
|
||||||
added: number;
|
|
||||||
modified: number;
|
|
||||||
deleted: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KnowledgeGraph {
|
export interface SessionMetadataCache {
|
||||||
nodes: Array<{
|
session_id: string;
|
||||||
id: string;
|
session_type: string;
|
||||||
type: string;
|
title?: string;
|
||||||
label: string;
|
summary?: string;
|
||||||
}>;
|
keywords?: string[]; // stored as JSON
|
||||||
edges: Array<{
|
token_estimate?: number;
|
||||||
source: string;
|
file_patterns?: string[]; // stored as JSON
|
||||||
target: string;
|
created_at?: string;
|
||||||
type: string;
|
last_accessed?: string;
|
||||||
}>;
|
access_count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,6 +87,9 @@ export class CoreMemoryStore {
|
|||||||
* Initialize database schema
|
* Initialize database schema
|
||||||
*/
|
*/
|
||||||
private initDatabase(): void {
|
private initDatabase(): void {
|
||||||
|
// Migrate old tables
|
||||||
|
this.migrateDatabase();
|
||||||
|
|
||||||
this.db.exec(`
|
this.db.exec(`
|
||||||
-- Core memories table
|
-- Core memories table
|
||||||
CREATE TABLE IF NOT EXISTS memories (
|
CREATE TABLE IF NOT EXISTS memories (
|
||||||
@@ -99,49 +103,82 @@ export class CoreMemoryStore {
|
|||||||
metadata TEXT
|
metadata TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Knowledge graph nodes table
|
-- Session clusters table
|
||||||
CREATE TABLE IF NOT EXISTS knowledge_graph (
|
CREATE TABLE IF NOT EXISTS session_clusters (
|
||||||
memory_id TEXT NOT NULL,
|
id TEXT PRIMARY KEY,
|
||||||
node_id TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
node_type TEXT NOT NULL,
|
description TEXT,
|
||||||
node_label TEXT NOT NULL,
|
intent TEXT,
|
||||||
PRIMARY KEY (memory_id, node_id),
|
created_at TEXT NOT NULL,
|
||||||
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
updated_at TEXT NOT NULL,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
metadata TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Knowledge graph edges table
|
-- Cluster members table
|
||||||
CREATE TABLE IF NOT EXISTS knowledge_graph_edges (
|
CREATE TABLE IF NOT EXISTS cluster_members (
|
||||||
memory_id TEXT NOT NULL,
|
cluster_id TEXT NOT NULL,
|
||||||
edge_source TEXT NOT NULL,
|
session_id TEXT NOT NULL,
|
||||||
edge_target TEXT NOT NULL,
|
session_type TEXT NOT NULL,
|
||||||
edge_type TEXT NOT NULL,
|
sequence_order INTEGER NOT NULL,
|
||||||
PRIMARY KEY (memory_id, edge_source, edge_target),
|
added_at TEXT NOT NULL,
|
||||||
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
relevance_score REAL DEFAULT 1.0,
|
||||||
|
PRIMARY KEY (cluster_id, session_id),
|
||||||
|
FOREIGN KEY (cluster_id) REFERENCES session_clusters(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Evolution history table
|
-- Cluster relations table
|
||||||
CREATE TABLE IF NOT EXISTS evolution_history (
|
CREATE TABLE IF NOT EXISTS cluster_relations (
|
||||||
memory_id TEXT NOT NULL,
|
source_cluster_id TEXT NOT NULL,
|
||||||
version INTEGER NOT NULL,
|
target_cluster_id TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
relation_type TEXT NOT NULL,
|
||||||
timestamp TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
diff_stats TEXT,
|
PRIMARY KEY (source_cluster_id, target_cluster_id),
|
||||||
PRIMARY KEY (memory_id, version),
|
FOREIGN KEY (source_cluster_id) REFERENCES session_clusters(id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
|
FOREIGN KEY (target_cluster_id) REFERENCES session_clusters(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Session metadata cache table
|
||||||
|
CREATE TABLE IF NOT EXISTS session_metadata_cache (
|
||||||
|
session_id TEXT PRIMARY KEY,
|
||||||
|
session_type TEXT NOT NULL,
|
||||||
|
title TEXT,
|
||||||
|
summary TEXT,
|
||||||
|
keywords TEXT,
|
||||||
|
token_estimate INTEGER,
|
||||||
|
file_patterns TEXT,
|
||||||
|
created_at TEXT,
|
||||||
|
last_accessed TEXT,
|
||||||
|
access_count INTEGER DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Indexes for efficient queries
|
-- Indexes for efficient queries
|
||||||
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_memories_created ON memories(created_at DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at DESC);
|
||||||
CREATE INDEX IF NOT EXISTS idx_memories_archived ON memories(archived);
|
CREATE INDEX IF NOT EXISTS idx_memories_archived ON memories(archived);
|
||||||
CREATE INDEX IF NOT EXISTS idx_knowledge_graph_memory ON knowledge_graph(memory_id);
|
CREATE INDEX IF NOT EXISTS idx_session_clusters_status ON session_clusters(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_knowledge_graph_edges_memory ON knowledge_graph_edges(memory_id);
|
CREATE INDEX IF NOT EXISTS idx_cluster_members_cluster ON cluster_members(cluster_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_evolution_history_memory ON evolution_history(memory_id);
|
CREATE INDEX IF NOT EXISTS idx_cluster_members_session ON cluster_members(session_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_session_metadata_type ON session_metadata_cache(session_type);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate timestamp-based ID
|
* Migrate database by removing old tables
|
||||||
|
*/
|
||||||
|
private migrateDatabase(): void {
|
||||||
|
const oldTables = ['knowledge_graph', 'knowledge_graph_edges', 'evolution_history'];
|
||||||
|
for (const table of oldTables) {
|
||||||
|
try {
|
||||||
|
this.db.exec(`DROP TABLE IF EXISTS ${table}`);
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if table doesn't exist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate timestamp-based ID for core memory
|
||||||
*/
|
*/
|
||||||
private generateId(): string {
|
private generateId(): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
@@ -154,6 +191,20 @@ export class CoreMemoryStore {
|
|||||||
return `CMEM-${year}${month}${day}-${hours}${minutes}${seconds}`;
|
return `CMEM-${year}${month}${day}-${hours}${minutes}${seconds}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate cluster ID
|
||||||
|
*/
|
||||||
|
generateClusterId(): string {
|
||||||
|
const now = new Date();
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(now.getDate()).padStart(2, '0');
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
return `CLST-${year}${month}${day}-${hours}${minutes}${seconds}`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Upsert a core memory
|
* Upsert a core memory
|
||||||
*/
|
*/
|
||||||
@@ -182,10 +233,6 @@ export class CoreMemoryStore {
|
|||||||
id
|
id
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add evolution history entry
|
|
||||||
const currentVersion = this.getLatestVersion(id);
|
|
||||||
this.addEvolutionVersion(id, currentVersion + 1, memory.content);
|
|
||||||
|
|
||||||
return this.getMemory(id)!;
|
return this.getMemory(id)!;
|
||||||
} else {
|
} else {
|
||||||
// Insert new memory
|
// Insert new memory
|
||||||
@@ -205,9 +252,6 @@ export class CoreMemoryStore {
|
|||||||
memory.metadata || null
|
memory.metadata || null
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add initial evolution history entry (version 1)
|
|
||||||
this.addEvolutionVersion(id, 1, memory.content);
|
|
||||||
|
|
||||||
return this.getMemory(id)!;
|
return this.getMemory(id)!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -318,204 +362,357 @@ ${memory.content}
|
|||||||
`);
|
`);
|
||||||
stmt.run(summary, new Date().toISOString(), memoryId);
|
stmt.run(summary, new Date().toISOString(), memoryId);
|
||||||
|
|
||||||
// Add evolution history entry
|
|
||||||
const currentVersion = this.getLatestVersion(memoryId);
|
|
||||||
this.addEvolutionVersion(memoryId, currentVersion + 1, memory.content);
|
|
||||||
|
|
||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract knowledge graph from memory content
|
* Create a new session cluster
|
||||||
*/
|
*/
|
||||||
extractKnowledgeGraph(memoryId: string): KnowledgeGraph {
|
createCluster(cluster: Partial<SessionCluster> & { name: string }): SessionCluster {
|
||||||
const memory = this.getMemory(memoryId);
|
const now = new Date().toISOString();
|
||||||
if (!memory) throw new Error('Memory not found');
|
const id = cluster.id || this.generateClusterId();
|
||||||
|
|
||||||
// Simple extraction based on patterns in content
|
const stmt = this.db.prepare(`
|
||||||
const nodes: KnowledgeGraph['nodes'] = [];
|
INSERT INTO session_clusters (id, name, description, intent, created_at, updated_at, status, metadata)
|
||||||
const edges: KnowledgeGraph['edges'] = [];
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
const nodeSet = new Set<string>();
|
|
||||||
|
|
||||||
// Extract file references
|
|
||||||
const filePattern = /(?:file|path|module):\s*([^\s,]+(?:\.ts|\.js|\.py|\.go|\.java|\.rs))/gi;
|
|
||||||
let match;
|
|
||||||
while ((match = filePattern.exec(memory.content)) !== null) {
|
|
||||||
const filePath = match[1];
|
|
||||||
if (!nodeSet.has(filePath)) {
|
|
||||||
nodes.push({ id: filePath, type: 'file', label: filePath.split('/').pop() || filePath });
|
|
||||||
nodeSet.add(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract function/class references
|
|
||||||
const functionPattern = /(?:function|class|method):\s*(\w+)/gi;
|
|
||||||
while ((match = functionPattern.exec(memory.content)) !== null) {
|
|
||||||
const funcName = match[1];
|
|
||||||
const nodeId = `func:${funcName}`;
|
|
||||||
if (!nodeSet.has(nodeId)) {
|
|
||||||
nodes.push({ id: nodeId, type: 'function', label: funcName });
|
|
||||||
nodeSet.add(nodeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract module references
|
|
||||||
const modulePattern = /(?:module|package):\s*(\w+(?:\/\w+)*)/gi;
|
|
||||||
while ((match = modulePattern.exec(memory.content)) !== null) {
|
|
||||||
const moduleName = match[1];
|
|
||||||
const nodeId = `module:${moduleName}`;
|
|
||||||
if (!nodeSet.has(nodeId)) {
|
|
||||||
nodes.push({ id: nodeId, type: 'module', label: moduleName });
|
|
||||||
nodeSet.add(nodeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract relationships
|
|
||||||
const dependsPattern = /(\w+)\s+depends on\s+(\w+)/gi;
|
|
||||||
while ((match = dependsPattern.exec(memory.content)) !== null) {
|
|
||||||
const source = match[1];
|
|
||||||
const target = match[2];
|
|
||||||
edges.push({ source, target, type: 'depends_on' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const usesPattern = /(\w+)\s+uses\s+(\w+)/gi;
|
|
||||||
while ((match = usesPattern.exec(memory.content)) !== null) {
|
|
||||||
const source = match[1];
|
|
||||||
const target = match[2];
|
|
||||||
edges.push({ source, target, type: 'uses' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to database
|
|
||||||
this.db.prepare(`DELETE FROM knowledge_graph WHERE memory_id = ?`).run(memoryId);
|
|
||||||
this.db.prepare(`DELETE FROM knowledge_graph_edges WHERE memory_id = ?`).run(memoryId);
|
|
||||||
|
|
||||||
const nodeStmt = this.db.prepare(`
|
|
||||||
INSERT INTO knowledge_graph (memory_id, node_id, node_type, node_label)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
`);
|
`);
|
||||||
|
|
||||||
for (const node of nodes) {
|
stmt.run(
|
||||||
nodeStmt.run(memoryId, node.id, node.type, node.label);
|
id,
|
||||||
}
|
cluster.name,
|
||||||
|
cluster.description || null,
|
||||||
|
cluster.intent || null,
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
cluster.status || 'active',
|
||||||
|
cluster.metadata || null
|
||||||
|
);
|
||||||
|
|
||||||
const edgeStmt = this.db.prepare(`
|
return this.getCluster(id)!;
|
||||||
INSERT INTO knowledge_graph_edges (memory_id, edge_source, edge_target, edge_type)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
`);
|
|
||||||
|
|
||||||
for (const edge of edges) {
|
|
||||||
edgeStmt.run(memoryId, edge.source, edge.target, edge.type);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { nodes, edges };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get knowledge graph for a memory
|
* Get cluster by ID
|
||||||
*/
|
*/
|
||||||
getKnowledgeGraph(memoryId: string): KnowledgeGraph {
|
getCluster(id: string): SessionCluster | null {
|
||||||
const nodeStmt = this.db.prepare(`
|
const stmt = this.db.prepare(`SELECT * FROM session_clusters WHERE id = ?`);
|
||||||
SELECT node_id, node_type, node_label
|
const row = stmt.get(id) as any;
|
||||||
FROM knowledge_graph
|
if (!row) return null;
|
||||||
WHERE memory_id = ?
|
|
||||||
`);
|
|
||||||
|
|
||||||
const edgeStmt = this.db.prepare(`
|
return {
|
||||||
SELECT edge_source, edge_target, edge_type
|
id: row.id,
|
||||||
FROM knowledge_graph_edges
|
name: row.name,
|
||||||
WHERE memory_id = ?
|
description: row.description,
|
||||||
`);
|
intent: row.intent,
|
||||||
|
created_at: row.created_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
status: row.status,
|
||||||
|
metadata: row.metadata
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const nodeRows = nodeStmt.all(memoryId) as any[];
|
/**
|
||||||
const edgeRows = edgeStmt.all(memoryId) as any[];
|
* List all clusters
|
||||||
|
*/
|
||||||
|
listClusters(status?: string): SessionCluster[] {
|
||||||
|
let query = 'SELECT * FROM session_clusters';
|
||||||
|
const params: any[] = [];
|
||||||
|
|
||||||
const nodes = nodeRows.map(row => ({
|
if (status) {
|
||||||
id: row.node_id,
|
query += ' WHERE status = ?';
|
||||||
type: row.node_type,
|
params.push(status);
|
||||||
label: row.node_label
|
}
|
||||||
|
|
||||||
|
query += ' ORDER BY updated_at DESC';
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(query);
|
||||||
|
const rows = stmt.all(...params) as any[];
|
||||||
|
|
||||||
|
return rows.map(row => ({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
intent: row.intent,
|
||||||
|
created_at: row.created_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
status: row.status,
|
||||||
|
metadata: row.metadata
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const edges = edgeRows.map(row => ({
|
|
||||||
source: row.edge_source,
|
|
||||||
target: row.edge_target,
|
|
||||||
type: row.edge_type
|
|
||||||
}));
|
|
||||||
|
|
||||||
return { nodes, edges };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get latest version number for a memory
|
* Update cluster
|
||||||
*/
|
*/
|
||||||
private getLatestVersion(memoryId: string): number {
|
updateCluster(id: string, updates: Partial<SessionCluster>): SessionCluster | null {
|
||||||
|
const existing = this.getCluster(id);
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT MAX(version) as max_version
|
UPDATE session_clusters
|
||||||
FROM evolution_history
|
SET name = ?, description = ?, intent = ?, updated_at = ?, status = ?, metadata = ?
|
||||||
WHERE memory_id = ?
|
WHERE id = ?
|
||||||
`);
|
`);
|
||||||
const result = stmt.get(memoryId) as { max_version: number | null };
|
|
||||||
return result.max_version || 0;
|
stmt.run(
|
||||||
|
updates.name || existing.name,
|
||||||
|
updates.description !== undefined ? updates.description : existing.description,
|
||||||
|
updates.intent !== undefined ? updates.intent : existing.intent,
|
||||||
|
now,
|
||||||
|
updates.status || existing.status,
|
||||||
|
updates.metadata !== undefined ? updates.metadata : existing.metadata,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.getCluster(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add evolution version
|
* Delete cluster
|
||||||
*/
|
*/
|
||||||
private addEvolutionVersion(memoryId: string, version: number, content: string): void {
|
deleteCluster(id: string): boolean {
|
||||||
|
const stmt = this.db.prepare(`DELETE FROM session_clusters WHERE id = ?`);
|
||||||
|
const result = stmt.run(id);
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add member to cluster
|
||||||
|
*/
|
||||||
|
addClusterMember(member: Omit<ClusterMember, 'added_at'>): ClusterMember {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
INSERT INTO evolution_history (memory_id, version, content, timestamp)
|
INSERT INTO cluster_members (cluster_id, session_id, session_type, sequence_order, added_at, relevance_score)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
|
stmt.run(
|
||||||
|
member.cluster_id,
|
||||||
|
member.session_id,
|
||||||
|
member.session_type,
|
||||||
|
member.sequence_order,
|
||||||
|
now,
|
||||||
|
member.relevance_score
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...member,
|
||||||
|
added_at: now
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove member from cluster
|
||||||
|
*/
|
||||||
|
removeClusterMember(clusterId: string, sessionId: string): boolean {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
DELETE FROM cluster_members
|
||||||
|
WHERE cluster_id = ? AND session_id = ?
|
||||||
|
`);
|
||||||
|
const result = stmt.run(clusterId, sessionId);
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all members of a cluster
|
||||||
|
*/
|
||||||
|
getClusterMembers(clusterId: string): ClusterMember[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT * FROM cluster_members
|
||||||
|
WHERE cluster_id = ?
|
||||||
|
ORDER BY sequence_order ASC
|
||||||
|
`);
|
||||||
|
|
||||||
|
const rows = stmt.all(clusterId) as any[];
|
||||||
|
return rows.map(row => ({
|
||||||
|
cluster_id: row.cluster_id,
|
||||||
|
session_id: row.session_id,
|
||||||
|
session_type: row.session_type,
|
||||||
|
sequence_order: row.sequence_order,
|
||||||
|
added_at: row.added_at,
|
||||||
|
relevance_score: row.relevance_score
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all clusters that contain a session
|
||||||
|
*/
|
||||||
|
getSessionClusters(sessionId: string): SessionCluster[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT sc.*
|
||||||
|
FROM session_clusters sc
|
||||||
|
INNER JOIN cluster_members cm ON sc.id = cm.cluster_id
|
||||||
|
WHERE cm.session_id = ?
|
||||||
|
ORDER BY sc.updated_at DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
const rows = stmt.all(sessionId) as any[];
|
||||||
|
return rows.map(row => ({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
intent: row.intent,
|
||||||
|
created_at: row.created_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
status: row.status,
|
||||||
|
metadata: row.metadata
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add relation between clusters
|
||||||
|
*/
|
||||||
|
addClusterRelation(relation: Omit<ClusterRelation, 'created_at'>): ClusterRelation {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
INSERT INTO cluster_relations (source_cluster_id, target_cluster_id, relation_type, created_at)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?)
|
||||||
`);
|
`);
|
||||||
stmt.run(memoryId, version, content, new Date().toISOString());
|
|
||||||
|
stmt.run(
|
||||||
|
relation.source_cluster_id,
|
||||||
|
relation.target_cluster_id,
|
||||||
|
relation.relation_type,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...relation,
|
||||||
|
created_at: now
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Track evolution history
|
* Remove relation between clusters
|
||||||
*/
|
*/
|
||||||
trackEvolution(memoryId: string): EvolutionVersion[] {
|
removeClusterRelation(sourceId: string, targetId: string): boolean {
|
||||||
const stmt = this.db.prepare(`
|
const stmt = this.db.prepare(`
|
||||||
SELECT version, content, timestamp, diff_stats
|
DELETE FROM cluster_relations
|
||||||
FROM evolution_history
|
WHERE source_cluster_id = ? AND target_cluster_id = ?
|
||||||
WHERE memory_id = ?
|
`);
|
||||||
ORDER BY version ASC
|
const result = stmt.run(sourceId, targetId);
|
||||||
|
return result.changes > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all relations for a cluster
|
||||||
|
*/
|
||||||
|
getClusterRelations(clusterId: string): ClusterRelation[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT * FROM cluster_relations
|
||||||
|
WHERE source_cluster_id = ? OR target_cluster_id = ?
|
||||||
|
ORDER BY created_at DESC
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const rows = stmt.all(memoryId) as any[];
|
const rows = stmt.all(clusterId, clusterId) as any[];
|
||||||
return rows.map((row, index) => {
|
return rows.map(row => ({
|
||||||
let diffStats: EvolutionVersion['diff_stats'];
|
source_cluster_id: row.source_cluster_id,
|
||||||
|
target_cluster_id: row.target_cluster_id,
|
||||||
|
relation_type: row.relation_type,
|
||||||
|
created_at: row.created_at
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (index > 0) {
|
/**
|
||||||
const prevContent = rows[index - 1].content;
|
* Upsert session metadata
|
||||||
const currentContent = row.content;
|
*/
|
||||||
|
upsertSessionMetadata(metadata: SessionMetadataCache): SessionMetadataCache {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
// Simple diff calculation
|
const existing = this.getSessionMetadata(metadata.session_id);
|
||||||
const prevLines = prevContent.split('\n');
|
|
||||||
const currentLines = currentContent.split('\n');
|
|
||||||
|
|
||||||
let added = 0;
|
if (existing) {
|
||||||
let deleted = 0;
|
const stmt = this.db.prepare(`
|
||||||
let modified = 0;
|
UPDATE session_metadata_cache
|
||||||
|
SET session_type = ?, title = ?, summary = ?, keywords = ?, token_estimate = ?,
|
||||||
|
file_patterns = ?, last_accessed = ?, access_count = ?
|
||||||
|
WHERE session_id = ?
|
||||||
|
`);
|
||||||
|
|
||||||
const maxLen = Math.max(prevLines.length, currentLines.length);
|
stmt.run(
|
||||||
for (let i = 0; i < maxLen; i++) {
|
metadata.session_type,
|
||||||
const prevLine = prevLines[i];
|
metadata.title || null,
|
||||||
const currentLine = currentLines[i];
|
metadata.summary || null,
|
||||||
|
metadata.keywords ? JSON.stringify(metadata.keywords) : null,
|
||||||
|
metadata.token_estimate || null,
|
||||||
|
metadata.file_patterns ? JSON.stringify(metadata.file_patterns) : null,
|
||||||
|
now,
|
||||||
|
existing.access_count + 1,
|
||||||
|
metadata.session_id
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
INSERT INTO session_metadata_cache
|
||||||
|
(session_id, session_type, title, summary, keywords, token_estimate, file_patterns, created_at, last_accessed, access_count)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`);
|
||||||
|
|
||||||
if (!prevLine && currentLine) added++;
|
stmt.run(
|
||||||
else if (prevLine && !currentLine) deleted++;
|
metadata.session_id,
|
||||||
else if (prevLine !== currentLine) modified++;
|
metadata.session_type,
|
||||||
}
|
metadata.title || null,
|
||||||
|
metadata.summary || null,
|
||||||
|
metadata.keywords ? JSON.stringify(metadata.keywords) : null,
|
||||||
|
metadata.token_estimate || null,
|
||||||
|
metadata.file_patterns ? JSON.stringify(metadata.file_patterns) : null,
|
||||||
|
metadata.created_at || now,
|
||||||
|
now,
|
||||||
|
metadata.access_count || 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
diffStats = { added, modified, deleted };
|
return this.getSessionMetadata(metadata.session_id)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
/**
|
||||||
memory_id: memoryId,
|
* Get session metadata
|
||||||
version: row.version,
|
*/
|
||||||
content: row.content,
|
getSessionMetadata(sessionId: string): SessionMetadataCache | null {
|
||||||
timestamp: row.timestamp,
|
const stmt = this.db.prepare(`SELECT * FROM session_metadata_cache WHERE session_id = ?`);
|
||||||
diff_stats: diffStats
|
const row = stmt.get(sessionId) as any;
|
||||||
};
|
if (!row) return null;
|
||||||
});
|
|
||||||
|
return {
|
||||||
|
session_id: row.session_id,
|
||||||
|
session_type: row.session_type,
|
||||||
|
title: row.title,
|
||||||
|
summary: row.summary,
|
||||||
|
keywords: row.keywords ? JSON.parse(row.keywords) : undefined,
|
||||||
|
token_estimate: row.token_estimate,
|
||||||
|
file_patterns: row.file_patterns ? JSON.parse(row.file_patterns) : undefined,
|
||||||
|
created_at: row.created_at,
|
||||||
|
last_accessed: row.last_accessed,
|
||||||
|
access_count: row.access_count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search sessions by keyword
|
||||||
|
*/
|
||||||
|
searchSessionsByKeyword(keyword: string): SessionMetadataCache[] {
|
||||||
|
const stmt = this.db.prepare(`
|
||||||
|
SELECT * FROM session_metadata_cache
|
||||||
|
WHERE title LIKE ? OR summary LIKE ? OR keywords LIKE ?
|
||||||
|
ORDER BY access_count DESC, last_accessed DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
const pattern = `%${keyword}%`;
|
||||||
|
const rows = stmt.all(pattern, pattern, pattern) as any[];
|
||||||
|
|
||||||
|
return rows.map(row => ({
|
||||||
|
session_id: row.session_id,
|
||||||
|
session_type: row.session_type,
|
||||||
|
title: row.title,
|
||||||
|
summary: row.summary,
|
||||||
|
keywords: row.keywords ? JSON.parse(row.keywords) : undefined,
|
||||||
|
token_estimate: row.token_estimate,
|
||||||
|
file_patterns: row.file_patterns ? JSON.parse(row.file_patterns) : undefined,
|
||||||
|
created_at: row.created_at,
|
||||||
|
last_accessed: row.last_accessed,
|
||||||
|
access_count: row.access_count
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import { getCoreMemoryStore } from '../core-memory-store.js';
|
import { getCoreMemoryStore } from '../core-memory-store.js';
|
||||||
import type { CoreMemory, KnowledgeGraph, EvolutionVersion } from '../core-memory-store.js';
|
import type { CoreMemory } from '../core-memory-store.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route context interface
|
* Route context interface
|
||||||
@@ -197,142 +197,5 @@ export async function handleCoreMemoryRoutes(ctx: RouteContext): Promise<boolean
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// API: Core Memory - Extract knowledge graph
|
|
||||||
if (pathname.startsWith('/api/core-memory/memories/') && pathname.endsWith('/knowledge-graph') && req.method === 'GET') {
|
|
||||||
const memoryId = pathname.replace('/api/core-memory/memories/', '').replace('/knowledge-graph', '');
|
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const store = getCoreMemoryStore(projectPath);
|
|
||||||
const knowledgeGraph = store.getKnowledgeGraph(memoryId);
|
|
||||||
|
|
||||||
// If no graph exists, extract it first
|
|
||||||
if (knowledgeGraph.nodes.length === 0) {
|
|
||||||
const extracted = store.extractKnowledgeGraph(memoryId);
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: true, knowledgeGraph: extracted }));
|
|
||||||
} else {
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: true, knowledgeGraph }));
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API: Core Memory - Track evolution history
|
|
||||||
if (pathname.startsWith('/api/core-memory/memories/') && pathname.endsWith('/evolution') && req.method === 'GET') {
|
|
||||||
const memoryId = pathname.replace('/api/core-memory/memories/', '').replace('/evolution', '');
|
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const store = getCoreMemoryStore(projectPath);
|
|
||||||
const evolution = store.trackEvolution(memoryId);
|
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ success: true, evolution }));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API: Core Memory - Get aggregated graph data for graph explorer
|
|
||||||
if (pathname === '/api/core-memory/graph' && req.method === 'GET') {
|
|
||||||
const projectPath = url.searchParams.get('path') || initialPath;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const store = getCoreMemoryStore(projectPath);
|
|
||||||
const memories = store.getMemories({ archived: false });
|
|
||||||
|
|
||||||
// Aggregate all knowledge graphs from memories
|
|
||||||
const aggregatedNodes: Array<{
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
symbol_type?: string;
|
|
||||||
path?: string;
|
|
||||||
line_number?: number;
|
|
||||||
imports?: number;
|
|
||||||
exports?: number;
|
|
||||||
references?: number;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
const aggregatedEdges: Array<{
|
|
||||||
source: string;
|
|
||||||
target: string;
|
|
||||||
type: string;
|
|
||||||
weight: number;
|
|
||||||
}> = [];
|
|
||||||
|
|
||||||
const nodeMap = new Map<string, any>();
|
|
||||||
const edgeMap = new Map<string, any>();
|
|
||||||
|
|
||||||
// Collect nodes and edges from all memories
|
|
||||||
memories.forEach((memory: CoreMemory) => {
|
|
||||||
const graph = store.getKnowledgeGraph(memory.id);
|
|
||||||
|
|
||||||
// Process nodes
|
|
||||||
graph.nodes.forEach((node: any) => {
|
|
||||||
const nodeId = node.id || node.name;
|
|
||||||
if (!nodeMap.has(nodeId)) {
|
|
||||||
nodeMap.set(nodeId, {
|
|
||||||
id: nodeId,
|
|
||||||
name: node.name || node.label || nodeId,
|
|
||||||
type: node.type || 'MODULE',
|
|
||||||
symbol_type: node.symbol_type,
|
|
||||||
path: node.path || node.file_path,
|
|
||||||
line_number: node.line_number,
|
|
||||||
imports: node.imports || 0,
|
|
||||||
exports: node.exports || 0,
|
|
||||||
references: node.references || 0
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Aggregate counts for duplicate nodes
|
|
||||||
const existing = nodeMap.get(nodeId);
|
|
||||||
existing.imports = (existing.imports || 0) + (node.imports || 0);
|
|
||||||
existing.exports = (existing.exports || 0) + (node.exports || 0);
|
|
||||||
existing.references = (existing.references || 0) + (node.references || 0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process edges
|
|
||||||
graph.edges.forEach((edge: any) => {
|
|
||||||
const edgeKey = `${edge.source}-${edge.target}-${edge.type || 'CALLS'}`;
|
|
||||||
if (!edgeMap.has(edgeKey)) {
|
|
||||||
edgeMap.set(edgeKey, {
|
|
||||||
source: edge.source || edge.from,
|
|
||||||
target: edge.target || edge.to,
|
|
||||||
type: edge.type || edge.relation_type || 'CALLS',
|
|
||||||
weight: edge.weight || 1
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Aggregate weights for duplicate edges
|
|
||||||
const existing = edgeMap.get(edgeKey);
|
|
||||||
existing.weight = (existing.weight || 1) + (edge.weight || 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert maps to arrays
|
|
||||||
aggregatedNodes.push(...nodeMap.values());
|
|
||||||
aggregatedEdges.push(...edgeMap.values());
|
|
||||||
|
|
||||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({
|
|
||||||
success: true,
|
|
||||||
nodes: aggregatedNodes,
|
|
||||||
edges: aggregatedEdges
|
|
||||||
}));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: (error as Error).message }));
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,9 +94,6 @@ function executeImport(params: Params): ImportResult {
|
|||||||
content: text.trim(),
|
content: text.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extract knowledge graph
|
|
||||||
store.extractKnowledgeGraph(memory.id);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
operation: 'import',
|
operation: 'import',
|
||||||
id: memory.id,
|
id: memory.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user