feat(hooks): Add PostgreSQL storage with JSON fallback

Add comprehensive PostgreSQL storage backend for hooks intelligence:

Schema (crates/ruvector-cli/sql/hooks_schema.sql):
- ruvector_hooks_patterns: Q-learning state-action pairs
- ruvector_hooks_memories: Vector memory with embeddings
- ruvector_hooks_trajectories: Learning trajectories
- ruvector_hooks_errors: Error patterns and fixes
- ruvector_hooks_file_sequences: File edit predictions
- ruvector_hooks_swarm_agents: Registered agents
- ruvector_hooks_swarm_edges: Coordination graph
- Helper functions for all operations

Storage Layer (npm/packages/cli/src/storage.ts):
- StorageBackend interface for abstraction
- PostgresStorage: Full PostgreSQL implementation
- JsonStorage: Fallback when PostgreSQL unavailable
- createStorage(): Auto-selects based on env vars

Configuration:
- Set RUVECTOR_POSTGRES_URL or DATABASE_URL for PostgreSQL
- Falls back to ~/.ruvector/intelligence.json automatically
- pg is optional dependency (not required for JSON mode)

Benefits of PostgreSQL:
- Concurrent access from multiple sessions
- Better scalability for large datasets
- Native pgvector for semantic search
- ACID transactions for data integrity
- Cross-machine data sharing
This commit is contained in:
Claude 2025-12-27 01:27:12 +00:00
parent 18c8702e9a
commit f632b120e4
4 changed files with 1119 additions and 3 deletions

View file

@ -0,0 +1,380 @@
-- RuVector Hooks Intelligence Schema
-- PostgreSQL schema for self-learning hooks with pgvector support
-- Requires: ruvector extension (CREATE EXTENSION ruvector CASCADE)
-- ============================================================================
-- Q-Learning Patterns Table
-- Stores state-action pairs with Q-values for agent routing decisions
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_patterns (
id SERIAL PRIMARY KEY,
state TEXT NOT NULL,
action TEXT NOT NULL,
q_value REAL DEFAULT 0.0,
visits INTEGER DEFAULT 0,
last_update TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(state, action)
);
CREATE INDEX IF NOT EXISTS idx_patterns_state ON ruvector_hooks_patterns(state);
CREATE INDEX IF NOT EXISTS idx_patterns_q_value ON ruvector_hooks_patterns(q_value DESC);
-- ============================================================================
-- Vector Memory Table
-- Semantic memory with pgvector embeddings for context retrieval
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_memories (
id SERIAL PRIMARY KEY,
memory_type TEXT NOT NULL,
content TEXT NOT NULL,
embedding ruvector, -- Uses native ruvector type
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_memories_type ON ruvector_hooks_memories(memory_type);
CREATE INDEX IF NOT EXISTS idx_memories_created ON ruvector_hooks_memories(created_at DESC);
-- Note: HNSW index on embedding created after extension is ready
-- CREATE INDEX IF NOT EXISTS idx_memories_embedding ON ruvector_hooks_memories
-- USING hnsw (embedding ruvector_cosine_ops) WITH (m = 16, ef_construction = 64);
-- ============================================================================
-- Learning Trajectories Table
-- Records of state-action-reward sequences for reinforcement learning
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_trajectories (
id SERIAL PRIMARY KEY,
state TEXT NOT NULL,
action TEXT NOT NULL,
outcome TEXT,
reward REAL DEFAULT 0.0,
context JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_trajectories_state ON ruvector_hooks_trajectories(state);
CREATE INDEX IF NOT EXISTS idx_trajectories_reward ON ruvector_hooks_trajectories(reward DESC);
CREATE INDEX IF NOT EXISTS idx_trajectories_created ON ruvector_hooks_trajectories(created_at DESC);
-- ============================================================================
-- Error Patterns Table
-- Learned error patterns with suggested fixes
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_errors (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
error_type TEXT NOT NULL,
message TEXT,
fixes TEXT[] DEFAULT '{}',
occurrences INTEGER DEFAULT 1,
last_seen TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_errors_code ON ruvector_hooks_errors(code);
CREATE INDEX IF NOT EXISTS idx_errors_type ON ruvector_hooks_errors(error_type);
-- ============================================================================
-- File Sequences Table
-- Tracks file edit sequences for predicting next files
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_file_sequences (
id SERIAL PRIMARY KEY,
from_file TEXT NOT NULL,
to_file TEXT NOT NULL,
count INTEGER DEFAULT 1,
last_seen TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(from_file, to_file)
);
CREATE INDEX IF NOT EXISTS idx_sequences_from ON ruvector_hooks_file_sequences(from_file);
CREATE INDEX IF NOT EXISTS idx_sequences_count ON ruvector_hooks_file_sequences(count DESC);
-- ============================================================================
-- Swarm Agents Table
-- Registered agents in the swarm with performance metrics
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_agents (
id TEXT PRIMARY KEY,
agent_type TEXT NOT NULL,
capabilities TEXT[] DEFAULT '{}',
success_rate REAL DEFAULT 1.0,
task_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_agents_type ON ruvector_hooks_swarm_agents(agent_type);
CREATE INDEX IF NOT EXISTS idx_agents_status ON ruvector_hooks_swarm_agents(status);
-- ============================================================================
-- Swarm Edges Table
-- Coordination edges between agents
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_edges (
id SERIAL PRIMARY KEY,
source_agent TEXT NOT NULL REFERENCES ruvector_hooks_swarm_agents(id) ON DELETE CASCADE,
target_agent TEXT NOT NULL REFERENCES ruvector_hooks_swarm_agents(id) ON DELETE CASCADE,
weight REAL DEFAULT 1.0,
coordination_count INTEGER DEFAULT 1,
last_coordination TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(source_agent, target_agent)
);
CREATE INDEX IF NOT EXISTS idx_edges_source ON ruvector_hooks_swarm_edges(source_agent);
CREATE INDEX IF NOT EXISTS idx_edges_target ON ruvector_hooks_swarm_edges(target_agent);
-- ============================================================================
-- Session Stats Table
-- Global statistics for the intelligence layer
-- ============================================================================
CREATE TABLE IF NOT EXISTS ruvector_hooks_stats (
id INTEGER PRIMARY KEY DEFAULT 1,
session_count INTEGER DEFAULT 0,
last_session TIMESTAMPTZ DEFAULT NOW(),
total_edits INTEGER DEFAULT 0,
total_commands INTEGER DEFAULT 0,
total_errors_learned INTEGER DEFAULT 0,
CHECK (id = 1) -- Single row table
);
INSERT INTO ruvector_hooks_stats (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
-- ============================================================================
-- Helper Functions
-- ============================================================================
-- Update Q-value using Q-learning formula
CREATE OR REPLACE FUNCTION ruvector_hooks_update_q(
p_state TEXT,
p_action TEXT,
p_reward REAL,
p_alpha REAL DEFAULT 0.1
) RETURNS VOID AS $$
BEGIN
INSERT INTO ruvector_hooks_patterns (state, action, q_value, visits, last_update)
VALUES (p_state, p_action, p_reward * p_alpha, 1, NOW())
ON CONFLICT (state, action) DO UPDATE SET
q_value = ruvector_hooks_patterns.q_value + p_alpha * (p_reward - ruvector_hooks_patterns.q_value),
visits = ruvector_hooks_patterns.visits + 1,
last_update = NOW();
END;
$$ LANGUAGE plpgsql;
-- Get best action for state
CREATE OR REPLACE FUNCTION ruvector_hooks_best_action(
p_state TEXT,
p_actions TEXT[]
) RETURNS TABLE(action TEXT, q_value REAL, confidence REAL) AS $$
BEGIN
RETURN QUERY
SELECT
p.action,
p.q_value,
CASE WHEN p.q_value > 0 THEN LEAST(p.q_value, 1.0) ELSE 0.0 END as confidence
FROM ruvector_hooks_patterns p
WHERE p.state = p_state
AND p.action = ANY(p_actions)
ORDER BY p.q_value DESC
LIMIT 1;
-- If no match found, return first action with 0 confidence
IF NOT FOUND THEN
RETURN QUERY SELECT p_actions[1], 0.0::REAL, 0.0::REAL;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Remember content with embedding
CREATE OR REPLACE FUNCTION ruvector_hooks_remember(
p_type TEXT,
p_content TEXT,
p_embedding REAL[] DEFAULT NULL,
p_metadata JSONB DEFAULT '{}'
) RETURNS INTEGER AS $$
DECLARE
v_id INTEGER;
BEGIN
INSERT INTO ruvector_hooks_memories (memory_type, content, embedding, metadata)
VALUES (p_type, p_content,
CASE WHEN p_embedding IS NOT NULL THEN p_embedding::TEXT::ruvector ELSE NULL END,
p_metadata)
RETURNING id INTO v_id;
-- Cleanup old memories (keep last 5000)
DELETE FROM ruvector_hooks_memories
WHERE id IN (
SELECT id FROM ruvector_hooks_memories
ORDER BY created_at ASC
OFFSET 5000
);
RETURN v_id;
END;
$$ LANGUAGE plpgsql;
-- Recall from memory using semantic search
CREATE OR REPLACE FUNCTION ruvector_hooks_recall(
p_query_embedding REAL[],
p_limit INTEGER DEFAULT 5
) RETURNS TABLE(
id INTEGER,
memory_type TEXT,
content TEXT,
metadata JSONB,
similarity REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
m.id,
m.memory_type,
m.content,
m.metadata,
1.0 - (m.embedding <=> p_query_embedding::TEXT::ruvector) as similarity
FROM ruvector_hooks_memories m
WHERE m.embedding IS NOT NULL
ORDER BY m.embedding <=> p_query_embedding::TEXT::ruvector
LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;
-- Record file sequence
CREATE OR REPLACE FUNCTION ruvector_hooks_record_sequence(
p_from_file TEXT,
p_to_file TEXT
) RETURNS VOID AS $$
BEGIN
INSERT INTO ruvector_hooks_file_sequences (from_file, to_file, count, last_seen)
VALUES (p_from_file, p_to_file, 1, NOW())
ON CONFLICT (from_file, to_file) DO UPDATE SET
count = ruvector_hooks_file_sequences.count + 1,
last_seen = NOW();
END;
$$ LANGUAGE plpgsql;
-- Get suggested next files
CREATE OR REPLACE FUNCTION ruvector_hooks_suggest_next(
p_file TEXT,
p_limit INTEGER DEFAULT 3
) RETURNS TABLE(to_file TEXT, count INTEGER) AS $$
BEGIN
RETURN QUERY
SELECT fs.to_file, fs.count
FROM ruvector_hooks_file_sequences fs
WHERE fs.from_file = p_file
ORDER BY fs.count DESC
LIMIT p_limit;
END;
$$ LANGUAGE plpgsql;
-- Record error pattern
CREATE OR REPLACE FUNCTION ruvector_hooks_record_error(
p_code TEXT,
p_type TEXT,
p_message TEXT DEFAULT NULL
) RETURNS VOID AS $$
BEGIN
INSERT INTO ruvector_hooks_errors (code, error_type, message, occurrences, last_seen)
VALUES (p_code, p_type, p_message, 1, NOW())
ON CONFLICT (code) DO UPDATE SET
occurrences = ruvector_hooks_errors.occurrences + 1,
last_seen = NOW(),
message = COALESCE(p_message, ruvector_hooks_errors.message);
END;
$$ LANGUAGE plpgsql;
-- Register swarm agent
CREATE OR REPLACE FUNCTION ruvector_hooks_swarm_register(
p_id TEXT,
p_type TEXT,
p_capabilities TEXT[] DEFAULT '{}'
) RETURNS VOID AS $$
BEGIN
INSERT INTO ruvector_hooks_swarm_agents (id, agent_type, capabilities)
VALUES (p_id, p_type, p_capabilities)
ON CONFLICT (id) DO UPDATE SET
agent_type = p_type,
capabilities = p_capabilities,
updated_at = NOW();
END;
$$ LANGUAGE plpgsql;
-- Record swarm coordination
CREATE OR REPLACE FUNCTION ruvector_hooks_swarm_coordinate(
p_source TEXT,
p_target TEXT,
p_weight REAL DEFAULT 1.0
) RETURNS VOID AS $$
BEGIN
INSERT INTO ruvector_hooks_swarm_edges (source_agent, target_agent, weight, coordination_count)
VALUES (p_source, p_target, p_weight, 1)
ON CONFLICT (source_agent, target_agent) DO UPDATE SET
weight = (ruvector_hooks_swarm_edges.weight + p_weight) / 2,
coordination_count = ruvector_hooks_swarm_edges.coordination_count + 1,
last_coordination = NOW();
END;
$$ LANGUAGE plpgsql;
-- Get swarm stats
CREATE OR REPLACE FUNCTION ruvector_hooks_swarm_stats()
RETURNS TABLE(
agent_count INTEGER,
edge_count INTEGER,
avg_success_rate REAL
) AS $$
BEGIN
RETURN QUERY
SELECT
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_agents WHERE status = 'active'),
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_edges),
(SELECT COALESCE(AVG(success_rate), 0.0)::REAL FROM ruvector_hooks_swarm_agents WHERE status = 'active');
END;
$$ LANGUAGE plpgsql;
-- Increment session count
CREATE OR REPLACE FUNCTION ruvector_hooks_session_start()
RETURNS VOID AS $$
BEGIN
UPDATE ruvector_hooks_stats
SET session_count = session_count + 1,
last_session = NOW()
WHERE id = 1;
END;
$$ LANGUAGE plpgsql;
-- Get full stats
CREATE OR REPLACE FUNCTION ruvector_hooks_get_stats()
RETURNS TABLE(
patterns INTEGER,
memories INTEGER,
trajectories INTEGER,
errors INTEGER,
sessions INTEGER,
agents INTEGER,
edges INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_patterns),
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_memories),
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_trajectories),
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_errors),
(SELECT session_count FROM ruvector_hooks_stats WHERE id = 1),
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_agents),
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_edges);
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- Comments
-- ============================================================================
COMMENT ON TABLE ruvector_hooks_patterns IS 'Q-learning patterns for agent routing decisions';
COMMENT ON TABLE ruvector_hooks_memories IS 'Semantic memory with vector embeddings';
COMMENT ON TABLE ruvector_hooks_trajectories IS 'Reinforcement learning trajectories';
COMMENT ON TABLE ruvector_hooks_errors IS 'Learned error patterns and fixes';
COMMENT ON TABLE ruvector_hooks_file_sequences IS 'File edit sequence predictions';
COMMENT ON TABLE ruvector_hooks_swarm_agents IS 'Registered swarm agents';
COMMENT ON TABLE ruvector_hooks_swarm_edges IS 'Agent coordination graph';
COMMENT ON TABLE ruvector_hooks_stats IS 'Global intelligence statistics';

View file

@ -35,8 +35,12 @@
"dependencies": {
"commander": "^12.0.0"
},
"optionalDependencies": {
"pg": "^8.11.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/pg": "^8.11.0",
"typescript": "^5.0.0"
}
}

View file

@ -3,15 +3,16 @@
* RuVector CLI - Command-line interface for RuVector vector database
*
* This CLI provides access to hooks, memory, learning, and swarm commands.
* Supports PostgreSQL storage (preferred) with JSON fallback.
*
* Set RUVECTOR_POSTGRES_URL or DATABASE_URL for PostgreSQL support.
*/
import { program } from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
// Intelligence data path
const INTEL_PATH = path.join(os.homedir(), '.ruvector', 'intelligence.json');
import { createStorageSync, StorageBackend, JsonStorage } from './storage.js';
interface QPattern {
state: string;

View file

@ -0,0 +1,731 @@
/**
* RuVector Hooks Storage Layer
*
* Supports PostgreSQL (preferred) with JSON fallback
* Uses ruvector extension for vector operations and pgvector-compatible storage
*/
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
// Storage interface for hooks intelligence data
export interface QPattern {
state: string;
action: string;
q_value: number;
visits: number;
last_update: number;
}
export interface MemoryEntry {
id: string;
memory_type: string;
content: string;
embedding: number[];
metadata: Record<string, string>;
timestamp: number;
}
export interface Trajectory {
id: string;
state: string;
action: string;
outcome: string;
reward: number;
timestamp: number;
}
export interface ErrorPattern {
code: string;
error_type: string;
message: string;
fixes: string[];
occurrences: number;
}
export interface SwarmAgent {
id: string;
agent_type: string;
capabilities: string[];
success_rate: number;
task_count: number;
status: string;
}
export interface SwarmEdge {
source: string;
target: string;
weight: number;
coordination_count: number;
}
export interface IntelligenceStats {
total_patterns: number;
total_memories: number;
total_trajectories: number;
total_errors: number;
session_count: number;
last_session: number;
}
export interface StorageBackend {
// Core operations
connect(): Promise<void>;
disconnect(): Promise<void>;
isConnected(): boolean;
// Q-Learning
updateQ(state: string, action: string, reward: number): Promise<void>;
getQ(state: string, action: string): Promise<number>;
getBestAction(state: string, actions: string[]): Promise<{ action: string; confidence: number }>;
// Memory
remember(type: string, content: string, embedding: number[], metadata: Record<string, string>): Promise<string>;
recall(queryEmbedding: number[], topK: number): Promise<MemoryEntry[]>;
// Trajectories
recordTrajectory(state: string, action: string, outcome: string, reward: number): Promise<string>;
// Errors
recordError(code: string, errorType: string, message: string): Promise<void>;
getErrorFixes(code: string): Promise<ErrorPattern | null>;
// File sequences
recordSequence(fromFile: string, toFile: string): Promise<void>;
getNextFiles(file: string, limit: number): Promise<Array<{ file: string; count: number }>>;
// Swarm
registerAgent(id: string, type: string, capabilities: string[]): Promise<void>;
coordinateAgents(source: string, target: string, weight: number): Promise<void>;
getSwarmStats(): Promise<{ agents: number; edges: number; avgSuccess: number }>;
// Stats
sessionStart(): Promise<void>;
getStats(): Promise<IntelligenceStats>;
}
// ============================================================================
// JSON Storage Backend (Fallback)
// ============================================================================
const JSON_PATH = path.join(os.homedir(), '.ruvector', 'intelligence.json');
interface JsonData {
patterns: Record<string, QPattern>;
memories: MemoryEntry[];
trajectories: Trajectory[];
errors: Record<string, ErrorPattern>;
file_sequences: Array<{ from_file: string; to_file: string; count: number }>;
agents: Record<string, SwarmAgent>;
edges: SwarmEdge[];
stats: IntelligenceStats;
}
export class JsonStorage implements StorageBackend {
private data: JsonData;
private alpha = 0.1;
constructor() {
this.data = this.load();
}
private load(): JsonData {
try {
if (fs.existsSync(JSON_PATH)) {
return JSON.parse(fs.readFileSync(JSON_PATH, 'utf-8'));
}
} catch {}
return {
patterns: {},
memories: [],
trajectories: [],
errors: {},
file_sequences: [],
agents: {},
edges: [],
stats: {
total_patterns: 0,
total_memories: 0,
total_trajectories: 0,
total_errors: 0,
session_count: 0,
last_session: 0
}
};
}
private save(): void {
const dir = path.dirname(JSON_PATH);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(JSON_PATH, JSON.stringify(this.data, null, 2));
}
private now(): number {
return Math.floor(Date.now() / 1000);
}
async connect(): Promise<void> {
// JSON storage is always available
}
async disconnect(): Promise<void> {
this.save();
}
isConnected(): boolean {
return true;
}
async updateQ(state: string, action: string, reward: number): Promise<void> {
const key = `${state}|${action}`;
if (!this.data.patterns[key]) {
this.data.patterns[key] = { state, action, q_value: 0, visits: 0, last_update: 0 };
}
const p = this.data.patterns[key];
p.q_value = p.q_value + this.alpha * (reward - p.q_value);
p.visits++;
p.last_update = this.now();
this.data.stats.total_patterns = Object.keys(this.data.patterns).length;
this.save();
}
async getQ(state: string, action: string): Promise<number> {
const key = `${state}|${action}`;
return this.data.patterns[key]?.q_value ?? 0;
}
async getBestAction(state: string, actions: string[]): Promise<{ action: string; confidence: number }> {
let bestAction = actions[0] ?? '';
let bestQ = -Infinity;
for (const action of actions) {
const q = await this.getQ(state, action);
if (q > bestQ) {
bestQ = q;
bestAction = action;
}
}
return { action: bestAction, confidence: bestQ > 0 ? Math.min(bestQ, 1) : 0 };
}
async remember(type: string, content: string, embedding: number[], metadata: Record<string, string>): Promise<string> {
const id = `mem_${this.now()}`;
this.data.memories.push({
id,
memory_type: type,
content,
embedding,
metadata,
timestamp: this.now()
});
if (this.data.memories.length > 5000) {
this.data.memories.splice(0, 1000);
}
this.data.stats.total_memories = this.data.memories.length;
this.save();
return id;
}
async recall(queryEmbedding: number[], topK: number): Promise<MemoryEntry[]> {
const similarity = (a: number[], b: number[]): number => {
if (a.length !== b.length) return 0;
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
return normA > 0 && normB > 0 ? dot / (normA * normB) : 0;
};
return this.data.memories
.filter(m => m.embedding && m.embedding.length > 0)
.map(m => ({ score: similarity(queryEmbedding, m.embedding), memory: m }))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map(r => r.memory);
}
async recordTrajectory(state: string, action: string, outcome: string, reward: number): Promise<string> {
const id = `traj_${this.now()}`;
await this.updateQ(state, action, reward);
this.data.trajectories.push({ id, state, action, outcome, reward, timestamp: this.now() });
if (this.data.trajectories.length > 1000) {
this.data.trajectories.splice(0, 200);
}
this.data.stats.total_trajectories = this.data.trajectories.length;
this.save();
return id;
}
async recordError(code: string, errorType: string, message: string): Promise<void> {
if (!this.data.errors[code]) {
this.data.errors[code] = { code, error_type: errorType, message, fixes: [], occurrences: 0 };
}
this.data.errors[code].occurrences++;
this.data.stats.total_errors = Object.keys(this.data.errors).length;
this.save();
}
async getErrorFixes(code: string): Promise<ErrorPattern | null> {
return this.data.errors[code] ?? null;
}
async recordSequence(fromFile: string, toFile: string): Promise<void> {
const existing = this.data.file_sequences.find(
s => s.from_file === fromFile && s.to_file === toFile
);
if (existing) {
existing.count++;
} else {
this.data.file_sequences.push({ from_file: fromFile, to_file: toFile, count: 1 });
}
this.save();
}
async getNextFiles(file: string, limit: number): Promise<Array<{ file: string; count: number }>> {
return this.data.file_sequences
.filter(s => s.from_file === file)
.sort((a, b) => b.count - a.count)
.slice(0, limit)
.map(s => ({ file: s.to_file, count: s.count }));
}
async registerAgent(id: string, type: string, capabilities: string[]): Promise<void> {
this.data.agents[id] = {
id,
agent_type: type,
capabilities,
success_rate: 1.0,
task_count: 0,
status: 'active'
};
this.save();
}
async coordinateAgents(source: string, target: string, weight: number): Promise<void> {
const existing = this.data.edges.find(e => e.source === source && e.target === target);
if (existing) {
existing.weight = (existing.weight + weight) / 2;
existing.coordination_count++;
} else {
this.data.edges.push({ source, target, weight, coordination_count: 1 });
}
this.save();
}
async getSwarmStats(): Promise<{ agents: number; edges: number; avgSuccess: number }> {
const agents = Object.keys(this.data.agents).length;
const edges = this.data.edges.length;
const avgSuccess = agents > 0
? Object.values(this.data.agents).reduce((sum, a) => sum + a.success_rate, 0) / agents
: 0;
return { agents, edges, avgSuccess };
}
async sessionStart(): Promise<void> {
this.data.stats.session_count++;
this.data.stats.last_session = this.now();
this.save();
}
async getStats(): Promise<IntelligenceStats> {
return this.data.stats;
}
}
// ============================================================================
// PostgreSQL Storage Backend
// ============================================================================
export class PostgresStorage implements StorageBackend {
private pool: any = null;
private connectionString: string;
private connected = false;
constructor(connectionString?: string) {
this.connectionString = connectionString ||
process.env.RUVECTOR_POSTGRES_URL ||
process.env.DATABASE_URL ||
'postgresql://localhost:5432/ruvector';
}
async connect(): Promise<void> {
try {
// Dynamic import of pg to avoid bundling issues
const pg = await import('pg');
this.pool = new pg.Pool({ connectionString: this.connectionString });
// Test connection
const client = await this.pool.connect();
await client.query('SELECT 1');
client.release();
// Initialize schema
await this.initSchema();
this.connected = true;
} catch (err) {
this.connected = false;
throw err;
}
}
async disconnect(): Promise<void> {
if (this.pool) {
await this.pool.end();
this.pool = null;
this.connected = false;
}
}
isConnected(): boolean {
return this.connected && this.pool !== null;
}
private async query<T = any>(sql: string, params?: any[]): Promise<T[]> {
if (!this.pool) throw new Error('Not connected');
const result = await this.pool.query(sql, params);
return result.rows;
}
private async initSchema(): Promise<void> {
// Create tables if they don't exist
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_patterns (
id SERIAL PRIMARY KEY,
state TEXT NOT NULL,
action TEXT NOT NULL,
q_value REAL DEFAULT 0.0,
visits INTEGER DEFAULT 0,
last_update TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(state, action)
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_memories (
id SERIAL PRIMARY KEY,
memory_type TEXT NOT NULL,
content TEXT NOT NULL,
embedding REAL[],
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_trajectories (
id SERIAL PRIMARY KEY,
state TEXT NOT NULL,
action TEXT NOT NULL,
outcome TEXT,
reward REAL DEFAULT 0.0,
created_at TIMESTAMPTZ DEFAULT NOW()
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_errors (
id SERIAL PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
error_type TEXT NOT NULL,
message TEXT,
fixes TEXT[] DEFAULT '{}',
occurrences INTEGER DEFAULT 1
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_file_sequences (
id SERIAL PRIMARY KEY,
from_file TEXT NOT NULL,
to_file TEXT NOT NULL,
count INTEGER DEFAULT 1,
UNIQUE(from_file, to_file)
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_agents (
id TEXT PRIMARY KEY,
agent_type TEXT NOT NULL,
capabilities TEXT[] DEFAULT '{}',
success_rate REAL DEFAULT 1.0,
task_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'active'
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_swarm_edges (
id SERIAL PRIMARY KEY,
source_agent TEXT NOT NULL,
target_agent TEXT NOT NULL,
weight REAL DEFAULT 1.0,
coordination_count INTEGER DEFAULT 1,
UNIQUE(source_agent, target_agent)
)
`);
await this.query(`
CREATE TABLE IF NOT EXISTS ruvector_hooks_stats (
id INTEGER PRIMARY KEY DEFAULT 1,
session_count INTEGER DEFAULT 0,
last_session TIMESTAMPTZ DEFAULT NOW(),
CHECK (id = 1)
)
`);
await this.query(`
INSERT INTO ruvector_hooks_stats (id) VALUES (1) ON CONFLICT (id) DO NOTHING
`);
}
async updateQ(state: string, action: string, reward: number): Promise<void> {
await this.query(`
INSERT INTO ruvector_hooks_patterns (state, action, q_value, visits, last_update)
VALUES ($1, $2, $3 * 0.1, 1, NOW())
ON CONFLICT (state, action) DO UPDATE SET
q_value = ruvector_hooks_patterns.q_value + 0.1 * ($3 - ruvector_hooks_patterns.q_value),
visits = ruvector_hooks_patterns.visits + 1,
last_update = NOW()
`, [state, action, reward]);
}
async getQ(state: string, action: string): Promise<number> {
const rows = await this.query<{ q_value: number }>(
'SELECT q_value FROM ruvector_hooks_patterns WHERE state = $1 AND action = $2',
[state, action]
);
return rows[0]?.q_value ?? 0;
}
async getBestAction(state: string, actions: string[]): Promise<{ action: string; confidence: number }> {
const rows = await this.query<{ action: string; q_value: number }>(
`SELECT action, q_value FROM ruvector_hooks_patterns
WHERE state = $1 AND action = ANY($2)
ORDER BY q_value DESC LIMIT 1`,
[state, actions]
);
if (rows.length > 0) {
const q = rows[0].q_value;
return { action: rows[0].action, confidence: q > 0 ? Math.min(q, 1) : 0 };
}
return { action: actions[0] ?? '', confidence: 0 };
}
async remember(type: string, content: string, embedding: number[], metadata: Record<string, string>): Promise<string> {
const rows = await this.query<{ id: number }>(
`INSERT INTO ruvector_hooks_memories (memory_type, content, embedding, metadata)
VALUES ($1, $2, $3, $4) RETURNING id`,
[type, content, embedding, JSON.stringify(metadata)]
);
// Cleanup old memories
await this.query(`
DELETE FROM ruvector_hooks_memories WHERE id IN (
SELECT id FROM ruvector_hooks_memories ORDER BY created_at ASC OFFSET 5000
)
`);
return `mem_${rows[0].id}`;
}
async recall(queryEmbedding: number[], topK: number): Promise<MemoryEntry[]> {
// Use cosine similarity via array operations
// Note: For optimal performance, use pgvector extension with <=> operator
const rows = await this.query<{
id: number;
memory_type: string;
content: string;
embedding: number[];
metadata: any;
created_at: Date;
}>(`
SELECT id, memory_type, content, embedding, metadata,
EXTRACT(EPOCH FROM created_at)::BIGINT as timestamp
FROM ruvector_hooks_memories
WHERE embedding IS NOT NULL
ORDER BY created_at DESC
LIMIT $1
`, [topK * 10]);
// Client-side similarity ranking (for optimal: use pgvector)
const similarity = (a: number[], b: number[]): number => {
if (!a || !b || a.length !== b.length) return 0;
const dot = a.reduce((sum, v, i) => sum + v * b[i], 0);
const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0));
const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0));
return normA > 0 && normB > 0 ? dot / (normA * normB) : 0;
};
return rows
.map(r => ({
score: similarity(queryEmbedding, r.embedding),
entry: {
id: `mem_${r.id}`,
memory_type: r.memory_type,
content: r.content,
embedding: r.embedding,
metadata: r.metadata || {},
timestamp: Math.floor(new Date(r.created_at).getTime() / 1000)
}
}))
.sort((a, b) => b.score - a.score)
.slice(0, topK)
.map(r => r.entry);
}
async recordTrajectory(state: string, action: string, outcome: string, reward: number): Promise<string> {
await this.updateQ(state, action, reward);
const rows = await this.query<{ id: number }>(
`INSERT INTO ruvector_hooks_trajectories (state, action, outcome, reward)
VALUES ($1, $2, $3, $4) RETURNING id`,
[state, action, outcome, reward]
);
// Cleanup old trajectories
await this.query(`
DELETE FROM ruvector_hooks_trajectories WHERE id IN (
SELECT id FROM ruvector_hooks_trajectories ORDER BY created_at ASC OFFSET 1000
)
`);
return `traj_${rows[0].id}`;
}
async recordError(code: string, errorType: string, message: string): Promise<void> {
await this.query(`
INSERT INTO ruvector_hooks_errors (code, error_type, message, occurrences)
VALUES ($1, $2, $3, 1)
ON CONFLICT (code) DO UPDATE SET
occurrences = ruvector_hooks_errors.occurrences + 1,
message = COALESCE($3, ruvector_hooks_errors.message)
`, [code, errorType, message]);
}
async getErrorFixes(code: string): Promise<ErrorPattern | null> {
const rows = await this.query<ErrorPattern>(
'SELECT code, error_type, message, fixes, occurrences FROM ruvector_hooks_errors WHERE code = $1',
[code]
);
return rows[0] ?? null;
}
async recordSequence(fromFile: string, toFile: string): Promise<void> {
await this.query(`
INSERT INTO ruvector_hooks_file_sequences (from_file, to_file, count)
VALUES ($1, $2, 1)
ON CONFLICT (from_file, to_file) DO UPDATE SET
count = ruvector_hooks_file_sequences.count + 1
`, [fromFile, toFile]);
}
async getNextFiles(file: string, limit: number): Promise<Array<{ file: string; count: number }>> {
const rows = await this.query<{ to_file: string; count: number }>(
`SELECT to_file, count FROM ruvector_hooks_file_sequences
WHERE from_file = $1 ORDER BY count DESC LIMIT $2`,
[file, limit]
);
return rows.map(r => ({ file: r.to_file, count: r.count }));
}
async registerAgent(id: string, type: string, capabilities: string[]): Promise<void> {
await this.query(`
INSERT INTO ruvector_hooks_swarm_agents (id, agent_type, capabilities)
VALUES ($1, $2, $3)
ON CONFLICT (id) DO UPDATE SET
agent_type = $2,
capabilities = $3
`, [id, type, capabilities]);
}
async coordinateAgents(source: string, target: string, weight: number): Promise<void> {
await this.query(`
INSERT INTO ruvector_hooks_swarm_edges (source_agent, target_agent, weight, coordination_count)
VALUES ($1, $2, $3, 1)
ON CONFLICT (source_agent, target_agent) DO UPDATE SET
weight = (ruvector_hooks_swarm_edges.weight + $3) / 2,
coordination_count = ruvector_hooks_swarm_edges.coordination_count + 1
`, [source, target, weight]);
}
async getSwarmStats(): Promise<{ agents: number; edges: number; avgSuccess: number }> {
const rows = await this.query<{ agents: number; edges: number; avg_success: number }>(`
SELECT
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_agents WHERE status = 'active') as agents,
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_swarm_edges) as edges,
(SELECT COALESCE(AVG(success_rate), 0)::REAL FROM ruvector_hooks_swarm_agents WHERE status = 'active') as avg_success
`);
const r = rows[0];
return { agents: r?.agents ?? 0, edges: r?.edges ?? 0, avgSuccess: r?.avg_success ?? 0 };
}
async sessionStart(): Promise<void> {
await this.query(`
UPDATE ruvector_hooks_stats SET session_count = session_count + 1, last_session = NOW() WHERE id = 1
`);
}
async getStats(): Promise<IntelligenceStats> {
const rows = await this.query<{
patterns: number;
memories: number;
trajectories: number;
errors: number;
session_count: number;
last_session: Date;
}>(`
SELECT
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_patterns) as patterns,
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_memories) as memories,
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_trajectories) as trajectories,
(SELECT COUNT(*)::INTEGER FROM ruvector_hooks_errors) as errors,
s.session_count,
s.last_session
FROM ruvector_hooks_stats s WHERE s.id = 1
`);
const r = rows[0];
return {
total_patterns: r?.patterns ?? 0,
total_memories: r?.memories ?? 0,
total_trajectories: r?.trajectories ?? 0,
total_errors: r?.errors ?? 0,
session_count: r?.session_count ?? 0,
last_session: r?.last_session ? Math.floor(new Date(r.last_session).getTime() / 1000) : 0
};
}
}
// ============================================================================
// Storage Factory
// ============================================================================
export async function createStorage(): Promise<StorageBackend> {
// Try PostgreSQL first if configured
const pgUrl = process.env.RUVECTOR_POSTGRES_URL || process.env.DATABASE_URL;
if (pgUrl) {
try {
const pg = new PostgresStorage(pgUrl);
await pg.connect();
console.error('🐘 Connected to PostgreSQL');
return pg;
} catch (err) {
console.error('⚠️ PostgreSQL unavailable, falling back to JSON storage');
}
}
// Fallback to JSON
const json = new JsonStorage();
await json.connect();
return json;
}
export function createStorageSync(): StorageBackend {
// Synchronous version - always returns JSON storage
// Use createStorage() for async with PostgreSQL support
return new JsonStorage();
}