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.
Files changed (69) hide show
  1. package/.env.example +8 -4
  2. package/CHANGELOG.md +129 -3
  3. package/LICENSE +59 -0
  4. package/README.md +422 -24
  5. package/config/migrations/001_add_skill_summary.sql +75 -0
  6. package/config/migrations/001_rollback_skill_summary.sql +60 -0
  7. package/config/sqlite-schema.sql +68 -4
  8. package/dist/benchmark/types.d.ts +4 -4
  9. package/dist/cli.js +204 -10
  10. package/dist/cli.js.map +1 -1
  11. package/dist/consolidation/pipeline.d.ts +6 -1
  12. package/dist/consolidation/pipeline.d.ts.map +1 -1
  13. package/dist/consolidation/pipeline.js +53 -35
  14. package/dist/consolidation/pipeline.js.map +1 -1
  15. package/dist/memory-layers/semantic-memory.d.ts +9 -19
  16. package/dist/memory-layers/semantic-memory.d.ts.map +1 -1
  17. package/dist/memory-layers/semantic-memory.js +31 -54
  18. package/dist/memory-layers/semantic-memory.js.map +1 -1
  19. package/dist/memory-layers/skill-lazy-loading.d.ts +160 -0
  20. package/dist/memory-layers/skill-lazy-loading.d.ts.map +1 -0
  21. package/dist/memory-layers/skill-lazy-loading.js +247 -0
  22. package/dist/memory-layers/skill-lazy-loading.js.map +1 -0
  23. package/dist/memory-layers/skill-library.d.ts +133 -4
  24. package/dist/memory-layers/skill-library.d.ts.map +1 -1
  25. package/dist/memory-layers/skill-library.js +427 -8
  26. package/dist/memory-layers/skill-library.js.map +1 -1
  27. package/dist/memory-layers/working-memory.d.ts +35 -14
  28. package/dist/memory-layers/working-memory.d.ts.map +1 -1
  29. package/dist/memory-layers/working-memory.js +114 -36
  30. package/dist/memory-layers/working-memory.js.map +1 -1
  31. package/dist/retrieval/hybrid-search.d.ts +2 -2
  32. package/dist/retrieval/hybrid-search.d.ts.map +1 -1
  33. package/dist/retrieval/hybrid-search.js +4 -3
  34. package/dist/retrieval/hybrid-search.js.map +1 -1
  35. package/dist/router/smart-router.d.ts +25 -0
  36. package/dist/router/smart-router.d.ts.map +1 -1
  37. package/dist/router/smart-router.js +90 -10
  38. package/dist/router/smart-router.js.map +1 -1
  39. package/dist/scheduler/consolidation-scheduler.d.ts +129 -0
  40. package/dist/scheduler/consolidation-scheduler.d.ts.map +1 -0
  41. package/dist/scheduler/consolidation-scheduler.js +179 -0
  42. package/dist/scheduler/consolidation-scheduler.js.map +1 -0
  43. package/dist/scheduler/index.d.ts +7 -0
  44. package/dist/scheduler/index.d.ts.map +1 -0
  45. package/dist/scheduler/index.js +7 -0
  46. package/dist/scheduler/index.js.map +1 -0
  47. package/dist/server-wrapper.js +0 -0
  48. package/dist/server.js +892 -30
  49. package/dist/server.js.map +1 -1
  50. package/dist/synthesis/conflict-detector.d.ts +4 -6
  51. package/dist/synthesis/conflict-detector.d.ts.map +1 -1
  52. package/dist/synthesis/conflict-detector.js +19 -21
  53. package/dist/synthesis/conflict-detector.js.map +1 -1
  54. package/dist/utils/paths.d.ts +72 -0
  55. package/dist/utils/paths.d.ts.map +1 -0
  56. package/dist/utils/paths.js +160 -0
  57. package/dist/utils/paths.js.map +1 -0
  58. package/dist/utils/validation.d.ts +123 -1
  59. package/dist/utils/validation.d.ts.map +1 -1
  60. package/dist/utils/validation.js +146 -7
  61. package/dist/utils/validation.js.map +1 -1
  62. package/docker-compose.yml +11 -13
  63. package/embedding-service/README.md +8 -0
  64. package/package.json +6 -3
  65. package/scripts/FRESH_INSTALL.md +1 -1
  66. package/scripts/ensure-infrastructure.sh +86 -15
  67. package/scripts/health-check.sh +2 -1
  68. package/scripts/stop-infrastructure.sh +23 -0
  69. 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
- const dbPath = process.env.SQLITE_DB || ":memory:";
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
- console.error("[INFO] SQLite database initialized at", dbPath);
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 Smart Router with working memory
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
- const searchResults = await connections.hybridSearch.hybridSearch(queryEmbedding, maxResults);
369
- console.error(`[INFO] Semantic search: query="${validatedInput.query}", results=${searchResults.length}`);
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 += ` WHERE memory_type IN (${placeholders})`;
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 += " WHERE memory_type = ?";
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 countStmt = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories");
472
- const countResult = countStmt.get();
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.1.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));