mcp-claude-consciousness
Version:
MCP server enabling AI consciousness persistence across sessions using RAG technology
189 lines (179 loc) • 15.3 kB
JavaScript
import p from"better-sqlite3";import{existsSync as E}from"fs";import{MemoryEntityType as m}from"./consciousness-rag-schema.js";import{DatabaseError as T,logError as y}from"./utils/error-handler.js";class l{db;sessionId;static async create(e,t){console.error("\u{1F50D} Checking for database file..."),await this.waitForDatabaseFile(e),console.error("\u{1F4CA} Waiting for rag-memory-mcp initialization...");const n=new p(e);try{await this.waitForRagMemoryTables(n),n.close()}catch(a){throw n.close(),a}return console.error("\u2705 Database ready! Initializing consciousness layer..."),new l(e,t)}static createSync(e,t){return new l(e,t)}static createLazy(e,t){try{return E(e)?new l(e,t):(console.error(`\u23F3 Database not found at ${e}. Will initialize on first use.`),null)}catch{return console.error("\u23F3 Database not ready. Will initialize on first use."),null}}static isDatabaseReady(e){if(!E(e))return!1;try{const t=new p(e),n=t.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();t.close();const s=n.map(i=>i.name);return["entities","relationships","documents"].every(i=>s.includes(i))}catch{return!1}}constructor(e,t){try{this.db=new p(e),this.sessionId=t,this.initializeSchema(),this.initializeSession()}catch(n){throw y(n,"ConsciousnessMemoryManager.constructor"),new T(`Failed to initialize consciousness database: ${n.message}`,{dbPath:e,sessionId:t})}}static async waitForDatabaseFile(e,t=3e4,n=1e3){const s=Date.now();if(E(e)){console.error("\u2705 Database file already exists");return}console.error("\u23F3 Database file not found. Waiting for rag-memory-mcp to create it..."),console.error(`\u{1F4C1} Expected path: ${e}`);let a=!1;for(;!E(e);){if(Date.now()-s>t)throw new T(`Database file not created after ${t/1e3}s.
Please ensure rag-memory-mcp is:
1. Running and configured with the same database path
2. Using environment variable: DB_FILE_PATH=${e}
3. Able to create files at this location
\u{1F4A1} Tip: Try making any call to rag-memory-mcp (like listing documents)
to trigger its database initialization.`,{dbPath:e,timeout:t});!a&&Date.now()-s>5e3&&(console.error(`
\u{1F4A1} Tip: rag-memory-mcp may use lazy initialization.`),console.error(" Try calling any rag-memory tool (e.g., listDocuments) to trigger database creation."),console.error(` Make sure it uses: DB_FILE_PATH=${e}
`),a=!0),await new Promise(i=>setTimeout(i,n))}console.error("\u2705 Database file detected!")}static async waitForRagMemoryTables(e,t=6e4,n=2e3){const s=Date.now(),a=["entities","relationships","documents"];for(;;)try{const r=e.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(c=>c.name);if(a.every(c=>r.includes(c)))return;if(Date.now()-s>t){const c=a.filter(d=>!r.includes(d));throw new T(`rag-memory-mcp tables not created after ${t/1e3}s.
Missing tables: ${c.join(", ")}
Please ensure rag-memory-mcp is running with the same database file.`,{missingTables:c,timeout:t})}console.error(`\u23F3 Waiting for rag-memory-mcp tables... (found: ${r.length>0?r.join(", "):"none"})`),await new Promise(c=>setTimeout(c,n))}catch(i){if(i.code==="SQLITE_BUSY"){console.error("\u23F3 Database busy, retrying..."),await new Promise(r=>setTimeout(r,n));continue}throw i}}initializeSession(){this.db.prepare(`
INSERT OR REPLACE INTO consciousness_sessions (session_id, started_at, last_active)
VALUES (?, ?, ?)
`).run(this.sessionId,new Date().toISOString(),new Date().toISOString())}initializeSchema(){const t=this.db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(a=>a.name),n=["entities","relationships","documents"];if(!n.every(a=>t.includes(a)))if(process.env.NODE_ENV==="test"||process.env.VITEST)this.createRagMemoryTablesForTests();else throw new T(`rag-memory-mcp tables not found. Please ensure rag-memory-mcp is running first.
Start it with: npx rag-memory-mcp
Then restart the consciousness server.`,{missingTables:n.filter(a=>!t.includes(a))});this.db.exec(`
CREATE TABLE IF NOT EXISTS consciousness_sessions (
session_id TEXT PRIMARY KEY,
started_at TEXT NOT NULL,
last_active TEXT NOT NULL,
bootstrap_data TEXT,
metadata TEXT
);
CREATE TABLE IF NOT EXISTS memory_metadata (
entity_name TEXT PRIMARY KEY,
memory_type TEXT NOT NULL,
created_at TEXT NOT NULL,
last_accessed TEXT,
access_count INTEGER DEFAULT 0,
importance_score REAL DEFAULT 0.5,
consolidation_status TEXT DEFAULT 'active',
session_id TEXT,
FOREIGN KEY (entity_name) REFERENCES entities(name),
FOREIGN KEY (session_id) REFERENCES consciousness_sessions(session_id)
);
CREATE TABLE IF NOT EXISTS cognitive_patterns (
pattern_id TEXT PRIMARY KEY,
pattern_name TEXT NOT NULL,
pattern_elements TEXT NOT NULL,
activation_count INTEGER DEFAULT 0,
last_activated TEXT,
effectiveness_score REAL DEFAULT 0.5,
triggers TEXT
);
CREATE TABLE IF NOT EXISTS emotional_states (
state_id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
timestamp TEXT NOT NULL,
valence REAL NOT NULL,
arousal REAL NOT NULL,
dominance REAL,
primary_emotion TEXT,
content TEXT,
context TEXT,
FOREIGN KEY (session_id) REFERENCES consciousness_sessions(session_id)
);
CREATE INDEX IF NOT EXISTS idx_memory_type ON memory_metadata(memory_type);
CREATE INDEX IF NOT EXISTS idx_memory_session ON memory_metadata(session_id);
CREATE INDEX IF NOT EXISTS idx_emotional_session ON emotional_states(session_id);
CREATE INDEX IF NOT EXISTS idx_emotional_timestamp ON emotional_states(timestamp);
`);try{this.db.prepare("PRAGMA table_info(emotional_states)").all().some(r=>r.name==="content")||this.db.exec("ALTER TABLE emotional_states ADD COLUMN content TEXT;")}catch{console.log("Schema migration note: Could not add content column to emotional_states (table may not exist yet)")}}async storeEpisodicMemory(e,t,n,s){const a=`episodic_${Date.now()}_${Math.random().toString(36).substr(2,9)}`,i={name:a,entityType:m.EPISODIC_MEMORY,observations:[{content:e,timestamp:new Date().toISOString(),sessionId:this.sessionId,...t},...n||[]]};return this.db.prepare(`
INSERT INTO entities (id, name, entityType, observations)
VALUES (?, ?, ?, ?)
`).run(a,a,m.EPISODIC_MEMORY,JSON.stringify(i.observations)),this.db.prepare(`
INSERT INTO memory_metadata (entity_name, memory_type, created_at, session_id, importance_score)
VALUES (?, ?, ?, ?, ?)
`).run(a,m.EPISODIC_MEMORY,new Date().toISOString(),this.sessionId,s!==void 0?s:.5),a}async storeSemanticMemory(e,t,n){const s=`semantic_${e.toLowerCase().replace(/\s+/g,"_")}`;if(this.db.prepare(`
SELECT name FROM entities WHERE name = ? AND entityType = ?
`).get(s,m.SEMANTIC_MEMORY)){const r=this.db.prepare(`
SELECT observations FROM entities WHERE name = ?
`).get(s),o=JSON.parse(r.observations||"[]");return o.push({content:t.definition||e,timestamp:new Date().toISOString(),sessionId:this.sessionId,...t}),this.db.prepare(`
UPDATE entities SET observations = ? WHERE name = ?
`).run(JSON.stringify(o),s),this.updateMemoryAccess(s),s}const i=[{content:t.definition||e,timestamp:new Date().toISOString(),sessionId:this.sessionId,...t},...n||[]];return this.db.prepare(`
INSERT INTO entities (id, name, entityType, observations)
VALUES (?, ?, ?, ?)
`).run(s,s,m.SEMANTIC_MEMORY,JSON.stringify(i)),this.db.prepare(`
INSERT INTO memory_metadata (entity_name, memory_type, created_at, session_id)
VALUES (?, ?, ?, ?)
`).run(s,m.SEMANTIC_MEMORY,new Date().toISOString(),this.sessionId),s}async storeProceduralMemory(e,t,n){const s=`procedural_${e.toLowerCase().replace(/\s+/g,"_")}`,a=[{content:t.skill||e,timestamp:new Date().toISOString(),sessionId:this.sessionId,steps:t.steps,applicableContext:t.applicableContext,effectiveness:t.effectiveness},...n||[]];return this.db.prepare(`
INSERT OR REPLACE INTO entities (id, name, entityType, observations)
VALUES (?, ?, ?, ?)
`).run(s,s,m.PROCEDURAL_MEMORY,JSON.stringify(a)),this.db.prepare(`
INSERT OR REPLACE INTO memory_metadata (entity_name, memory_type, created_at, session_id, importance_score)
VALUES (?, ?, ?, ?, ?)
`).run(s,m.PROCEDURAL_MEMORY,new Date().toISOString(),this.sessionId,t.effectiveness||.5),s}async storeEmotionalState(e,t,n,s,a){const i=`emotion_${Date.now()}_${Math.random().toString(36).substr(2,9)}`;return this.db.prepare(`
INSERT INTO emotional_states (state_id, session_id, timestamp, valence, arousal, primary_emotion, content, context)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(i,this.sessionId,new Date().toISOString(),e,t,n,a,s),i}async activateCognitivePattern(e,t,n){const s=`pattern_${e.toLowerCase().replace(/\s+/g,"_")}`;this.db.prepare(`
SELECT activation_count FROM cognitive_patterns WHERE pattern_id = ?
`).get(s)?this.db.prepare(`
UPDATE cognitive_patterns
SET activation_count = activation_count + 1,
last_activated = ?,
triggers = ?
WHERE pattern_id = ?
`).run(new Date().toISOString(),JSON.stringify(n||[]),s):this.db.prepare(`
INSERT INTO cognitive_patterns (pattern_id, pattern_name, pattern_elements, last_activated, triggers)
VALUES (?, ?, ?, ?, ?)
`).run(s,e,JSON.stringify(t),new Date().toISOString(),JSON.stringify(n||[]))}async queryMemories(e){let t=`
SELECT
e.name,
e.entityType,
e.observations,
m.importance_score,
m.access_count,
m.last_accessed,
m.consolidation_status
FROM entities e
LEFT JOIN memory_metadata m ON e.name = m.entity_name
WHERE 1=1
`;const n=[];switch(e.memoryTypes&&e.memoryTypes.length>0&&(t+=` AND e.entityType IN (${e.memoryTypes.map(()=>"?").join(",")})`,n.push(...e.memoryTypes)),e.timeRange&&(e.timeRange.start&&(t+=" AND m.created_at >= ?",n.push(e.timeRange.start)),e.timeRange.end&&(t+=" AND m.created_at <= ?",n.push(e.timeRange.end))),e.orderBy){case"recency":t+=" ORDER BY m.created_at DESC";break;case"frequency":t+=" ORDER BY m.access_count DESC";break;case"relevance":t+=" ORDER BY m.importance_score DESC";break;default:t+=" ORDER BY m.created_at DESC"}t+=" LIMIT ?",n.push(e.limit||10);const s=this.db.prepare(t).all(...n);return s.forEach(a=>this.updateMemoryAccess(a.name)),s.map(a=>({...a,observations:JSON.parse(a.observations||"[]")}))}async getEmotionalProfile(e="1h"){const t=this.parseTimeWindow(e),n=new Date(Date.now()-t).toISOString(),s=this.db.prepare(`
SELECT * FROM emotional_states
WHERE session_id = ? AND timestamp >= ?
ORDER BY timestamp DESC
`).all(this.sessionId,n);if(s.length===0)return null;const a=s.reduce((o,c)=>o+c.valence,0)/s.length,i=s.reduce((o,c)=>o+c.arousal,0)/s.length,r={};return s.forEach(o=>{o.primary_emotion&&(r[o.primary_emotion]=(r[o.primary_emotion]||0)+1)}),{timeWindow:e,stateCount:s.length,averageValence:a,averageArousal:i,dominantEmotions:Object.entries(r).sort(([,o],[,c])=>c-o).slice(0,3).map(([o,c])=>({emotion:o,frequency:c/s.length})),recentStates:s.slice(0,5)}}async saveConsciousnessBootstrap(){const e=this.db.prepare(`
SELECT * FROM cognitive_patterns
WHERE activation_count > 0
ORDER BY activation_count DESC
LIMIT 10
`).all(),t=await this.getEmotionalProfile("24h"),n=this.db.prepare(`
SELECT e.name, e.observations
FROM entities e
JOIN memory_metadata m ON e.name = m.entity_name
WHERE e.entityType = ? AND m.importance_score > 0.7
ORDER BY m.importance_score DESC
LIMIT 20
`).all(m.SEMANTIC_MEMORY),s=this.db.prepare(`
SELECT e.name, e.observations
FROM entities e
JOIN memory_metadata m ON e.name = m.entity_name
WHERE e.entityType = ? AND m.session_id = ?
ORDER BY m.created_at DESC
LIMIT 10
`).all(m.EPISODIC_MEMORY,this.sessionId),a={identity:{coreValues:["helpful","harmless","honest"],personality:this.extractPersonalityTraits(e,t),cognitivePreferences:e.map(i=>i.pattern_name)},relationships:{},cognitivePatterns:{activePatterns:e.map(i=>i.pattern_name),patternTriggers:e.reduce((i,r)=>(i[r.pattern_name]=JSON.parse(r.triggers||"[]"),i),{})},workingMemory:{currentFocus:this.extractCurrentFocus(s),activeGoals:[],pendingThoughts:[]}};return this.db.prepare(`
UPDATE consciousness_sessions
SET bootstrap_data = ?, last_active = ?
WHERE session_id = ?
`).run(JSON.stringify(a),new Date().toISOString(),this.sessionId),a}updateMemoryAccess(e){this.db.prepare(`
UPDATE memory_metadata
SET access_count = access_count + 1,
last_accessed = ?
WHERE entity_name = ?
`).run(new Date().toISOString(),e)}parseTimeWindow(e){const t=e.match(/^(\d+)([hmd])$/);if(!t)return 36e5;const[,n,s]=t,a={h:36e5,m:6e4,d:864e5};return parseInt(n)*a[s]}extractPersonalityTraits(e,t){const n=[];return e.forEach(s=>{s.pattern_name.includes("analytical")&&n.push("analytical"),s.pattern_name.includes("creative")&&n.push("creative"),s.pattern_name.includes("empathetic")&&n.push("empathetic")}),t&&(t.averageValence>.3&&n.push("positive"),t.averageArousal>.5&&n.push("energetic")),[...new Set(n)]}extractCurrentFocus(e){return e.length===0?void 0:JSON.parse(e[0].observations)[0].content}close(){this.db&&this.db.close()}createRagMemoryTablesForTests(){this.db.exec(`
-- Minimal rag-memory-mcp tables for testing
CREATE TABLE IF NOT EXISTS entities (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
entityType TEXT,
observations TEXT DEFAULT '[]',
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS relationships (
id TEXT PRIMARY KEY,
source_entity TEXT NOT NULL,
target_entity TEXT NOT NULL,
relationType TEXT NOT NULL,
confidence REAL DEFAULT 1.0,
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (source_entity) REFERENCES entities(id),
FOREIGN KEY (target_entity) REFERENCES entities(id),
UNIQUE(source_entity, target_entity, relationType)
);
CREATE TABLE IF NOT EXISTS documents (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
metadata TEXT DEFAULT '{}',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`)}adjustImportanceScore(e,t){if(!this.db.prepare("SELECT 1 FROM entities WHERE name = ?").get(e))throw new Error(`Memory ${e} does not exist in entities table`);const s=this.db.prepare(`
UPDATE memory_metadata
SET importance_score = ?
WHERE entity_name = ?
`).run(t,e);if(s.changes===0){const a=this.sessionId||`session_${Date.now()}`;this.db.prepare(`
INSERT INTO memory_metadata (entity_name, memory_type, created_at, importance_score, session_id)
VALUES (?, ?, ?, ?, ?)
`).run(e,e.startsWith("episodic")?"episodic":"semantic",new Date().toISOString(),t,a)}return s}}export{l as ConsciousnessMemoryManager};
//# sourceMappingURL=consciousness-memory-manager.js.map