vesper-memory 0.4.0 → 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 (63) hide show
  1. package/CHANGELOG.md +57 -3
  2. package/LICENSE +59 -0
  3. package/README.md +420 -23
  4. package/config/migrations/001_add_skill_summary.sql +75 -0
  5. package/config/migrations/001_rollback_skill_summary.sql +60 -0
  6. package/config/sqlite-schema.sql +68 -4
  7. package/dist/benchmark/types.d.ts +4 -4
  8. package/dist/cli.js +0 -0
  9. package/dist/consolidation/pipeline.d.ts +6 -1
  10. package/dist/consolidation/pipeline.d.ts.map +1 -1
  11. package/dist/consolidation/pipeline.js +53 -35
  12. package/dist/consolidation/pipeline.js.map +1 -1
  13. package/dist/memory-layers/semantic-memory.d.ts +9 -19
  14. package/dist/memory-layers/semantic-memory.d.ts.map +1 -1
  15. package/dist/memory-layers/semantic-memory.js +31 -54
  16. package/dist/memory-layers/semantic-memory.js.map +1 -1
  17. package/dist/memory-layers/skill-lazy-loading.d.ts +160 -0
  18. package/dist/memory-layers/skill-lazy-loading.d.ts.map +1 -0
  19. package/dist/memory-layers/skill-lazy-loading.js +247 -0
  20. package/dist/memory-layers/skill-lazy-loading.js.map +1 -0
  21. package/dist/memory-layers/skill-library.d.ts +133 -4
  22. package/dist/memory-layers/skill-library.d.ts.map +1 -1
  23. package/dist/memory-layers/skill-library.js +427 -8
  24. package/dist/memory-layers/skill-library.js.map +1 -1
  25. package/dist/memory-layers/working-memory.d.ts +35 -14
  26. package/dist/memory-layers/working-memory.d.ts.map +1 -1
  27. package/dist/memory-layers/working-memory.js +114 -36
  28. package/dist/memory-layers/working-memory.js.map +1 -1
  29. package/dist/retrieval/hybrid-search.d.ts +2 -2
  30. package/dist/retrieval/hybrid-search.d.ts.map +1 -1
  31. package/dist/retrieval/hybrid-search.js +4 -3
  32. package/dist/retrieval/hybrid-search.js.map +1 -1
  33. package/dist/router/smart-router.d.ts +25 -0
  34. package/dist/router/smart-router.d.ts.map +1 -1
  35. package/dist/router/smart-router.js +90 -10
  36. package/dist/router/smart-router.js.map +1 -1
  37. package/dist/scheduler/consolidation-scheduler.d.ts +129 -0
  38. package/dist/scheduler/consolidation-scheduler.d.ts.map +1 -0
  39. package/dist/scheduler/consolidation-scheduler.js +179 -0
  40. package/dist/scheduler/consolidation-scheduler.js.map +1 -0
  41. package/dist/scheduler/index.d.ts +7 -0
  42. package/dist/scheduler/index.d.ts.map +1 -0
  43. package/dist/scheduler/index.js +7 -0
  44. package/dist/scheduler/index.js.map +1 -0
  45. package/dist/server-wrapper.js +0 -0
  46. package/dist/server.js +879 -28
  47. package/dist/server.js.map +1 -1
  48. package/dist/synthesis/conflict-detector.d.ts +4 -6
  49. package/dist/synthesis/conflict-detector.d.ts.map +1 -1
  50. package/dist/synthesis/conflict-detector.js +19 -21
  51. package/dist/synthesis/conflict-detector.js.map +1 -1
  52. package/dist/utils/validation.d.ts +123 -1
  53. package/dist/utils/validation.d.ts.map +1 -1
  54. package/dist/utils/validation.js +146 -7
  55. package/dist/utils/validation.js.map +1 -1
  56. package/docker-compose.yml +6 -6
  57. package/embedding-service/README.md +8 -0
  58. package/package.json +4 -2
  59. package/scripts/FRESH_INSTALL.md +1 -1
  60. package/scripts/ensure-infrastructure.sh +86 -26
  61. package/scripts/health-check.sh +2 -1
  62. package/scripts/stop-infrastructure.sh +23 -0
  63. package/scripts/test-mcp-server.sh +2 -1
package/dist/server.js CHANGED
@@ -16,11 +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
20
  import { getSqlitePath, ensureDirectories } from "./utils/paths.js";
21
21
  import { createEmbeddingClient } from "./embeddings/client.js";
22
22
  import { HybridSearchEngine } from "./retrieval/hybrid-search.js";
23
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";
24
27
  import * as SmartRouter from "./router/smart-router.js";
25
28
  // Import better-sqlite3 constructor using createRequire
26
29
  const require = createRequire(import.meta.url);
@@ -34,6 +37,10 @@ let vesperEnabled = true;
34
37
  /**
35
38
  * Tool definitions for the MCP server
36
39
  */
40
+ const NAMESPACE_PROP = {
41
+ type: "string",
42
+ description: "Namespace for multi-agent isolation (default: 'default')",
43
+ };
37
44
  const TOOLS = [
38
45
  {
39
46
  name: "store_memory",
@@ -47,7 +54,7 @@ const TOOLS = [
47
54
  },
48
55
  memory_type: {
49
56
  type: "string",
50
- enum: ["episodic", "semantic", "procedural"],
57
+ enum: ["episodic", "semantic", "procedural", "decision"],
51
58
  description: "Type of memory being stored",
52
59
  },
53
60
  metadata: {
@@ -55,6 +62,10 @@ const TOOLS = [
55
62
  description: "Additional metadata (timestamp, source, tags, etc.)",
56
63
  additionalProperties: true,
57
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" },
58
69
  },
59
70
  required: ["content", "memory_type"],
60
71
  },
@@ -72,7 +83,7 @@ const TOOLS = [
72
83
  memory_types: {
73
84
  type: "array",
74
85
  items: { type: "string" },
75
- 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.",
76
87
  },
77
88
  max_results: {
78
89
  type: "number",
@@ -83,6 +94,10 @@ const TOOLS = [
83
94
  enum: ["fast_path", "semantic", "full_text", "graph"],
84
95
  description: "Retrieval strategy to use (default: auto-select)",
85
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" },
86
101
  },
87
102
  required: ["query"],
88
103
  },
@@ -101,6 +116,7 @@ const TOOLS = [
101
116
  type: "string",
102
117
  description: "Optional filter by memory type",
103
118
  },
119
+ namespace: NAMESPACE_PROP,
104
120
  },
105
121
  },
106
122
  },
@@ -114,6 +130,7 @@ const TOOLS = [
114
130
  type: "boolean",
115
131
  description: "Include detailed per-layer statistics (default: false)",
116
132
  },
133
+ namespace: NAMESPACE_PROP,
117
134
  },
118
135
  },
119
136
  },
@@ -141,6 +158,98 @@ const TOOLS = [
141
158
  properties: {},
142
159
  },
143
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
+ },
144
253
  ];
145
254
  /**
146
255
  * Initialize connections to Redis, Qdrant, and SQLite
@@ -244,10 +353,71 @@ async function initializeConnections() {
244
353
  console.error("\x1b[32m💡 Solution:\x1b[0m Start Qdrant with: docker-compose up -d qdrant");
245
354
  connections.hybridSearch = undefined;
246
355
  }
247
- // 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
248
408
  SmartRouter.init({
249
409
  workingMemory: connections.workingMemory,
250
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
+ }
251
421
  console.error("[INFO] ✅ Smart router initialized");
252
422
  // Print service status summary
253
423
  console.error("\n" + "=".repeat(60));
@@ -299,6 +469,280 @@ function initializeSqliteSchema() {
299
469
  updated_at INTEGER
300
470
  );
301
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
+ `);
302
746
  }
303
747
  /**
304
748
  * Handle store_memory tool call
@@ -310,6 +754,7 @@ async function handleStoreMemory(input) {
310
754
  try {
311
755
  // Validate input
312
756
  const validatedInput = validateInput(StoreMemoryInputSchema, input);
757
+ const namespace = validatedInput.namespace || 'default';
313
758
  // Use pure UUID for Qdrant compatibility
314
759
  const id = randomUUID();
315
760
  const now = Date.now();
@@ -324,19 +769,22 @@ async function handleStoreMemory(input) {
324
769
  console.error("[WARN] Failed to generate embedding (continuing without):", err instanceof Error ? err.message : String(err));
325
770
  }
326
771
  }
327
- // Store in SQLite
772
+ // Store in SQLite with namespace and agent attribution
328
773
  const stmt = connections.sqlite.prepare(`
329
- INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata)
330
- VALUES (?, ?, ?, ?, ?, ?)
774
+ INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace, agent_id, agent_role, task_id)
775
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
331
776
  `);
332
- stmt.run(id, validatedInput.content, validatedInput.memory_type, now, now, JSON.stringify(validatedInput.metadata || {}));
333
- // 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)
334
779
  if (embedding && connections.hybridSearch) {
335
780
  try {
336
781
  await connections.hybridSearch.upsertMemory(id, embedding, {
337
782
  content: validatedInput.content,
338
783
  memory_type: validatedInput.memory_type,
339
784
  created_at: now,
785
+ namespace,
786
+ agent_id: validatedInput.agent_id,
787
+ task_id: validatedInput.task_id,
340
788
  ...validatedInput.metadata,
341
789
  });
342
790
  console.error(`[INFO] Stored embedding in Qdrant for ${id}`);
@@ -346,10 +794,11 @@ async function handleStoreMemory(input) {
346
794
  throw new McpError(ErrorCode.InternalError, `Qdrant storage failed: ${err instanceof Error ? err.message : String(err)}`);
347
795
  }
348
796
  }
349
- 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"})`);
350
798
  return {
351
799
  success: true,
352
800
  memory_id: id,
801
+ namespace,
353
802
  timestamp: now,
354
803
  message: `Memory stored successfully (${validatedInput.memory_type})`,
355
804
  has_embedding: !!embedding,
@@ -369,18 +818,36 @@ async function handleRetrieveMemory(input) {
369
818
  try {
370
819
  // Validate input
371
820
  const validatedInput = validateInput(RetrieveMemoryInputSchema, input);
821
+ const namespace = validatedInput.namespace || 'default';
372
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
+ };
373
834
  // Try semantic search if embedding service is available
374
835
  if (connections.embeddingClient && connections.hybridSearch) {
375
836
  try {
376
837
  // Generate query embedding
377
838
  const queryEmbedding = await connections.embeddingClient.embed(validatedInput.query);
378
- // Perform hybrid search
379
- const searchResults = await connections.hybridSearch.hybridSearch(queryEmbedding, maxResults);
380
- 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}`);
381
847
  return {
382
848
  success: true,
383
849
  query: validatedInput.query,
850
+ namespace,
384
851
  routing_strategy: "semantic",
385
852
  results: searchResults.map((r) => ({
386
853
  id: r.id,
@@ -398,29 +865,45 @@ async function handleRetrieveMemory(input) {
398
865
  console.error("[WARN] Semantic search failed, falling back to text search:", err instanceof Error ? err.message : String(err));
399
866
  }
400
867
  }
401
- // Fallback: SQL-based text search
402
- let query = "SELECT id, content, memory_type, created_at, metadata FROM memories";
403
- 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];
404
871
  // Add type filter if specified
405
872
  if (validatedInput.memory_types && validatedInput.memory_types.length > 0) {
406
873
  const placeholders = validatedInput.memory_types.map(() => "?").join(",");
407
- query += ` WHERE memory_type IN (${placeholders})`;
874
+ query += ` AND memory_type IN (${placeholders})`;
408
875
  params.push(...validatedInput.memory_types);
409
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
+ }
410
890
  query += " ORDER BY created_at DESC LIMIT ?";
411
891
  params.push(maxResults);
412
892
  const stmt = connections.sqlite.prepare(query);
413
893
  const results = stmt.all(...params);
414
- 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}`);
415
895
  return {
416
896
  success: true,
417
897
  query: validatedInput.query,
898
+ namespace,
418
899
  routing_strategy: "text_fallback",
419
900
  results: results.map((r) => ({
420
901
  id: r.id,
421
902
  content: r.content,
422
903
  memory_type: r.memory_type,
423
904
  created_at: r.created_at,
905
+ agent_id: r.agent_id,
906
+ task_id: r.task_id,
424
907
  metadata: JSON.parse(r.metadata || "{}"),
425
908
  })),
426
909
  count: results.length,
@@ -440,20 +923,22 @@ async function handleListRecent(input) {
440
923
  try {
441
924
  // Validate input
442
925
  const validatedInput = validateInput(ListRecentInputSchema, input);
926
+ const namespace = validatedInput.namespace || 'default';
443
927
  const limit = validatedInput.limit || 5;
444
- let query = "SELECT id, content, memory_type, created_at FROM memories";
445
- const params = [];
928
+ let query = "SELECT id, content, memory_type, created_at, agent_id, task_id FROM memories WHERE namespace = ?";
929
+ const params = [namespace];
446
930
  if (validatedInput.memory_type) {
447
- query += " WHERE memory_type = ?";
931
+ query += " AND memory_type = ?";
448
932
  params.push(validatedInput.memory_type);
449
933
  }
450
934
  query += " ORDER BY created_at DESC LIMIT ?";
451
935
  params.push(limit);
452
936
  const stmt = connections.sqlite.prepare(query);
453
937
  const results = stmt.all(...params);
454
- 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"})`);
455
939
  return {
456
940
  success: true,
941
+ namespace,
457
942
  limit,
458
943
  memory_type_filter: validatedInput.memory_type || null,
459
944
  entries: results.map((r) => ({
@@ -461,6 +946,8 @@ async function handleListRecent(input) {
461
946
  content: r.content,
462
947
  memory_type: r.memory_type,
463
948
  created_at: r.created_at,
949
+ agent_id: r.agent_id,
950
+ task_id: r.task_id,
464
951
  })),
465
952
  count: results.length,
466
953
  };
@@ -479,15 +966,17 @@ async function handleGetStats(input) {
479
966
  try {
480
967
  // Validate input
481
968
  const validatedInput = validateInput(GetStatsInputSchema, input);
482
- const countStmt = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories");
483
- 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);
484
972
  const totalMemories = countResult.count;
485
973
  const typeStmt = connections.sqlite.prepare(`
486
- 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
487
975
  `);
488
- const typeResults = typeStmt.all();
976
+ const typeResults = typeStmt.all(namespace);
489
977
  const stats = {
490
978
  success: true,
979
+ namespace,
491
980
  timestamp: Date.now(),
492
981
  total_memories: totalMemories,
493
982
  redis_connected: connections.redis ? true : false,
@@ -556,6 +1045,342 @@ async function handleVesperStatus() {
556
1045
  : "Vesper is in pass-through mode (memory operations are skipped)",
557
1046
  };
558
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
+ }
559
1384
  /**
560
1385
  * Process tool calls
561
1386
  */
@@ -589,6 +1414,18 @@ async function processTool(name, input) {
589
1414
  return await handleListRecent(input);
590
1415
  case "get_stats":
591
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);
592
1429
  default:
593
1430
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
594
1431
  }
@@ -603,7 +1440,7 @@ async function main() {
603
1440
  // Create MCP server
604
1441
  const server = new Server({
605
1442
  name: "vesper",
606
- version: "0.1.0",
1443
+ version: "0.5.0",
607
1444
  }, {
608
1445
  capabilities: {
609
1446
  tools: {},
@@ -657,8 +1494,22 @@ async function main() {
657
1494
  console.error("[INFO] Starting stdio transport...");
658
1495
  await server.connect(transport);
659
1496
  console.error("[INFO] Vesper running on stdio transport");
660
- 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);
661
1510
  }
1511
+ process.on('SIGINT', cleanup);
1512
+ process.on('SIGTERM', cleanup);
662
1513
  // Run the server
663
1514
  main().catch((err) => {
664
1515
  console.error("[FATAL] Server error:", err instanceof Error ? err.message : String(err));