vesper-memory 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -4
- package/CHANGELOG.md +129 -3
- package/LICENSE +59 -0
- package/README.md +422 -24
- package/config/migrations/001_add_skill_summary.sql +75 -0
- package/config/migrations/001_rollback_skill_summary.sql +60 -0
- package/config/sqlite-schema.sql +68 -4
- package/dist/benchmark/types.d.ts +4 -4
- package/dist/cli.js +204 -10
- package/dist/cli.js.map +1 -1
- package/dist/consolidation/pipeline.d.ts +6 -1
- package/dist/consolidation/pipeline.d.ts.map +1 -1
- package/dist/consolidation/pipeline.js +53 -35
- package/dist/consolidation/pipeline.js.map +1 -1
- package/dist/memory-layers/semantic-memory.d.ts +9 -19
- package/dist/memory-layers/semantic-memory.d.ts.map +1 -1
- package/dist/memory-layers/semantic-memory.js +31 -54
- package/dist/memory-layers/semantic-memory.js.map +1 -1
- package/dist/memory-layers/skill-lazy-loading.d.ts +160 -0
- package/dist/memory-layers/skill-lazy-loading.d.ts.map +1 -0
- package/dist/memory-layers/skill-lazy-loading.js +247 -0
- package/dist/memory-layers/skill-lazy-loading.js.map +1 -0
- package/dist/memory-layers/skill-library.d.ts +133 -4
- package/dist/memory-layers/skill-library.d.ts.map +1 -1
- package/dist/memory-layers/skill-library.js +427 -8
- package/dist/memory-layers/skill-library.js.map +1 -1
- package/dist/memory-layers/working-memory.d.ts +35 -14
- package/dist/memory-layers/working-memory.d.ts.map +1 -1
- package/dist/memory-layers/working-memory.js +114 -36
- package/dist/memory-layers/working-memory.js.map +1 -1
- package/dist/retrieval/hybrid-search.d.ts +2 -2
- package/dist/retrieval/hybrid-search.d.ts.map +1 -1
- package/dist/retrieval/hybrid-search.js +4 -3
- package/dist/retrieval/hybrid-search.js.map +1 -1
- package/dist/router/smart-router.d.ts +25 -0
- package/dist/router/smart-router.d.ts.map +1 -1
- package/dist/router/smart-router.js +90 -10
- package/dist/router/smart-router.js.map +1 -1
- package/dist/scheduler/consolidation-scheduler.d.ts +129 -0
- package/dist/scheduler/consolidation-scheduler.d.ts.map +1 -0
- package/dist/scheduler/consolidation-scheduler.js +179 -0
- package/dist/scheduler/consolidation-scheduler.js.map +1 -0
- package/dist/scheduler/index.d.ts +7 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +7 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server-wrapper.js +0 -0
- package/dist/server.js +892 -30
- package/dist/server.js.map +1 -1
- package/dist/synthesis/conflict-detector.d.ts +4 -6
- package/dist/synthesis/conflict-detector.d.ts.map +1 -1
- package/dist/synthesis/conflict-detector.js +19 -21
- package/dist/synthesis/conflict-detector.js.map +1 -1
- package/dist/utils/paths.d.ts +72 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +160 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/validation.d.ts +123 -1
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +146 -7
- package/dist/utils/validation.js.map +1 -1
- package/docker-compose.yml +11 -13
- package/embedding-service/README.md +8 -0
- package/package.json +6 -3
- package/scripts/FRESH_INSTALL.md +1 -1
- package/scripts/ensure-infrastructure.sh +86 -15
- package/scripts/health-check.sh +2 -1
- package/scripts/stop-infrastructure.sh +23 -0
- package/scripts/test-mcp-server.sh +2 -1
package/dist/server.js
CHANGED
|
@@ -16,10 +16,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema, McpError, ErrorCode, } f
|
|
|
16
16
|
import Redis from "ioredis";
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
import { randomUUID } from "crypto";
|
|
19
|
-
import { validateInput, StoreMemoryInputSchema, RetrieveMemoryInputSchema, ListRecentInputSchema, GetStatsInputSchema, } from "./utils/validation.js";
|
|
19
|
+
import { validateInput, StoreMemoryInputSchema, RetrieveMemoryInputSchema, ListRecentInputSchema, GetStatsInputSchema, RecordSkillOutcomeInputSchema, LoadSkillInputSchema, ShareContextInputSchema, StoreDecisionInputSchema, NamespaceStatsInputSchema, } from "./utils/validation.js";
|
|
20
|
+
import { getSqlitePath, ensureDirectories } from "./utils/paths.js";
|
|
20
21
|
import { createEmbeddingClient } from "./embeddings/client.js";
|
|
21
22
|
import { HybridSearchEngine } from "./retrieval/hybrid-search.js";
|
|
22
23
|
import { WorkingMemoryLayer } from "./memory-layers/working-memory.js";
|
|
24
|
+
import { SemanticMemoryLayer } from "./memory-layers/semantic-memory.js";
|
|
25
|
+
import { SkillLibrary } from "./memory-layers/skill-library.js";
|
|
26
|
+
import { ConsolidationPipeline } from "./consolidation/pipeline.js";
|
|
23
27
|
import * as SmartRouter from "./router/smart-router.js";
|
|
24
28
|
// Import better-sqlite3 constructor using createRequire
|
|
25
29
|
const require = createRequire(import.meta.url);
|
|
@@ -33,6 +37,10 @@ let vesperEnabled = true;
|
|
|
33
37
|
/**
|
|
34
38
|
* Tool definitions for the MCP server
|
|
35
39
|
*/
|
|
40
|
+
const NAMESPACE_PROP = {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Namespace for multi-agent isolation (default: 'default')",
|
|
43
|
+
};
|
|
36
44
|
const TOOLS = [
|
|
37
45
|
{
|
|
38
46
|
name: "store_memory",
|
|
@@ -46,7 +54,7 @@ const TOOLS = [
|
|
|
46
54
|
},
|
|
47
55
|
memory_type: {
|
|
48
56
|
type: "string",
|
|
49
|
-
enum: ["episodic", "semantic", "procedural"],
|
|
57
|
+
enum: ["episodic", "semantic", "procedural", "decision"],
|
|
50
58
|
description: "Type of memory being stored",
|
|
51
59
|
},
|
|
52
60
|
metadata: {
|
|
@@ -54,6 +62,10 @@ const TOOLS = [
|
|
|
54
62
|
description: "Additional metadata (timestamp, source, tags, etc.)",
|
|
55
63
|
additionalProperties: true,
|
|
56
64
|
},
|
|
65
|
+
namespace: NAMESPACE_PROP,
|
|
66
|
+
agent_id: { type: "string", description: "ID of the agent storing this memory" },
|
|
67
|
+
agent_role: { type: "string", description: "Role of the agent (e.g., 'orchestrator', 'code-reviewer')" },
|
|
68
|
+
task_id: { type: "string", description: "Task ID this memory is associated with" },
|
|
57
69
|
},
|
|
58
70
|
required: ["content", "memory_type"],
|
|
59
71
|
},
|
|
@@ -71,7 +83,7 @@ const TOOLS = [
|
|
|
71
83
|
memory_types: {
|
|
72
84
|
type: "array",
|
|
73
85
|
items: { type: "string" },
|
|
74
|
-
description: "Filter by memory types (episodic, semantic, procedural). Omit for all types.",
|
|
86
|
+
description: "Filter by memory types (episodic, semantic, procedural, decision). Omit for all types.",
|
|
75
87
|
},
|
|
76
88
|
max_results: {
|
|
77
89
|
type: "number",
|
|
@@ -82,6 +94,10 @@ const TOOLS = [
|
|
|
82
94
|
enum: ["fast_path", "semantic", "full_text", "graph"],
|
|
83
95
|
description: "Retrieval strategy to use (default: auto-select)",
|
|
84
96
|
},
|
|
97
|
+
namespace: NAMESPACE_PROP,
|
|
98
|
+
agent_id: { type: "string", description: "Filter by agent ID" },
|
|
99
|
+
task_id: { type: "string", description: "Filter by task ID" },
|
|
100
|
+
exclude_agent: { type: "string", description: "Exclude memories from this agent" },
|
|
85
101
|
},
|
|
86
102
|
required: ["query"],
|
|
87
103
|
},
|
|
@@ -100,6 +116,7 @@ const TOOLS = [
|
|
|
100
116
|
type: "string",
|
|
101
117
|
description: "Optional filter by memory type",
|
|
102
118
|
},
|
|
119
|
+
namespace: NAMESPACE_PROP,
|
|
103
120
|
},
|
|
104
121
|
},
|
|
105
122
|
},
|
|
@@ -113,6 +130,7 @@ const TOOLS = [
|
|
|
113
130
|
type: "boolean",
|
|
114
131
|
description: "Include detailed per-layer statistics (default: false)",
|
|
115
132
|
},
|
|
133
|
+
namespace: NAMESPACE_PROP,
|
|
116
134
|
},
|
|
117
135
|
},
|
|
118
136
|
},
|
|
@@ -140,6 +158,98 @@ const TOOLS = [
|
|
|
140
158
|
properties: {},
|
|
141
159
|
},
|
|
142
160
|
},
|
|
161
|
+
{
|
|
162
|
+
name: "record_skill_outcome",
|
|
163
|
+
description: "Record success or failure feedback for a skill execution. Used to improve skill ranking over time.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
skill_id: {
|
|
168
|
+
type: "string",
|
|
169
|
+
description: "The ID of the skill to record feedback for",
|
|
170
|
+
},
|
|
171
|
+
outcome: {
|
|
172
|
+
type: "string",
|
|
173
|
+
enum: ["success", "failure"],
|
|
174
|
+
description: "Whether the skill execution was successful or not",
|
|
175
|
+
},
|
|
176
|
+
satisfaction: {
|
|
177
|
+
type: "number",
|
|
178
|
+
description: "User satisfaction score (0-1). Required for success outcome.",
|
|
179
|
+
},
|
|
180
|
+
namespace: NAMESPACE_PROP,
|
|
181
|
+
},
|
|
182
|
+
required: ["skill_id", "outcome"],
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "load_skill",
|
|
187
|
+
description: "Load full skill implementation on-demand. Use when a skill summary needs complete details for execution. Loaded skills are cached for the session.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
skill_id: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "The ID of the skill to load",
|
|
194
|
+
},
|
|
195
|
+
namespace: NAMESPACE_PROP,
|
|
196
|
+
},
|
|
197
|
+
required: ["skill_id"],
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: "share_context",
|
|
202
|
+
description: "Share context (memories, skills, entities) from one namespace to another. Used for agent handoffs in multi-agent workflows.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
source_namespace: { type: "string", description: "Namespace to share from" },
|
|
207
|
+
target_namespace: { type: "string", description: "Namespace to share to" },
|
|
208
|
+
task_id: { type: "string", description: "Filter source memories by task ID" },
|
|
209
|
+
query: { type: "string", description: "Semantic query to select relevant memories" },
|
|
210
|
+
max_items: { type: "number", description: "Max items to share (default: 10)" },
|
|
211
|
+
include_skills: { type: "boolean", description: "Include matching skills (default: false)" },
|
|
212
|
+
include_entities: { type: "boolean", description: "Include related entities (default: true)" },
|
|
213
|
+
},
|
|
214
|
+
required: ["source_namespace", "target_namespace"],
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "store_decision",
|
|
219
|
+
description: "Store an architectural or design decision with reduced decay and automatic conflict detection against existing decisions.",
|
|
220
|
+
inputSchema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
content: { type: "string", description: "The decision content" },
|
|
224
|
+
namespace: NAMESPACE_PROP,
|
|
225
|
+
agent_id: { type: "string", description: "ID of the agent making this decision" },
|
|
226
|
+
agent_role: { type: "string", description: "Role of the agent" },
|
|
227
|
+
task_id: { type: "string", description: "Related task ID" },
|
|
228
|
+
supersedes: { type: "string", description: "ID of a previous decision this replaces" },
|
|
229
|
+
metadata: { type: "object", description: "Additional metadata", additionalProperties: true },
|
|
230
|
+
},
|
|
231
|
+
required: ["content"],
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "list_namespaces",
|
|
236
|
+
description: "List all namespaces with memory counts. Useful for discovering active agent namespaces.",
|
|
237
|
+
inputSchema: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "namespace_stats",
|
|
244
|
+
description: "Get detailed statistics for a specific namespace including memory counts, entity counts, skill counts, and active agents.",
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: "object",
|
|
247
|
+
properties: {
|
|
248
|
+
namespace: { type: "string", description: "The namespace to get stats for" },
|
|
249
|
+
},
|
|
250
|
+
required: ["namespace"],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
143
253
|
];
|
|
144
254
|
/**
|
|
145
255
|
* Initialize connections to Redis, Qdrant, and SQLite
|
|
@@ -180,12 +290,22 @@ async function initializeConnections() {
|
|
|
180
290
|
}
|
|
181
291
|
// Initialize SQLite (metadata store)
|
|
182
292
|
try {
|
|
183
|
-
|
|
293
|
+
// Ensure ~/.vesper directories exist (for user-level storage)
|
|
294
|
+
ensureDirectories();
|
|
295
|
+
// Use SQLITE_DB env if set, otherwise use user-level path (~/.vesper/data/memory.db)
|
|
296
|
+
const isUserLevel = !process.env.SQLITE_DB;
|
|
297
|
+
const dbPath = process.env.SQLITE_DB || getSqlitePath();
|
|
184
298
|
connections.sqlite = new DatabaseConstructor(dbPath);
|
|
185
299
|
connections.sqlite.pragma("journal_mode = WAL");
|
|
186
300
|
// Initialize schema
|
|
187
301
|
initializeSqliteSchema();
|
|
188
|
-
|
|
302
|
+
// Clear logging about storage location
|
|
303
|
+
if (isUserLevel) {
|
|
304
|
+
console.error(`[INFO] SQLite database initialized at ${dbPath} (user-level storage)`);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.error(`[INFO] SQLite database initialized at ${dbPath} (SQLITE_DB override)`);
|
|
308
|
+
}
|
|
189
309
|
}
|
|
190
310
|
catch (err) {
|
|
191
311
|
console.error("[WARN] SQLite initialization failed:", err instanceof Error ? err.message : String(err));
|
|
@@ -233,10 +353,71 @@ async function initializeConnections() {
|
|
|
233
353
|
console.error("\x1b[32m💡 Solution:\x1b[0m Start Qdrant with: docker-compose up -d qdrant");
|
|
234
354
|
connections.hybridSearch = undefined;
|
|
235
355
|
}
|
|
236
|
-
// Initialize
|
|
356
|
+
// Initialize Semantic Memory Layer (requires SQLite)
|
|
357
|
+
if (connections.sqlite) {
|
|
358
|
+
try {
|
|
359
|
+
// Initialize semantic memory schema
|
|
360
|
+
initializeSemanticMemorySchema();
|
|
361
|
+
connections.semanticMemory = new SemanticMemoryLayer(connections.sqlite);
|
|
362
|
+
console.error("[INFO] ✅ Semantic memory layer initialized");
|
|
363
|
+
}
|
|
364
|
+
catch (err) {
|
|
365
|
+
console.error("[WARN] Semantic memory initialization failed:", err instanceof Error ? err.message : String(err));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Initialize Skill Library (requires SQLite)
|
|
369
|
+
if (connections.sqlite) {
|
|
370
|
+
try {
|
|
371
|
+
// Initialize skill library schema
|
|
372
|
+
initializeSkillLibrarySchema();
|
|
373
|
+
connections.skillLibrary = new SkillLibrary(connections.sqlite);
|
|
374
|
+
console.error("[INFO] ✅ Skill library initialized");
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
console.error("[WARN] Skill library initialization failed:", err instanceof Error ? err.message : String(err));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
// Initialize Consolidation Pipeline (requires all memory layers)
|
|
381
|
+
if (connections.workingMemory && connections.semanticMemory && connections.skillLibrary && connections.sqlite) {
|
|
382
|
+
try {
|
|
383
|
+
connections.consolidationPipeline = new ConsolidationPipeline(connections.workingMemory, connections.semanticMemory, connections.skillLibrary, connections.sqlite);
|
|
384
|
+
console.error("[INFO] ✅ Consolidation pipeline initialized");
|
|
385
|
+
// Run startup consolidation asynchronously (non-blocking)
|
|
386
|
+
// This runs every time the MCP server starts (when Claude Code restarts)
|
|
387
|
+
setImmediate(async () => {
|
|
388
|
+
try {
|
|
389
|
+
console.error("[INFO] 🔄 Running startup consolidation...");
|
|
390
|
+
const stats = await connections.consolidationPipeline.consolidate();
|
|
391
|
+
console.error(`[INFO] ✅ Startup consolidation complete: ${stats.memoriesProcessed} memories, ` +
|
|
392
|
+
`${stats.entitiesExtracted} entities, ${stats.conflictsDetected} conflicts, ` +
|
|
393
|
+
`${stats.skillsExtracted} skills in ${stats.duration}ms`);
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
console.error("[WARN] Startup consolidation failed:", err instanceof Error ? err.message : String(err));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
console.error("[WARN] Consolidation pipeline initialization failed:", err instanceof Error ? err.message : String(err));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
console.error("[WARN] Consolidation pipeline not initialized (missing dependencies)");
|
|
406
|
+
}
|
|
407
|
+
// Initialize Smart Router with working memory and skill library
|
|
237
408
|
SmartRouter.init({
|
|
238
409
|
workingMemory: connections.workingMemory,
|
|
239
410
|
});
|
|
411
|
+
// Initialize skill handler in router
|
|
412
|
+
if (connections.skillLibrary) {
|
|
413
|
+
SmartRouter.initSkillHandler(connections.skillLibrary);
|
|
414
|
+
console.error("[INFO] ✅ Smart router skill handler initialized");
|
|
415
|
+
}
|
|
416
|
+
// Initialize preference handler in router
|
|
417
|
+
if (connections.semanticMemory) {
|
|
418
|
+
SmartRouter.initPreferenceHandler(connections.semanticMemory);
|
|
419
|
+
console.error("[INFO] ✅ Smart router preference handler initialized");
|
|
420
|
+
}
|
|
240
421
|
console.error("[INFO] ✅ Smart router initialized");
|
|
241
422
|
// Print service status summary
|
|
242
423
|
console.error("\n" + "=".repeat(60));
|
|
@@ -288,6 +469,280 @@ function initializeSqliteSchema() {
|
|
|
288
469
|
updated_at INTEGER
|
|
289
470
|
);
|
|
290
471
|
`);
|
|
472
|
+
// v0.5.0 namespace migration for memories table
|
|
473
|
+
const memoryNamespaceCols = [
|
|
474
|
+
"ALTER TABLE memories ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
475
|
+
"ALTER TABLE memories ADD COLUMN agent_id TEXT",
|
|
476
|
+
"ALTER TABLE memories ADD COLUMN agent_role TEXT",
|
|
477
|
+
"ALTER TABLE memories ADD COLUMN task_id TEXT",
|
|
478
|
+
];
|
|
479
|
+
for (const sql of memoryNamespaceCols) {
|
|
480
|
+
try {
|
|
481
|
+
connections.sqlite.exec(sql);
|
|
482
|
+
}
|
|
483
|
+
catch (_) { /* column already exists */ }
|
|
484
|
+
}
|
|
485
|
+
connections.sqlite.exec(`
|
|
486
|
+
CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories(namespace);
|
|
487
|
+
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(namespace, agent_id);
|
|
488
|
+
`);
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Initialize SQLite schema for semantic memory (HippoRAG knowledge graph)
|
|
492
|
+
*/
|
|
493
|
+
function initializeSemanticMemorySchema() {
|
|
494
|
+
if (!connections.sqlite) {
|
|
495
|
+
throw new Error("SQLite not initialized");
|
|
496
|
+
}
|
|
497
|
+
connections.sqlite.exec(`
|
|
498
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
499
|
+
id TEXT PRIMARY KEY,
|
|
500
|
+
name TEXT NOT NULL,
|
|
501
|
+
type TEXT NOT NULL,
|
|
502
|
+
description TEXT,
|
|
503
|
+
confidence REAL DEFAULT 1.0,
|
|
504
|
+
created_at TEXT NOT NULL,
|
|
505
|
+
last_accessed TEXT NOT NULL,
|
|
506
|
+
access_count INTEGER DEFAULT 1
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(type);
|
|
510
|
+
CREATE INDEX IF NOT EXISTS idx_entities_name ON entities(name);
|
|
511
|
+
|
|
512
|
+
CREATE TABLE IF NOT EXISTS relationships (
|
|
513
|
+
id TEXT PRIMARY KEY,
|
|
514
|
+
source_id TEXT NOT NULL,
|
|
515
|
+
target_id TEXT NOT NULL,
|
|
516
|
+
relation_type TEXT NOT NULL,
|
|
517
|
+
strength REAL DEFAULT 0.8,
|
|
518
|
+
evidence TEXT,
|
|
519
|
+
created_at TEXT NOT NULL,
|
|
520
|
+
last_reinforced TEXT NOT NULL
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_source ON relationships(source_id);
|
|
524
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_target ON relationships(target_id);
|
|
525
|
+
|
|
526
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
527
|
+
id TEXT PRIMARY KEY,
|
|
528
|
+
entity_id TEXT NOT NULL,
|
|
529
|
+
property TEXT NOT NULL,
|
|
530
|
+
value TEXT NOT NULL,
|
|
531
|
+
confidence REAL DEFAULT 1.0,
|
|
532
|
+
valid_from TEXT,
|
|
533
|
+
valid_until TEXT,
|
|
534
|
+
source_conversation TEXT
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity_id);
|
|
538
|
+
|
|
539
|
+
CREATE TABLE IF NOT EXISTS conflicts (
|
|
540
|
+
id TEXT PRIMARY KEY,
|
|
541
|
+
fact_id_1 TEXT,
|
|
542
|
+
fact_id_2 TEXT,
|
|
543
|
+
conflict_type TEXT,
|
|
544
|
+
description TEXT,
|
|
545
|
+
severity TEXT,
|
|
546
|
+
resolution_status TEXT
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
CREATE TABLE IF NOT EXISTS backup_metadata (
|
|
550
|
+
id TEXT PRIMARY KEY,
|
|
551
|
+
backup_type TEXT NOT NULL,
|
|
552
|
+
backup_path TEXT NOT NULL,
|
|
553
|
+
created_at TEXT NOT NULL,
|
|
554
|
+
expires_at TEXT NOT NULL
|
|
555
|
+
);
|
|
556
|
+
`);
|
|
557
|
+
// v0.5.0 namespace migration for semantic memory tables
|
|
558
|
+
const semanticNamespaceMigrations = [
|
|
559
|
+
"ALTER TABLE entities ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
560
|
+
"ALTER TABLE relationships ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
561
|
+
"ALTER TABLE facts ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
562
|
+
"ALTER TABLE conflicts ADD COLUMN namespace TEXT DEFAULT 'default'",
|
|
563
|
+
];
|
|
564
|
+
for (const sql of semanticNamespaceMigrations) {
|
|
565
|
+
try {
|
|
566
|
+
connections.sqlite.exec(sql);
|
|
567
|
+
}
|
|
568
|
+
catch (_) { /* column already exists */ }
|
|
569
|
+
}
|
|
570
|
+
connections.sqlite.exec(`
|
|
571
|
+
CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace);
|
|
572
|
+
CREATE INDEX IF NOT EXISTS idx_relationships_namespace ON relationships(namespace);
|
|
573
|
+
CREATE INDEX IF NOT EXISTS idx_facts_namespace ON facts(namespace);
|
|
574
|
+
CREATE INDEX IF NOT EXISTS idx_conflicts_namespace ON conflicts(namespace);
|
|
575
|
+
`);
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Initialize SQLite schema for skill library (procedural memory)
|
|
579
|
+
*/
|
|
580
|
+
function initializeSkillLibrarySchema() {
|
|
581
|
+
if (!connections.sqlite) {
|
|
582
|
+
throw new Error("SQLite not initialized");
|
|
583
|
+
}
|
|
584
|
+
connections.sqlite.exec(`
|
|
585
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
586
|
+
id TEXT PRIMARY KEY,
|
|
587
|
+
name TEXT NOT NULL,
|
|
588
|
+
description TEXT NOT NULL,
|
|
589
|
+
category TEXT NOT NULL,
|
|
590
|
+
triggers TEXT NOT NULL,
|
|
591
|
+
success_count INTEGER DEFAULT 0,
|
|
592
|
+
failure_count INTEGER DEFAULT 0,
|
|
593
|
+
avg_user_satisfaction REAL DEFAULT 0.5
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
CREATE INDEX IF NOT EXISTS idx_skills_category ON skills(category);
|
|
597
|
+
`);
|
|
598
|
+
// v0.5.0 namespace migration for skills table
|
|
599
|
+
try {
|
|
600
|
+
connections.sqlite.exec("ALTER TABLE skills ADD COLUMN namespace TEXT DEFAULT 'default'");
|
|
601
|
+
}
|
|
602
|
+
catch (_) { /* column already exists */ }
|
|
603
|
+
connections.sqlite.exec(`CREATE INDEX IF NOT EXISTS idx_skills_namespace ON skills(namespace);`);
|
|
604
|
+
// Apply lazy loading migration (idempotent - safe to run multiple times)
|
|
605
|
+
try {
|
|
606
|
+
connections.sqlite.exec(`
|
|
607
|
+
-- Add lazy loading columns if they don't exist
|
|
608
|
+
-- SQLite 3.35.0+ supports IF NOT EXISTS for ALTER TABLE ADD COLUMN
|
|
609
|
+
|
|
610
|
+
-- Add summary column (lightweight description for context injection)
|
|
611
|
+
ALTER TABLE skills ADD COLUMN summary TEXT;
|
|
612
|
+
`);
|
|
613
|
+
}
|
|
614
|
+
catch (err) {
|
|
615
|
+
// Column already exists, continue
|
|
616
|
+
}
|
|
617
|
+
try {
|
|
618
|
+
connections.sqlite.exec(`
|
|
619
|
+
ALTER TABLE skills ADD COLUMN is_archived INTEGER DEFAULT 0;
|
|
620
|
+
`);
|
|
621
|
+
}
|
|
622
|
+
catch (err) {
|
|
623
|
+
// Column already exists
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
connections.sqlite.exec(`
|
|
627
|
+
ALTER TABLE skills ADD COLUMN last_used TEXT;
|
|
628
|
+
`);
|
|
629
|
+
}
|
|
630
|
+
catch (err) {
|
|
631
|
+
// Column already exists
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
connections.sqlite.exec(`
|
|
635
|
+
ALTER TABLE skills ADD COLUMN created_at TEXT DEFAULT CURRENT_TIMESTAMP;
|
|
636
|
+
`);
|
|
637
|
+
}
|
|
638
|
+
catch (err) {
|
|
639
|
+
// Column already exists
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
connections.sqlite.exec(`
|
|
643
|
+
ALTER TABLE skills ADD COLUMN last_modified TEXT DEFAULT CURRENT_TIMESTAMP;
|
|
644
|
+
`);
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
// Column already exists
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
connections.sqlite.exec(`
|
|
651
|
+
ALTER TABLE skills ADD COLUMN version INTEGER DEFAULT 1;
|
|
652
|
+
`);
|
|
653
|
+
}
|
|
654
|
+
catch (err) {
|
|
655
|
+
// Column already exists
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
connections.sqlite.exec(`
|
|
659
|
+
ALTER TABLE skills ADD COLUMN code TEXT;
|
|
660
|
+
`);
|
|
661
|
+
}
|
|
662
|
+
catch (err) {
|
|
663
|
+
// Column already exists
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
connections.sqlite.exec(`
|
|
667
|
+
ALTER TABLE skills ADD COLUMN code_type TEXT DEFAULT 'reference';
|
|
668
|
+
`);
|
|
669
|
+
}
|
|
670
|
+
catch (err) {
|
|
671
|
+
// Column already exists
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
connections.sqlite.exec(`
|
|
675
|
+
ALTER TABLE skills ADD COLUMN prerequisites TEXT;
|
|
676
|
+
`);
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
// Column already exists
|
|
680
|
+
}
|
|
681
|
+
try {
|
|
682
|
+
connections.sqlite.exec(`
|
|
683
|
+
ALTER TABLE skills ADD COLUMN uses_skills TEXT;
|
|
684
|
+
`);
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
// Column already exists
|
|
688
|
+
}
|
|
689
|
+
try {
|
|
690
|
+
connections.sqlite.exec(`
|
|
691
|
+
ALTER TABLE skills ADD COLUMN used_by_skills TEXT;
|
|
692
|
+
`);
|
|
693
|
+
}
|
|
694
|
+
catch (err) {
|
|
695
|
+
// Column already exists
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
connections.sqlite.exec(`
|
|
699
|
+
ALTER TABLE skills ADD COLUMN created_from TEXT;
|
|
700
|
+
`);
|
|
701
|
+
}
|
|
702
|
+
catch (err) {
|
|
703
|
+
// Column already exists
|
|
704
|
+
}
|
|
705
|
+
try {
|
|
706
|
+
connections.sqlite.exec(`
|
|
707
|
+
ALTER TABLE skills ADD COLUMN notes TEXT;
|
|
708
|
+
`);
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
// Column already exists
|
|
712
|
+
}
|
|
713
|
+
// Create indexes for lazy loading (idempotent)
|
|
714
|
+
connections.sqlite.exec(`
|
|
715
|
+
CREATE INDEX IF NOT EXISTS idx_skills_lazy_loading
|
|
716
|
+
ON skills(is_archived, avg_user_satisfaction DESC, success_count DESC);
|
|
717
|
+
|
|
718
|
+
CREATE INDEX IF NOT EXISTS idx_skills_last_used
|
|
719
|
+
ON skills(last_used DESC);
|
|
720
|
+
|
|
721
|
+
CREATE INDEX IF NOT EXISTS idx_skills_category_quality
|
|
722
|
+
ON skills(category, avg_user_satisfaction DESC, success_count DESC);
|
|
723
|
+
`);
|
|
724
|
+
// Update existing skills with defaults
|
|
725
|
+
connections.sqlite.exec(`
|
|
726
|
+
UPDATE skills
|
|
727
|
+
SET summary = CASE
|
|
728
|
+
WHEN summary IS NULL AND length(description) <= 100 THEN description
|
|
729
|
+
WHEN summary IS NULL THEN substr(description, 1, 97) || '...'
|
|
730
|
+
ELSE summary
|
|
731
|
+
END
|
|
732
|
+
WHERE summary IS NULL OR summary = '';
|
|
733
|
+
|
|
734
|
+
UPDATE skills
|
|
735
|
+
SET created_at = CURRENT_TIMESTAMP
|
|
736
|
+
WHERE created_at IS NULL;
|
|
737
|
+
|
|
738
|
+
UPDATE skills
|
|
739
|
+
SET last_modified = CURRENT_TIMESTAMP
|
|
740
|
+
WHERE last_modified IS NULL;
|
|
741
|
+
|
|
742
|
+
UPDATE skills
|
|
743
|
+
SET is_archived = 0
|
|
744
|
+
WHERE is_archived IS NULL;
|
|
745
|
+
`);
|
|
291
746
|
}
|
|
292
747
|
/**
|
|
293
748
|
* Handle store_memory tool call
|
|
@@ -299,6 +754,7 @@ async function handleStoreMemory(input) {
|
|
|
299
754
|
try {
|
|
300
755
|
// Validate input
|
|
301
756
|
const validatedInput = validateInput(StoreMemoryInputSchema, input);
|
|
757
|
+
const namespace = validatedInput.namespace || 'default';
|
|
302
758
|
// Use pure UUID for Qdrant compatibility
|
|
303
759
|
const id = randomUUID();
|
|
304
760
|
const now = Date.now();
|
|
@@ -313,19 +769,22 @@ async function handleStoreMemory(input) {
|
|
|
313
769
|
console.error("[WARN] Failed to generate embedding (continuing without):", err instanceof Error ? err.message : String(err));
|
|
314
770
|
}
|
|
315
771
|
}
|
|
316
|
-
// Store in SQLite
|
|
772
|
+
// Store in SQLite with namespace and agent attribution
|
|
317
773
|
const stmt = connections.sqlite.prepare(`
|
|
318
|
-
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata)
|
|
319
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
774
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace, agent_id, agent_role, task_id)
|
|
775
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
320
776
|
`);
|
|
321
|
-
stmt.run(id, validatedInput.content, validatedInput.memory_type, now, now, JSON.stringify(validatedInput.metadata || {}));
|
|
322
|
-
// Store embedding in Qdrant if available
|
|
777
|
+
stmt.run(id, validatedInput.content, validatedInput.memory_type, now, now, JSON.stringify(validatedInput.metadata || {}), namespace, validatedInput.agent_id || null, validatedInput.agent_role || null, validatedInput.task_id || null);
|
|
778
|
+
// Store embedding in Qdrant if available (include namespace in payload)
|
|
323
779
|
if (embedding && connections.hybridSearch) {
|
|
324
780
|
try {
|
|
325
781
|
await connections.hybridSearch.upsertMemory(id, embedding, {
|
|
326
782
|
content: validatedInput.content,
|
|
327
783
|
memory_type: validatedInput.memory_type,
|
|
328
784
|
created_at: now,
|
|
785
|
+
namespace,
|
|
786
|
+
agent_id: validatedInput.agent_id,
|
|
787
|
+
task_id: validatedInput.task_id,
|
|
329
788
|
...validatedInput.metadata,
|
|
330
789
|
});
|
|
331
790
|
console.error(`[INFO] Stored embedding in Qdrant for ${id}`);
|
|
@@ -335,10 +794,11 @@ async function handleStoreMemory(input) {
|
|
|
335
794
|
throw new McpError(ErrorCode.InternalError, `Qdrant storage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
336
795
|
}
|
|
337
796
|
}
|
|
338
|
-
console.error(`[INFO] Memory stored: ${id} (type: ${validatedInput.memory_type}, length: ${validatedInput.content.length}, embedding: ${embedding ? "yes" : "no"})`);
|
|
797
|
+
console.error(`[INFO] Memory stored: ${id} (type: ${validatedInput.memory_type}, ns: ${namespace}, length: ${validatedInput.content.length}, embedding: ${embedding ? "yes" : "no"})`);
|
|
339
798
|
return {
|
|
340
799
|
success: true,
|
|
341
800
|
memory_id: id,
|
|
801
|
+
namespace,
|
|
342
802
|
timestamp: now,
|
|
343
803
|
message: `Memory stored successfully (${validatedInput.memory_type})`,
|
|
344
804
|
has_embedding: !!embedding,
|
|
@@ -358,18 +818,36 @@ async function handleRetrieveMemory(input) {
|
|
|
358
818
|
try {
|
|
359
819
|
// Validate input
|
|
360
820
|
const validatedInput = validateInput(RetrieveMemoryInputSchema, input);
|
|
821
|
+
const namespace = validatedInput.namespace || 'default';
|
|
361
822
|
const maxResults = validatedInput.max_results || 5;
|
|
823
|
+
// Build Qdrant filter for namespace
|
|
824
|
+
const qdrantFilter = {
|
|
825
|
+
must: [
|
|
826
|
+
{ key: "namespace", match: { value: namespace } },
|
|
827
|
+
...(validatedInput.agent_id ? [{ key: "agent_id", match: { value: validatedInput.agent_id } }] : []),
|
|
828
|
+
...(validatedInput.task_id ? [{ key: "task_id", match: { value: validatedInput.task_id } }] : []),
|
|
829
|
+
],
|
|
830
|
+
...(validatedInput.exclude_agent ? {
|
|
831
|
+
must_not: [{ key: "agent_id", match: { value: validatedInput.exclude_agent } }]
|
|
832
|
+
} : {}),
|
|
833
|
+
};
|
|
362
834
|
// Try semantic search if embedding service is available
|
|
363
835
|
if (connections.embeddingClient && connections.hybridSearch) {
|
|
364
836
|
try {
|
|
365
837
|
// Generate query embedding
|
|
366
838
|
const queryEmbedding = await connections.embeddingClient.embed(validatedInput.query);
|
|
367
|
-
// Perform hybrid search
|
|
368
|
-
|
|
369
|
-
|
|
839
|
+
// Perform hybrid search with namespace filter
|
|
840
|
+
let searchResults = await connections.hybridSearch.hybridSearch(queryEmbedding, maxResults, qdrantFilter);
|
|
841
|
+
// Fallback: if namespace is "default" and filtered search returns empty,
|
|
842
|
+
// retry without filter (backward compat for pre-namespace vectors)
|
|
843
|
+
if (searchResults.length === 0 && namespace === 'default') {
|
|
844
|
+
searchResults = await connections.hybridSearch.hybridSearch(queryEmbedding, maxResults);
|
|
845
|
+
}
|
|
846
|
+
console.error(`[INFO] Semantic search: query="${validatedInput.query}", ns=${namespace}, results=${searchResults.length}`);
|
|
370
847
|
return {
|
|
371
848
|
success: true,
|
|
372
849
|
query: validatedInput.query,
|
|
850
|
+
namespace,
|
|
373
851
|
routing_strategy: "semantic",
|
|
374
852
|
results: searchResults.map((r) => ({
|
|
375
853
|
id: r.id,
|
|
@@ -387,29 +865,45 @@ async function handleRetrieveMemory(input) {
|
|
|
387
865
|
console.error("[WARN] Semantic search failed, falling back to text search:", err instanceof Error ? err.message : String(err));
|
|
388
866
|
}
|
|
389
867
|
}
|
|
390
|
-
// Fallback: SQL-based text search
|
|
391
|
-
let query = "SELECT id, content, memory_type, created_at, metadata FROM memories";
|
|
392
|
-
const params = [];
|
|
868
|
+
// Fallback: SQL-based text search with namespace filter
|
|
869
|
+
let query = "SELECT id, content, memory_type, created_at, metadata, agent_id, task_id FROM memories WHERE namespace = ?";
|
|
870
|
+
const params = [namespace];
|
|
393
871
|
// Add type filter if specified
|
|
394
872
|
if (validatedInput.memory_types && validatedInput.memory_types.length > 0) {
|
|
395
873
|
const placeholders = validatedInput.memory_types.map(() => "?").join(",");
|
|
396
|
-
query += `
|
|
874
|
+
query += ` AND memory_type IN (${placeholders})`;
|
|
397
875
|
params.push(...validatedInput.memory_types);
|
|
398
876
|
}
|
|
877
|
+
// Add agent/task filters
|
|
878
|
+
if (validatedInput.agent_id) {
|
|
879
|
+
query += " AND agent_id = ?";
|
|
880
|
+
params.push(validatedInput.agent_id);
|
|
881
|
+
}
|
|
882
|
+
if (validatedInput.task_id) {
|
|
883
|
+
query += " AND task_id = ?";
|
|
884
|
+
params.push(validatedInput.task_id);
|
|
885
|
+
}
|
|
886
|
+
if (validatedInput.exclude_agent) {
|
|
887
|
+
query += " AND (agent_id IS NULL OR agent_id != ?)";
|
|
888
|
+
params.push(validatedInput.exclude_agent);
|
|
889
|
+
}
|
|
399
890
|
query += " ORDER BY created_at DESC LIMIT ?";
|
|
400
891
|
params.push(maxResults);
|
|
401
892
|
const stmt = connections.sqlite.prepare(query);
|
|
402
893
|
const results = stmt.all(...params);
|
|
403
|
-
console.error(`[INFO] Text search (fallback): query="${validatedInput.query}", results=${results.length}`);
|
|
894
|
+
console.error(`[INFO] Text search (fallback): query="${validatedInput.query}", ns=${namespace}, results=${results.length}`);
|
|
404
895
|
return {
|
|
405
896
|
success: true,
|
|
406
897
|
query: validatedInput.query,
|
|
898
|
+
namespace,
|
|
407
899
|
routing_strategy: "text_fallback",
|
|
408
900
|
results: results.map((r) => ({
|
|
409
901
|
id: r.id,
|
|
410
902
|
content: r.content,
|
|
411
903
|
memory_type: r.memory_type,
|
|
412
904
|
created_at: r.created_at,
|
|
905
|
+
agent_id: r.agent_id,
|
|
906
|
+
task_id: r.task_id,
|
|
413
907
|
metadata: JSON.parse(r.metadata || "{}"),
|
|
414
908
|
})),
|
|
415
909
|
count: results.length,
|
|
@@ -429,20 +923,22 @@ async function handleListRecent(input) {
|
|
|
429
923
|
try {
|
|
430
924
|
// Validate input
|
|
431
925
|
const validatedInput = validateInput(ListRecentInputSchema, input);
|
|
926
|
+
const namespace = validatedInput.namespace || 'default';
|
|
432
927
|
const limit = validatedInput.limit || 5;
|
|
433
|
-
let query = "SELECT id, content, memory_type, created_at FROM memories";
|
|
434
|
-
const params = [];
|
|
928
|
+
let query = "SELECT id, content, memory_type, created_at, agent_id, task_id FROM memories WHERE namespace = ?";
|
|
929
|
+
const params = [namespace];
|
|
435
930
|
if (validatedInput.memory_type) {
|
|
436
|
-
query += "
|
|
931
|
+
query += " AND memory_type = ?";
|
|
437
932
|
params.push(validatedInput.memory_type);
|
|
438
933
|
}
|
|
439
934
|
query += " ORDER BY created_at DESC LIMIT ?";
|
|
440
935
|
params.push(limit);
|
|
441
936
|
const stmt = connections.sqlite.prepare(query);
|
|
442
937
|
const results = stmt.all(...params);
|
|
443
|
-
console.error(`[INFO] Listed ${results.length} recent memories (limit=${limit}, type=${validatedInput.memory_type || "all"})`);
|
|
938
|
+
console.error(`[INFO] Listed ${results.length} recent memories (ns=${namespace}, limit=${limit}, type=${validatedInput.memory_type || "all"})`);
|
|
444
939
|
return {
|
|
445
940
|
success: true,
|
|
941
|
+
namespace,
|
|
446
942
|
limit,
|
|
447
943
|
memory_type_filter: validatedInput.memory_type || null,
|
|
448
944
|
entries: results.map((r) => ({
|
|
@@ -450,6 +946,8 @@ async function handleListRecent(input) {
|
|
|
450
946
|
content: r.content,
|
|
451
947
|
memory_type: r.memory_type,
|
|
452
948
|
created_at: r.created_at,
|
|
949
|
+
agent_id: r.agent_id,
|
|
950
|
+
task_id: r.task_id,
|
|
453
951
|
})),
|
|
454
952
|
count: results.length,
|
|
455
953
|
};
|
|
@@ -468,15 +966,17 @@ async function handleGetStats(input) {
|
|
|
468
966
|
try {
|
|
469
967
|
// Validate input
|
|
470
968
|
const validatedInput = validateInput(GetStatsInputSchema, input);
|
|
471
|
-
const
|
|
472
|
-
const
|
|
969
|
+
const namespace = validatedInput.namespace || 'default';
|
|
970
|
+
const countStmt = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ?");
|
|
971
|
+
const countResult = countStmt.get(namespace);
|
|
473
972
|
const totalMemories = countResult.count;
|
|
474
973
|
const typeStmt = connections.sqlite.prepare(`
|
|
475
|
-
SELECT memory_type, COUNT(*) as count FROM memories GROUP BY memory_type
|
|
974
|
+
SELECT memory_type, COUNT(*) as count FROM memories WHERE namespace = ? GROUP BY memory_type
|
|
476
975
|
`);
|
|
477
|
-
const typeResults = typeStmt.all();
|
|
976
|
+
const typeResults = typeStmt.all(namespace);
|
|
478
977
|
const stats = {
|
|
479
978
|
success: true,
|
|
979
|
+
namespace,
|
|
480
980
|
timestamp: Date.now(),
|
|
481
981
|
total_memories: totalMemories,
|
|
482
982
|
redis_connected: connections.redis ? true : false,
|
|
@@ -545,6 +1045,342 @@ async function handleVesperStatus() {
|
|
|
545
1045
|
: "Vesper is in pass-through mode (memory operations are skipped)",
|
|
546
1046
|
};
|
|
547
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Handle record_skill_outcome tool call
|
|
1050
|
+
*
|
|
1051
|
+
* Records success or failure feedback for a skill execution.
|
|
1052
|
+
* Used to improve skill ranking over time.
|
|
1053
|
+
*/
|
|
1054
|
+
async function handleRecordSkillOutcome(input) {
|
|
1055
|
+
if (!connections.skillLibrary) {
|
|
1056
|
+
throw new McpError(ErrorCode.InternalError, "Skill library not available");
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
// Validate input
|
|
1060
|
+
const validatedInput = validateInput(RecordSkillOutcomeInputSchema, input);
|
|
1061
|
+
if (validatedInput.outcome === "success") {
|
|
1062
|
+
if (validatedInput.satisfaction === undefined) {
|
|
1063
|
+
throw new McpError(ErrorCode.InvalidParams, "satisfaction is required for success outcome");
|
|
1064
|
+
}
|
|
1065
|
+
connections.skillLibrary.recordSuccess(validatedInput.skill_id, validatedInput.satisfaction);
|
|
1066
|
+
console.error(`[INFO] Recorded skill success: ${validatedInput.skill_id} (satisfaction: ${validatedInput.satisfaction})`);
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
connections.skillLibrary.recordFailure(validatedInput.skill_id);
|
|
1070
|
+
console.error(`[INFO] Recorded skill failure: ${validatedInput.skill_id}`);
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
success: true,
|
|
1074
|
+
skill_id: validatedInput.skill_id,
|
|
1075
|
+
outcome: validatedInput.outcome,
|
|
1076
|
+
satisfaction: validatedInput.satisfaction,
|
|
1077
|
+
message: `Skill ${validatedInput.outcome} recorded for ${validatedInput.skill_id}`,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
throw new McpError(ErrorCode.InternalError, `Failed to record skill outcome: ${err instanceof Error ? err.message : String(err)}`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Handle load_skill tool call
|
|
1086
|
+
*
|
|
1087
|
+
* Loads full skill implementation on-demand when a skill is invoked.
|
|
1088
|
+
* This is part of the lazy loading system to reduce token usage by 80-90%.
|
|
1089
|
+
*
|
|
1090
|
+
* Performance:
|
|
1091
|
+
* - First load: ~20ms (database query)
|
|
1092
|
+
* - Cached load: ~5ms (working memory cache hit)
|
|
1093
|
+
*
|
|
1094
|
+
* Caching:
|
|
1095
|
+
* - Loaded skills are cached in Redis for 1 hour (3600s)
|
|
1096
|
+
* - Cache automatically expires or can be invalidated
|
|
1097
|
+
* - Reduces database load and improves response time
|
|
1098
|
+
*
|
|
1099
|
+
* @param input - LoadSkillInput with skill_id
|
|
1100
|
+
* @returns Full skill implementation with all details
|
|
1101
|
+
*/
|
|
1102
|
+
async function handleLoadSkill(input) {
|
|
1103
|
+
if (!connections.skillLibrary) {
|
|
1104
|
+
throw new McpError(ErrorCode.InternalError, "Skill library not available");
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
// Validate input
|
|
1108
|
+
const validatedInput = validateInput(LoadSkillInputSchema, input);
|
|
1109
|
+
const startTime = Date.now();
|
|
1110
|
+
let fromCache = false;
|
|
1111
|
+
// Step 1: Check cache first (if working memory available)
|
|
1112
|
+
let fullSkill = null;
|
|
1113
|
+
if (connections.workingMemory) {
|
|
1114
|
+
try {
|
|
1115
|
+
const cached = await connections.workingMemory.getCachedSkill(validatedInput.skill_id);
|
|
1116
|
+
if (cached) {
|
|
1117
|
+
fullSkill = cached.skill;
|
|
1118
|
+
fromCache = true;
|
|
1119
|
+
console.error(`[INFO] Skill cache hit: ${fullSkill.name} (${validatedInput.skill_id}) ` +
|
|
1120
|
+
`access count: ${cached.access_count}`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
catch (err) {
|
|
1124
|
+
console.warn(`[WARN] Skill cache lookup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// Step 2: Load from database if not cached
|
|
1128
|
+
if (!fullSkill) {
|
|
1129
|
+
fullSkill = connections.skillLibrary.loadFull(validatedInput.skill_id);
|
|
1130
|
+
if (!fullSkill) {
|
|
1131
|
+
throw new McpError(ErrorCode.InvalidParams, `Skill not found: ${validatedInput.skill_id}`);
|
|
1132
|
+
}
|
|
1133
|
+
// Cache the loaded skill (if working memory available)
|
|
1134
|
+
if (connections.workingMemory) {
|
|
1135
|
+
try {
|
|
1136
|
+
await connections.workingMemory.cacheSkill(fullSkill, 3600); // 1 hour TTL
|
|
1137
|
+
}
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
console.warn(`[WARN] Failed to cache skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
const loadTime = Date.now() - startTime;
|
|
1144
|
+
console.error(`[INFO] Loaded full skill: ${fullSkill.name} (${validatedInput.skill_id}) ` +
|
|
1145
|
+
`in ${loadTime}ms (${fromCache ? 'cache' : 'database'})`);
|
|
1146
|
+
return {
|
|
1147
|
+
success: true,
|
|
1148
|
+
skill: {
|
|
1149
|
+
id: fullSkill.id,
|
|
1150
|
+
name: fullSkill.name,
|
|
1151
|
+
summary: fullSkill.summary,
|
|
1152
|
+
description: fullSkill.description,
|
|
1153
|
+
category: fullSkill.category,
|
|
1154
|
+
triggers: fullSkill.triggers,
|
|
1155
|
+
code: fullSkill.code,
|
|
1156
|
+
code_type: fullSkill.code_type,
|
|
1157
|
+
prerequisites: fullSkill.prerequisites,
|
|
1158
|
+
quality_score: fullSkill.quality_score,
|
|
1159
|
+
success_count: fullSkill.success_count,
|
|
1160
|
+
failure_count: fullSkill.failure_count,
|
|
1161
|
+
avg_user_satisfaction: fullSkill.avg_user_satisfaction,
|
|
1162
|
+
uses_skills: fullSkill.uses_skills,
|
|
1163
|
+
used_by_skills: fullSkill.used_by_skills,
|
|
1164
|
+
created_from: fullSkill.created_from,
|
|
1165
|
+
created_at: fullSkill.created_at.toISOString(),
|
|
1166
|
+
last_modified: fullSkill.last_modified.toISOString(),
|
|
1167
|
+
last_used: fullSkill.last_used?.toISOString(),
|
|
1168
|
+
version: fullSkill.version,
|
|
1169
|
+
notes: fullSkill.notes,
|
|
1170
|
+
},
|
|
1171
|
+
load_time_ms: loadTime,
|
|
1172
|
+
from_cache: fromCache,
|
|
1173
|
+
message: `Loaded full skill: ${fullSkill.name}${fromCache ? ' (cached)' : ''}`,
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
catch (err) {
|
|
1177
|
+
if (err instanceof McpError) {
|
|
1178
|
+
throw err;
|
|
1179
|
+
}
|
|
1180
|
+
throw new McpError(ErrorCode.InternalError, `Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* Handle share_context tool call
|
|
1185
|
+
*
|
|
1186
|
+
* Shares memories from one namespace to another for agent handoffs.
|
|
1187
|
+
*/
|
|
1188
|
+
async function handleShareContext(input) {
|
|
1189
|
+
if (!connections.sqlite) {
|
|
1190
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
const validatedInput = validateInput(ShareContextInputSchema, input);
|
|
1194
|
+
const { source_namespace, target_namespace } = validatedInput;
|
|
1195
|
+
const maxItems = validatedInput.max_items || 10;
|
|
1196
|
+
const now = Date.now();
|
|
1197
|
+
const sharedItems = [];
|
|
1198
|
+
// Query memories from source namespace
|
|
1199
|
+
let memQuery = "SELECT id, content, memory_type, created_at, metadata, agent_id, task_id FROM memories WHERE namespace = ?";
|
|
1200
|
+
const memParams = [source_namespace];
|
|
1201
|
+
if (validatedInput.task_id) {
|
|
1202
|
+
memQuery += " AND task_id = ?";
|
|
1203
|
+
memParams.push(validatedInput.task_id);
|
|
1204
|
+
}
|
|
1205
|
+
memQuery += " ORDER BY created_at DESC LIMIT ?";
|
|
1206
|
+
memParams.push(maxItems);
|
|
1207
|
+
const memories = connections.sqlite.prepare(memQuery).all(...memParams);
|
|
1208
|
+
// Copy memories to target namespace
|
|
1209
|
+
for (const mem of memories) {
|
|
1210
|
+
const newId = randomUUID();
|
|
1211
|
+
connections.sqlite.prepare(`
|
|
1212
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace, agent_id, agent_role, task_id)
|
|
1213
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1214
|
+
`).run(newId, mem.content, mem.memory_type, mem.created_at, now, mem.metadata, target_namespace, mem.agent_id, null, mem.task_id);
|
|
1215
|
+
sharedItems.push({ type: 'memory', id: newId, original_id: mem.id });
|
|
1216
|
+
}
|
|
1217
|
+
// Include entities if requested
|
|
1218
|
+
let entityCount = 0;
|
|
1219
|
+
if (validatedInput.include_entities) {
|
|
1220
|
+
const entities = connections.sqlite.prepare("SELECT * FROM entities WHERE namespace = ? LIMIT ?").all(source_namespace, maxItems);
|
|
1221
|
+
for (const entity of entities) {
|
|
1222
|
+
if (connections.semanticMemory) {
|
|
1223
|
+
connections.semanticMemory.upsertEntity({ name: entity.name, type: entity.type, description: entity.description, confidence: entity.confidence }, target_namespace);
|
|
1224
|
+
entityCount++;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
// Store handoff event in target namespace
|
|
1229
|
+
const handoffId = randomUUID();
|
|
1230
|
+
connections.sqlite.prepare(`
|
|
1231
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace)
|
|
1232
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1233
|
+
`).run(handoffId, `Context handoff from namespace '${source_namespace}': ${memories.length} memories, ${entityCount} entities shared`, 'episodic', now, now, JSON.stringify({ handoff: true, source_namespace, items_shared: sharedItems.length, entities_shared: entityCount }), target_namespace);
|
|
1234
|
+
console.error(`[INFO] Shared ${sharedItems.length} memories + ${entityCount} entities from ${source_namespace} to ${target_namespace}`);
|
|
1235
|
+
return {
|
|
1236
|
+
success: true,
|
|
1237
|
+
source_namespace,
|
|
1238
|
+
target_namespace,
|
|
1239
|
+
memories_shared: sharedItems.length,
|
|
1240
|
+
entities_shared: entityCount,
|
|
1241
|
+
handoff_id: handoffId,
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
catch (err) {
|
|
1245
|
+
throw new McpError(ErrorCode.InternalError, `Failed to share context: ${err instanceof Error ? err.message : String(err)}`);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Handle store_decision tool call
|
|
1250
|
+
*
|
|
1251
|
+
* Stores a decision with reduced decay and conflict detection.
|
|
1252
|
+
*/
|
|
1253
|
+
async function handleStoreDecision(input) {
|
|
1254
|
+
if (!connections.sqlite) {
|
|
1255
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
const validatedInput = validateInput(StoreDecisionInputSchema, input);
|
|
1259
|
+
const namespace = validatedInput.namespace || 'default';
|
|
1260
|
+
const id = randomUUID();
|
|
1261
|
+
const now = Date.now();
|
|
1262
|
+
// Build metadata with decay_factor
|
|
1263
|
+
const metadata = {
|
|
1264
|
+
...(validatedInput.metadata || {}),
|
|
1265
|
+
decay_factor: 0.25,
|
|
1266
|
+
supersedes: validatedInput.supersedes || null,
|
|
1267
|
+
};
|
|
1268
|
+
// Store as decision memory type
|
|
1269
|
+
connections.sqlite.prepare(`
|
|
1270
|
+
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace, agent_id, agent_role, task_id)
|
|
1271
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1272
|
+
`).run(id, validatedInput.content, 'decision', now, now, JSON.stringify(metadata), namespace, validatedInput.agent_id || null, validatedInput.agent_role || null, validatedInput.task_id || null);
|
|
1273
|
+
// Generate embedding and store in Qdrant
|
|
1274
|
+
let hasEmbedding = false;
|
|
1275
|
+
if (connections.embeddingClient && connections.hybridSearch) {
|
|
1276
|
+
try {
|
|
1277
|
+
const embedding = await connections.embeddingClient.embed(validatedInput.content);
|
|
1278
|
+
await connections.hybridSearch.upsertMemory(id, embedding, {
|
|
1279
|
+
content: validatedInput.content,
|
|
1280
|
+
memory_type: 'decision',
|
|
1281
|
+
created_at: now,
|
|
1282
|
+
namespace,
|
|
1283
|
+
agent_id: validatedInput.agent_id,
|
|
1284
|
+
task_id: validatedInput.task_id,
|
|
1285
|
+
decay_factor: 0.25,
|
|
1286
|
+
});
|
|
1287
|
+
hasEmbedding = true;
|
|
1288
|
+
}
|
|
1289
|
+
catch (err) {
|
|
1290
|
+
console.error("[WARN] Failed to generate embedding for decision:", err instanceof Error ? err.message : String(err));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
// If supersedes, mark the old decision's metadata
|
|
1294
|
+
if (validatedInput.supersedes) {
|
|
1295
|
+
const oldMem = connections.sqlite.prepare("SELECT metadata FROM memories WHERE id = ? AND namespace = ?").get(validatedInput.supersedes, namespace);
|
|
1296
|
+
if (oldMem) {
|
|
1297
|
+
const oldMeta = JSON.parse(oldMem.metadata || '{}');
|
|
1298
|
+
oldMeta.superseded_by = id;
|
|
1299
|
+
connections.sqlite.prepare("UPDATE memories SET metadata = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(oldMeta), now, validatedInput.supersedes);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
// Check for conflicting decisions in same namespace
|
|
1303
|
+
const existingDecisions = connections.sqlite.prepare("SELECT id, content FROM memories WHERE namespace = ? AND memory_type = 'decision' AND id != ? ORDER BY created_at DESC LIMIT 10").all(namespace, id);
|
|
1304
|
+
console.error(`[INFO] Decision stored: ${id} (ns: ${namespace}, supersedes: ${validatedInput.supersedes || 'none'})`);
|
|
1305
|
+
return {
|
|
1306
|
+
success: true,
|
|
1307
|
+
decision_id: id,
|
|
1308
|
+
namespace,
|
|
1309
|
+
timestamp: now,
|
|
1310
|
+
has_embedding: hasEmbedding,
|
|
1311
|
+
supersedes: validatedInput.supersedes || null,
|
|
1312
|
+
existing_decisions_count: existingDecisions.length,
|
|
1313
|
+
message: `Decision stored successfully`,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
catch (err) {
|
|
1317
|
+
throw new McpError(ErrorCode.InternalError, `Failed to store decision: ${err instanceof Error ? err.message : String(err)}`);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Handle list_namespaces tool call
|
|
1322
|
+
*/
|
|
1323
|
+
async function handleListNamespaces() {
|
|
1324
|
+
if (!connections.sqlite) {
|
|
1325
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
const namespaces = connections.sqlite.prepare(`
|
|
1329
|
+
SELECT namespace, COUNT(*) as memory_count,
|
|
1330
|
+
COUNT(DISTINCT agent_id) as agent_count,
|
|
1331
|
+
MAX(created_at) as last_activity
|
|
1332
|
+
FROM memories
|
|
1333
|
+
WHERE namespace IS NOT NULL
|
|
1334
|
+
GROUP BY namespace
|
|
1335
|
+
ORDER BY last_activity DESC
|
|
1336
|
+
`).all();
|
|
1337
|
+
return {
|
|
1338
|
+
success: true,
|
|
1339
|
+
namespaces: namespaces.map(ns => ({
|
|
1340
|
+
namespace: ns.namespace,
|
|
1341
|
+
memory_count: ns.memory_count,
|
|
1342
|
+
agent_count: ns.agent_count,
|
|
1343
|
+
last_activity: ns.last_activity,
|
|
1344
|
+
})),
|
|
1345
|
+
count: namespaces.length,
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
catch (err) {
|
|
1349
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list namespaces: ${err instanceof Error ? err.message : String(err)}`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/**
|
|
1353
|
+
* Handle namespace_stats tool call
|
|
1354
|
+
*/
|
|
1355
|
+
async function handleNamespaceStats(input) {
|
|
1356
|
+
if (!connections.sqlite) {
|
|
1357
|
+
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1358
|
+
}
|
|
1359
|
+
try {
|
|
1360
|
+
const validatedInput = validateInput(NamespaceStatsInputSchema, input);
|
|
1361
|
+
const namespace = validatedInput.namespace;
|
|
1362
|
+
const memoryCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ?").get(namespace).count;
|
|
1363
|
+
const typeBreakdown = connections.sqlite.prepare("SELECT memory_type, COUNT(*) as count FROM memories WHERE namespace = ? GROUP BY memory_type").all(namespace);
|
|
1364
|
+
const entityCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM entities WHERE namespace = ?").get(namespace).count;
|
|
1365
|
+
const skillCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM skills WHERE namespace = ?").get(namespace).count;
|
|
1366
|
+
const agents = connections.sqlite.prepare("SELECT DISTINCT agent_id, agent_role FROM memories WHERE namespace = ? AND agent_id IS NOT NULL").all(namespace);
|
|
1367
|
+
const decisionCount = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ? AND memory_type = 'decision'").get(namespace).count;
|
|
1368
|
+
return {
|
|
1369
|
+
success: true,
|
|
1370
|
+
namespace,
|
|
1371
|
+
memories: memoryCount,
|
|
1372
|
+
memory_type_breakdown: Object.fromEntries(typeBreakdown.map(t => [t.memory_type, t.count])),
|
|
1373
|
+
entities: entityCount,
|
|
1374
|
+
skills: skillCount,
|
|
1375
|
+
decisions: decisionCount,
|
|
1376
|
+
agents: agents.map(a => ({ agent_id: a.agent_id, agent_role: a.agent_role })),
|
|
1377
|
+
agent_count: agents.length,
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
catch (err) {
|
|
1381
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get namespace stats: ${err instanceof Error ? err.message : String(err)}`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
548
1384
|
/**
|
|
549
1385
|
* Process tool calls
|
|
550
1386
|
*/
|
|
@@ -578,6 +1414,18 @@ async function processTool(name, input) {
|
|
|
578
1414
|
return await handleListRecent(input);
|
|
579
1415
|
case "get_stats":
|
|
580
1416
|
return await handleGetStats(input);
|
|
1417
|
+
case "record_skill_outcome":
|
|
1418
|
+
return await handleRecordSkillOutcome(input);
|
|
1419
|
+
case "load_skill":
|
|
1420
|
+
return await handleLoadSkill(input);
|
|
1421
|
+
case "share_context":
|
|
1422
|
+
return await handleShareContext(input);
|
|
1423
|
+
case "store_decision":
|
|
1424
|
+
return await handleStoreDecision(input);
|
|
1425
|
+
case "list_namespaces":
|
|
1426
|
+
return await handleListNamespaces();
|
|
1427
|
+
case "namespace_stats":
|
|
1428
|
+
return await handleNamespaceStats(input);
|
|
581
1429
|
default:
|
|
582
1430
|
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
583
1431
|
}
|
|
@@ -592,7 +1440,7 @@ async function main() {
|
|
|
592
1440
|
// Create MCP server
|
|
593
1441
|
const server = new Server({
|
|
594
1442
|
name: "vesper",
|
|
595
|
-
version: "0.
|
|
1443
|
+
version: "0.5.0",
|
|
596
1444
|
}, {
|
|
597
1445
|
capabilities: {
|
|
598
1446
|
tools: {},
|
|
@@ -646,8 +1494,22 @@ async function main() {
|
|
|
646
1494
|
console.error("[INFO] Starting stdio transport...");
|
|
647
1495
|
await server.connect(transport);
|
|
648
1496
|
console.error("[INFO] Vesper running on stdio transport");
|
|
649
|
-
console.error("[INFO] Available tools: store_memory, retrieve_memory, list_recent, get_stats, vesper_enable, vesper_disable, vesper_status");
|
|
1497
|
+
console.error("[INFO] Available tools: store_memory, retrieve_memory, list_recent, get_stats, record_skill_outcome, load_skill, vesper_enable, vesper_disable, vesper_status");
|
|
1498
|
+
}
|
|
1499
|
+
// Cleanup handler for graceful shutdown
|
|
1500
|
+
function cleanup() {
|
|
1501
|
+
console.error('[INFO] Shutting down Vesper...');
|
|
1502
|
+
if (connections.redis) {
|
|
1503
|
+
connections.redis.disconnect();
|
|
1504
|
+
}
|
|
1505
|
+
if (connections.sqlite) {
|
|
1506
|
+
connections.sqlite.close();
|
|
1507
|
+
}
|
|
1508
|
+
console.error('[INFO] Vesper shutdown complete');
|
|
1509
|
+
process.exit(0);
|
|
650
1510
|
}
|
|
1511
|
+
process.on('SIGINT', cleanup);
|
|
1512
|
+
process.on('SIGTERM', cleanup);
|
|
651
1513
|
// Run the server
|
|
652
1514
|
main().catch((err) => {
|
|
653
1515
|
console.error("[FATAL] Server error:", err instanceof Error ? err.message : String(err));
|