vesper-memory 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +6 -6
- package/config/sqlite-schema.sql +31 -0
- package/dist/cli.js +6 -49
- package/dist/cli.js.map +1 -1
- package/dist/consolidation/pipeline.d.ts +25 -6
- package/dist/consolidation/pipeline.d.ts.map +1 -1
- package/dist/consolidation/pipeline.js +310 -67
- package/dist/consolidation/pipeline.js.map +1 -1
- package/dist/memory-layers/semantic-memory.d.ts +8 -3
- package/dist/memory-layers/semantic-memory.d.ts.map +1 -1
- package/dist/memory-layers/semantic-memory.js +110 -50
- package/dist/memory-layers/semantic-memory.js.map +1 -1
- package/dist/memory-layers/skill-library.d.ts +9 -9
- package/dist/memory-layers/skill-library.d.ts.map +1 -1
- package/dist/memory-layers/skill-library.js +85 -56
- package/dist/memory-layers/skill-library.js.map +1 -1
- package/dist/memory-layers/working-memory.d.ts +2 -2
- package/dist/memory-layers/working-memory.d.ts.map +1 -1
- package/dist/memory-layers/working-memory.js +57 -47
- package/dist/memory-layers/working-memory.js.map +1 -1
- package/dist/retrieval/hybrid-search.d.ts.map +1 -1
- package/dist/retrieval/hybrid-search.js +10 -5
- package/dist/retrieval/hybrid-search.js.map +1 -1
- package/dist/retrieval/index-outbox.d.ts +50 -0
- package/dist/retrieval/index-outbox.d.ts.map +1 -0
- package/dist/retrieval/index-outbox.js +225 -0
- package/dist/retrieval/index-outbox.js.map +1 -0
- package/dist/retrieval/memory-retriever.d.ts +123 -0
- package/dist/retrieval/memory-retriever.d.ts.map +1 -0
- package/dist/retrieval/memory-retriever.js +723 -0
- package/dist/retrieval/memory-retriever.js.map +1 -0
- package/dist/router/smart-router.js +18 -18
- package/dist/server-wrapper.js +0 -0
- package/dist/server.d.ts +9 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +859 -355
- package/dist/server.js.map +1 -1
- package/dist/utils/claude-mcp-config.d.ts +3 -0
- package/dist/utils/claude-mcp-config.d.ts.map +1 -0
- package/dist/utils/claude-mcp-config.js +42 -0
- package/dist/utils/claude-mcp-config.js.map +1 -0
- package/dist/utils/monitoring.d.ts +1 -0
- package/dist/utils/monitoring.d.ts.map +1 -1
- package/dist/utils/monitoring.js +1 -0
- package/dist/utils/monitoring.js.map +1 -1
- package/dist/utils/paths.d.ts +13 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +24 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/validation.d.ts +14 -2
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +64 -48
- package/dist/utils/validation.js.map +1 -1
- package/docker-compose.yml +3 -3
- package/package.json +7 -4
- package/scripts/README.md +1 -1
- package/scripts/health-check.sh +8 -4
- package/scripts/init-qdrant.ts +42 -60
- package/scripts/test-mcp-server.sh +2 -2
- package/dist/config/rate-limits.d.ts +0 -76
- package/dist/config/rate-limits.d.ts.map +0 -1
- package/dist/config/rate-limits.js +0 -161
- package/dist/config/rate-limits.js.map +0 -1
- package/dist/middleware/auth.d.ts +0 -287
- package/dist/middleware/auth.d.ts.map +0 -1
- package/dist/middleware/auth.js +0 -610
- package/dist/middleware/auth.js.map +0 -1
- package/dist/monitoring/health.d.ts +0 -241
- package/dist/monitoring/health.d.ts.map +0 -1
- package/dist/monitoring/health.js +0 -649
- package/dist/monitoring/health.js.map +0 -1
- package/dist/monitoring/metrics.d.ts +0 -153
- package/dist/monitoring/metrics.d.ts.map +0 -1
- package/dist/monitoring/metrics.js +0 -349
- package/dist/monitoring/metrics.js.map +0 -1
- package/dist/security/rate-limit-middleware.d.ts +0 -119
- package/dist/security/rate-limit-middleware.d.ts.map +0 -1
- package/dist/security/rate-limit-middleware.js +0 -193
- package/dist/security/rate-limit-middleware.js.map +0 -1
package/dist/server.js
CHANGED
|
@@ -20,10 +20,14 @@ import { validateInput, StoreMemoryInputSchema, RetrieveMemoryInputSchema, ListR
|
|
|
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
|
+
import { MemoryRetriever } from "./retrieval/memory-retriever.js";
|
|
24
|
+
import { IndexOutboxProcessor, initializeIndexOutboxSchema } from "./retrieval/index-outbox.js";
|
|
23
25
|
import { WorkingMemoryLayer } from "./memory-layers/working-memory.js";
|
|
24
26
|
import { SemanticMemoryLayer } from "./memory-layers/semantic-memory.js";
|
|
25
27
|
import { SkillLibrary } from "./memory-layers/skill-library.js";
|
|
26
28
|
import { ConsolidationPipeline } from "./consolidation/pipeline.js";
|
|
29
|
+
import { ConsolidationScheduler } from "./scheduler/consolidation-scheduler.js";
|
|
30
|
+
import { PerformanceMonitor } from "./utils/monitoring.js";
|
|
27
31
|
import * as SmartRouter from "./router/smart-router.js";
|
|
28
32
|
// Import better-sqlite3 constructor using createRequire
|
|
29
33
|
const require = createRequire(import.meta.url);
|
|
@@ -34,6 +38,7 @@ const connections = {};
|
|
|
34
38
|
* Used for A/B benchmarking (enabled vs disabled comparison)
|
|
35
39
|
*/
|
|
36
40
|
let vesperEnabled = true;
|
|
41
|
+
const retrievalMonitor = new PerformanceMonitor();
|
|
37
42
|
/**
|
|
38
43
|
* Tool definitions for the MCP server
|
|
39
44
|
*/
|
|
@@ -64,8 +69,29 @@ const TOOLS = [
|
|
|
64
69
|
},
|
|
65
70
|
namespace: NAMESPACE_PROP,
|
|
66
71
|
agent_id: { type: "string", description: "ID of the agent storing this memory" },
|
|
67
|
-
agent_role: {
|
|
72
|
+
agent_role: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Role of the agent (e.g., 'orchestrator', 'code-reviewer')",
|
|
75
|
+
},
|
|
68
76
|
task_id: { type: "string", description: "Task ID this memory is associated with" },
|
|
77
|
+
importance: {
|
|
78
|
+
type: "number",
|
|
79
|
+
minimum: 0,
|
|
80
|
+
maximum: 1,
|
|
81
|
+
description: "Write-time salience from 0 to 1 (default: 0.5)",
|
|
82
|
+
},
|
|
83
|
+
valid_from: {
|
|
84
|
+
oneOf: [{ type: "number" }, { type: "string", format: "date-time" }],
|
|
85
|
+
description: "When this memory became valid (Unix milliseconds or ISO 8601)",
|
|
86
|
+
},
|
|
87
|
+
valid_until: {
|
|
88
|
+
oneOf: [{ type: "number" }, { type: "string", format: "date-time" }],
|
|
89
|
+
description: "When this memory stopped being valid (Unix milliseconds or ISO 8601)",
|
|
90
|
+
},
|
|
91
|
+
expired_at: {
|
|
92
|
+
oneOf: [{ type: "number" }, { type: "string", format: "date-time" }],
|
|
93
|
+
description: "Optional retention expiry; expired memories are excluded from retrieval",
|
|
94
|
+
},
|
|
69
95
|
},
|
|
70
96
|
required: ["content", "memory_type"],
|
|
71
97
|
},
|
|
@@ -91,13 +117,29 @@ const TOOLS = [
|
|
|
91
117
|
},
|
|
92
118
|
routing_strategy: {
|
|
93
119
|
type: "string",
|
|
94
|
-
enum: ["fast_path", "semantic", "full_text", "graph"],
|
|
120
|
+
enum: ["fast_path", "semantic", "full_text", "hybrid", "graph"],
|
|
95
121
|
description: "Retrieval strategy to use (default: auto-select)",
|
|
96
122
|
},
|
|
97
123
|
namespace: NAMESPACE_PROP,
|
|
98
124
|
agent_id: { type: "string", description: "Filter by agent ID" },
|
|
99
125
|
task_id: { type: "string", description: "Filter by task ID" },
|
|
100
126
|
exclude_agent: { type: "string", description: "Exclude memories from this agent" },
|
|
127
|
+
include_superseded: {
|
|
128
|
+
type: "boolean",
|
|
129
|
+
description: "Include decisions replaced by newer decisions (default: false)",
|
|
130
|
+
},
|
|
131
|
+
include_archived: {
|
|
132
|
+
type: "boolean",
|
|
133
|
+
description: "Include archived memories (default: false)",
|
|
134
|
+
},
|
|
135
|
+
as_of: {
|
|
136
|
+
oneOf: [{ type: "number" }, { type: "string", format: "date-time" }],
|
|
137
|
+
description: "Return memories valid at this time (Unix milliseconds or ISO 8601)",
|
|
138
|
+
},
|
|
139
|
+
explain: {
|
|
140
|
+
type: "boolean",
|
|
141
|
+
description: "Include content-free retrieval stage status and candidate counts",
|
|
142
|
+
},
|
|
101
143
|
},
|
|
102
144
|
required: ["query"],
|
|
103
145
|
},
|
|
@@ -223,15 +265,21 @@ const TOOLS = [
|
|
|
223
265
|
task_id: { type: "string", description: "Filter source memories by task ID" },
|
|
224
266
|
query: { type: "string", description: "Semantic query to select relevant memories" },
|
|
225
267
|
max_items: { type: "number", description: "Max items to share (default: 10)" },
|
|
226
|
-
include_skills: {
|
|
227
|
-
|
|
268
|
+
include_skills: {
|
|
269
|
+
type: "boolean",
|
|
270
|
+
description: "Include matching skills (default: false)",
|
|
271
|
+
},
|
|
272
|
+
include_entities: {
|
|
273
|
+
type: "boolean",
|
|
274
|
+
description: "Include related entities (default: true)",
|
|
275
|
+
},
|
|
228
276
|
},
|
|
229
277
|
required: ["source_namespace", "target_namespace"],
|
|
230
278
|
},
|
|
231
279
|
},
|
|
232
280
|
{
|
|
233
281
|
name: "store_decision",
|
|
234
|
-
description: "Store
|
|
282
|
+
description: "Store a versioned architectural or design decision with reduced decay; optionally close a superseded decision without deleting its history.",
|
|
235
283
|
inputSchema: {
|
|
236
284
|
type: "object",
|
|
237
285
|
properties: {
|
|
@@ -241,7 +289,19 @@ const TOOLS = [
|
|
|
241
289
|
agent_role: { type: "string", description: "Role of the agent" },
|
|
242
290
|
task_id: { type: "string", description: "Related task ID" },
|
|
243
291
|
supersedes: { type: "string", description: "ID of a previous decision this replaces" },
|
|
244
|
-
|
|
292
|
+
valid_from: {
|
|
293
|
+
oneOf: [{ type: "number" }, { type: "string", format: "date-time" }],
|
|
294
|
+
description: "When this decision became valid (Unix milliseconds or ISO 8601)",
|
|
295
|
+
},
|
|
296
|
+
valid_until: {
|
|
297
|
+
oneOf: [{ type: "number" }, { type: "string", format: "date-time" }],
|
|
298
|
+
description: "When this decision stopped being valid (Unix milliseconds or ISO 8601)",
|
|
299
|
+
},
|
|
300
|
+
metadata: {
|
|
301
|
+
type: "object",
|
|
302
|
+
description: "Additional metadata",
|
|
303
|
+
additionalProperties: true,
|
|
304
|
+
},
|
|
245
305
|
},
|
|
246
306
|
required: ["content"],
|
|
247
307
|
},
|
|
@@ -269,34 +329,49 @@ const TOOLS = [
|
|
|
269
329
|
/**
|
|
270
330
|
* Initialize connections to Redis, Qdrant, and SQLite
|
|
271
331
|
*/
|
|
272
|
-
async function initializeConnections() {
|
|
332
|
+
export async function initializeConnections() {
|
|
273
333
|
try {
|
|
274
334
|
console.error("[INFO] Initializing connections to external services...");
|
|
275
335
|
// Initialize Redis (optional - used for caching)
|
|
336
|
+
let redisClient;
|
|
276
337
|
try {
|
|
277
|
-
|
|
338
|
+
if (process.env.VESPER_DISABLE_OPTIONAL_SERVICES === "1") {
|
|
339
|
+
throw new Error("disabled by VESPER_DISABLE_OPTIONAL_SERVICES");
|
|
340
|
+
}
|
|
341
|
+
redisClient = new Redis({
|
|
278
342
|
host: process.env.REDIS_HOST || "localhost",
|
|
279
343
|
port: parseInt(process.env.REDIS_PORT || "6379"),
|
|
280
344
|
password: process.env.REDIS_PASSWORD,
|
|
281
|
-
retryStrategy: (times) => Math.min(times * 50, 2000),
|
|
345
|
+
retryStrategy: (times) => (times <= 3 ? Math.min(times * 50, 2000) : null),
|
|
282
346
|
maxRetriesPerRequest: 3,
|
|
283
347
|
connectTimeout: 5000,
|
|
284
348
|
});
|
|
285
|
-
|
|
349
|
+
redisClient.on("connect", () => {
|
|
286
350
|
console.error("[INFO] ✅ Redis connected");
|
|
287
351
|
});
|
|
288
|
-
|
|
352
|
+
redisClient.on("error", (err) => {
|
|
289
353
|
console.error("[WARN] Redis connection error:", err.message);
|
|
290
354
|
});
|
|
291
355
|
// Test connection with timeout
|
|
292
|
-
|
|
293
|
-
|
|
356
|
+
let pingTimer;
|
|
357
|
+
const pingTimeout = new Promise((_, reject) => {
|
|
358
|
+
pingTimer = setTimeout(() => reject(new Error("Connection timeout")), 5000);
|
|
359
|
+
});
|
|
360
|
+
try {
|
|
361
|
+
await Promise.race([redisClient.ping(), pingTimeout]);
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (pingTimer)
|
|
365
|
+
clearTimeout(pingTimer);
|
|
366
|
+
}
|
|
367
|
+
connections.redis = redisClient;
|
|
294
368
|
console.error("[INFO] ✅ Redis ping successful");
|
|
295
369
|
// Initialize Working Memory Layer
|
|
296
|
-
connections.workingMemory = new WorkingMemoryLayer(
|
|
370
|
+
connections.workingMemory = new WorkingMemoryLayer(redisClient, 5);
|
|
297
371
|
console.error("[INFO] ✅ Working memory layer initialized");
|
|
298
372
|
}
|
|
299
373
|
catch (err) {
|
|
374
|
+
redisClient?.disconnect();
|
|
300
375
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
301
376
|
console.error("\x1b[33m⚠️ Redis not available:\x1b[0m", errorMsg);
|
|
302
377
|
console.error("\x1b[34mℹ️ Impact:\x1b[0m Working memory disabled");
|
|
@@ -312,6 +387,8 @@ async function initializeConnections() {
|
|
|
312
387
|
const dbPath = process.env.SQLITE_DB || getSqlitePath();
|
|
313
388
|
connections.sqlite = new DatabaseConstructor(dbPath);
|
|
314
389
|
connections.sqlite.pragma("journal_mode = WAL");
|
|
390
|
+
connections.sqlite.pragma("foreign_keys = ON");
|
|
391
|
+
connections.sqlite.pragma("busy_timeout = 5000");
|
|
315
392
|
// Initialize schema
|
|
316
393
|
initializeSqliteSchema();
|
|
317
394
|
// Clear logging about storage location
|
|
@@ -328,6 +405,9 @@ async function initializeConnections() {
|
|
|
328
405
|
}
|
|
329
406
|
// Initialize Embedding Service (optional - graceful degradation)
|
|
330
407
|
try {
|
|
408
|
+
if (process.env.VESPER_DISABLE_OPTIONAL_SERVICES === "1") {
|
|
409
|
+
throw new Error("disabled by VESPER_DISABLE_OPTIONAL_SERVICES");
|
|
410
|
+
}
|
|
331
411
|
connections.embeddingClient = createEmbeddingClient({
|
|
332
412
|
serviceUrl: process.env.EMBEDDING_SERVICE_URL || "http://localhost:8000",
|
|
333
413
|
timeout: 10000,
|
|
@@ -335,10 +415,7 @@ async function initializeConnections() {
|
|
|
335
415
|
});
|
|
336
416
|
// Test connection with timeout
|
|
337
417
|
const healthTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 10000));
|
|
338
|
-
const health = await Promise.race([
|
|
339
|
-
connections.embeddingClient.health(),
|
|
340
|
-
healthTimeout,
|
|
341
|
-
]);
|
|
418
|
+
const health = await Promise.race([connections.embeddingClient.health(), healthTimeout]);
|
|
342
419
|
console.error(`[INFO] ✅ Embedding service connected: ${health.model} (${health.dimensions}-dim)`);
|
|
343
420
|
}
|
|
344
421
|
catch (err) {
|
|
@@ -350,15 +427,15 @@ async function initializeConnections() {
|
|
|
350
427
|
}
|
|
351
428
|
// Initialize Hybrid Search Engine (optional - requires Qdrant)
|
|
352
429
|
try {
|
|
430
|
+
if (process.env.VESPER_DISABLE_OPTIONAL_SERVICES === "1") {
|
|
431
|
+
throw new Error("disabled by VESPER_DISABLE_OPTIONAL_SERVICES");
|
|
432
|
+
}
|
|
353
433
|
const qdrantUrl = process.env.QDRANT_URL || "http://localhost:6333";
|
|
354
434
|
const qdrantApiKey = process.env.QDRANT_API_KEY;
|
|
355
435
|
connections.hybridSearch = new HybridSearchEngine(qdrantUrl, "memory-vectors", 1024, qdrantApiKey);
|
|
356
436
|
// Initialize collection with timeout
|
|
357
437
|
const initTimeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Initialization timeout")), 10000));
|
|
358
|
-
await Promise.race([
|
|
359
|
-
connections.hybridSearch.initializeCollection(),
|
|
360
|
-
initTimeout,
|
|
361
|
-
]);
|
|
438
|
+
await Promise.race([connections.hybridSearch.initializeCollection(), initTimeout]);
|
|
362
439
|
console.error("[INFO] ✅ Qdrant hybrid search initialized");
|
|
363
440
|
}
|
|
364
441
|
catch (err) {
|
|
@@ -392,32 +469,72 @@ async function initializeConnections() {
|
|
|
392
469
|
console.error("[WARN] Skill library initialization failed:", err instanceof Error ? err.message : String(err));
|
|
393
470
|
}
|
|
394
471
|
}
|
|
395
|
-
//
|
|
396
|
-
|
|
472
|
+
// SQLite is canonical. FTS retrieval is always available; Redis and Qdrant
|
|
473
|
+
// are optional projections repaired through the durable index outbox.
|
|
474
|
+
if (connections.sqlite) {
|
|
475
|
+
connections.memoryRetriever = new MemoryRetriever({
|
|
476
|
+
db: connections.sqlite,
|
|
477
|
+
embeddingClient: connections.embeddingClient,
|
|
478
|
+
hybridSearch: connections.hybridSearch,
|
|
479
|
+
});
|
|
480
|
+
connections.memoryRetriever.initialize();
|
|
481
|
+
connections.indexOutbox = new IndexOutboxProcessor(connections.sqlite, {
|
|
482
|
+
embeddingClient: connections.embeddingClient,
|
|
483
|
+
hybridSearch: connections.hybridSearch,
|
|
484
|
+
workingMemory: connections.workingMemory,
|
|
485
|
+
projectionConfig: {
|
|
486
|
+
qdrant: process.env.VESPER_DISABLE_OPTIONAL_SERVICES !== "1",
|
|
487
|
+
redis: process.env.VESPER_DISABLE_OPTIONAL_SERVICES !== "1",
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
setImmediate(async () => {
|
|
491
|
+
try {
|
|
492
|
+
const stats = await connections.indexOutbox.drain();
|
|
493
|
+
console.error(`[INFO] Index outbox: ${stats.completed} completed, ${stats.pending} pending`);
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
console.error("[WARN] Index outbox drain failed:", error instanceof Error ? error.message : String(error));
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
connections.indexOutboxTimer = setInterval(async () => {
|
|
500
|
+
try {
|
|
501
|
+
await connections.indexOutbox?.drain();
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
console.error("[WARN] Periodic index repair failed:", error instanceof Error ? error.message : String(error));
|
|
505
|
+
}
|
|
506
|
+
}, 30_000);
|
|
507
|
+
connections.indexOutboxTimer.unref();
|
|
508
|
+
}
|
|
509
|
+
// Consolidation reads canonical SQLite rows. Redis is an optional cache,
|
|
510
|
+
// not an ingestion dependency.
|
|
511
|
+
if (connections.semanticMemory && connections.skillLibrary && connections.sqlite) {
|
|
397
512
|
try {
|
|
398
513
|
connections.consolidationPipeline = new ConsolidationPipeline(connections.workingMemory, connections.semanticMemory, connections.skillLibrary, connections.sqlite);
|
|
399
514
|
console.error("[INFO] ✅ Consolidation pipeline initialized");
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
515
|
+
connections.consolidationScheduler = new ConsolidationScheduler(connections.consolidationPipeline);
|
|
516
|
+
connections.consolidationScheduler.start();
|
|
517
|
+
// Process the bounded, durable backlog without delaying MCP startup.
|
|
518
|
+
if (process.env.VESPER_DISABLE_STARTUP_CONSOLIDATION !== "1")
|
|
519
|
+
setImmediate(async () => {
|
|
520
|
+
try {
|
|
521
|
+
console.error("[INFO] 🔄 Running startup consolidation...");
|
|
522
|
+
const stats = await connections.consolidationScheduler.runNow();
|
|
523
|
+
console.error(`[INFO] ✅ Startup consolidation complete: ${stats.memoriesProcessed} memories, ` +
|
|
524
|
+
`${stats.entitiesExtracted} entities, ${stats.conflictsDetected} conflicts, ` +
|
|
525
|
+
`${stats.skillsExtracted} skills in ${stats.duration}ms`);
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
console.error("[WARN] Startup consolidation failed:", err instanceof Error ? err.message : String(err));
|
|
529
|
+
}
|
|
530
|
+
});
|
|
414
531
|
}
|
|
415
532
|
catch (err) {
|
|
416
533
|
console.error("[WARN] Consolidation pipeline initialization failed:", err instanceof Error ? err.message : String(err));
|
|
417
534
|
}
|
|
418
535
|
}
|
|
419
536
|
else {
|
|
420
|
-
console.error("[WARN] Consolidation pipeline not initialized (missing
|
|
537
|
+
console.error("[WARN] Consolidation pipeline not initialized (missing SQLite-backed layers)");
|
|
421
538
|
}
|
|
422
539
|
// Initialize Smart Router with all available dependencies
|
|
423
540
|
SmartRouter.init({
|
|
@@ -442,10 +559,10 @@ async function initializeConnections() {
|
|
|
442
559
|
console.error("\n" + "=".repeat(60));
|
|
443
560
|
console.error("📊 Vesper Service Status:");
|
|
444
561
|
console.error("=".repeat(60));
|
|
445
|
-
console.error(`SQLite: ${connections.sqlite ?
|
|
446
|
-
console.error(`Redis: ${connections.redis ?
|
|
447
|
-
console.error(`Embedding: ${connections.embeddingClient ?
|
|
448
|
-
console.error(`Qdrant: ${connections.hybridSearch ?
|
|
562
|
+
console.error(`SQLite: ${connections.sqlite ? "\x1b[32m✓ Ready\x1b[0m" : "\x1b[31m✗ Failed\x1b[0m"} (required)`);
|
|
563
|
+
console.error(`Redis: ${connections.redis ? "\x1b[32m✓ Ready\x1b[0m" : "\x1b[33m⚠ Disabled\x1b[0m"} (working memory)`);
|
|
564
|
+
console.error(`Embedding: ${connections.embeddingClient ? "\x1b[32m✓ Ready\x1b[0m" : "\x1b[33m⚠ Disabled\x1b[0m"} (semantic search)`);
|
|
565
|
+
console.error(`Qdrant: ${connections.hybridSearch ? "\x1b[32m✓ Ready\x1b[0m" : "\x1b[33m⚠ Disabled\x1b[0m"} (vector storage)`);
|
|
449
566
|
console.error("=".repeat(60));
|
|
450
567
|
// Show degraded mode warning if any optional services are down
|
|
451
568
|
const optionalServicesDown = !connections.redis || !connections.embeddingClient || !connections.hybridSearch;
|
|
@@ -488,23 +605,66 @@ function initializeSqliteSchema() {
|
|
|
488
605
|
updated_at INTEGER
|
|
489
606
|
);
|
|
490
607
|
`);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
608
|
+
ensureColumn("memories", "consolidated_at", "INTEGER");
|
|
609
|
+
ensureColumn("memories", "namespace", "TEXT DEFAULT 'default'");
|
|
610
|
+
ensureColumn("memories", "agent_id", "TEXT");
|
|
611
|
+
ensureColumn("memories", "agent_role", "TEXT");
|
|
612
|
+
ensureColumn("memories", "task_id", "TEXT");
|
|
613
|
+
ensureColumn("memories", "importance", "REAL DEFAULT 0.5");
|
|
614
|
+
ensureColumn("memories", "access_count", "INTEGER DEFAULT 0");
|
|
615
|
+
ensureColumn("memories", "last_accessed", "INTEGER");
|
|
616
|
+
const addedValidFrom = ensureColumn("memories", "valid_from", "INTEGER");
|
|
617
|
+
ensureColumn("memories", "valid_until", "INTEGER");
|
|
618
|
+
ensureColumn("memories", "expired_at", "INTEGER");
|
|
619
|
+
ensureColumn("memories", "archived_at", "INTEGER");
|
|
620
|
+
// Leave legacy rows unconsolidated. The old write path did not reliably
|
|
621
|
+
// populate the Redis source consumed by the old pipeline, so bounded runs of
|
|
622
|
+
// the canonical SQLite pipeline must be allowed to backfill them.
|
|
623
|
+
if (addedValidFrom) {
|
|
624
|
+
connections.sqlite.exec("UPDATE memories SET valid_from = created_at WHERE valid_from IS NULL");
|
|
503
625
|
}
|
|
504
626
|
connections.sqlite.exec(`
|
|
505
627
|
CREATE INDEX IF NOT EXISTS idx_memories_namespace ON memories(namespace);
|
|
506
628
|
CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(namespace, agent_id);
|
|
629
|
+
CREATE INDEX IF NOT EXISTS idx_memories_task ON memories(namespace, task_id);
|
|
630
|
+
CREATE INDEX IF NOT EXISTS idx_memories_validity
|
|
631
|
+
ON memories(namespace, valid_from, valid_until, expired_at, archived_at);
|
|
507
632
|
`);
|
|
633
|
+
initializeIndexOutboxSchema(connections.sqlite);
|
|
634
|
+
connections.sqlite.pragma("user_version = 1");
|
|
635
|
+
}
|
|
636
|
+
function ensureColumn(table, column, definition) {
|
|
637
|
+
if (!connections.sqlite)
|
|
638
|
+
throw new Error("SQLite not initialized");
|
|
639
|
+
const columns = connections.sqlite.pragma(`table_info(${table})`);
|
|
640
|
+
if (columns.some((existing) => existing.name === column))
|
|
641
|
+
return false;
|
|
642
|
+
connections.sqlite.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
function toEpochMillis(value) {
|
|
646
|
+
if (value === undefined)
|
|
647
|
+
return undefined;
|
|
648
|
+
if (typeof value === "number")
|
|
649
|
+
return value;
|
|
650
|
+
const parsed = Date.parse(value);
|
|
651
|
+
if (!Number.isFinite(parsed)) {
|
|
652
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid timestamp: ${value}`);
|
|
653
|
+
}
|
|
654
|
+
return parsed;
|
|
655
|
+
}
|
|
656
|
+
function sqliteEpochMillisExpression(expression) {
|
|
657
|
+
return `(CASE
|
|
658
|
+
WHEN typeof(${expression}) IN ('integer', 'real')
|
|
659
|
+
THEN CASE WHEN ABS(${expression}) < 100000000000 THEN ${expression} * 1000 ELSE ${expression} END
|
|
660
|
+
WHEN trim(${expression}) != '' AND trim(${expression}) NOT GLOB '*[^0-9.]*'
|
|
661
|
+
THEN CASE
|
|
662
|
+
WHEN ABS(CAST(${expression} AS REAL)) < 100000000000
|
|
663
|
+
THEN CAST(${expression} AS REAL) * 1000
|
|
664
|
+
ELSE CAST(${expression} AS REAL)
|
|
665
|
+
END
|
|
666
|
+
ELSE CAST(strftime('%s', ${expression}) AS INTEGER) * 1000
|
|
667
|
+
END)`;
|
|
508
668
|
}
|
|
509
669
|
/**
|
|
510
670
|
* Initialize SQLite schema for semantic memory (HippoRAG knowledge graph)
|
|
@@ -560,6 +720,8 @@ function initializeSemanticMemorySchema() {
|
|
|
560
720
|
fact_id_1 TEXT,
|
|
561
721
|
fact_id_2 TEXT,
|
|
562
722
|
conflict_type TEXT,
|
|
723
|
+
entity_id TEXT,
|
|
724
|
+
property TEXT,
|
|
563
725
|
description TEXT,
|
|
564
726
|
severity TEXT,
|
|
565
727
|
resolution_status TEXT
|
|
@@ -567,10 +729,18 @@ function initializeSemanticMemorySchema() {
|
|
|
567
729
|
|
|
568
730
|
CREATE TABLE IF NOT EXISTS backup_metadata (
|
|
569
731
|
id TEXT PRIMARY KEY,
|
|
732
|
+
backup_timestamp TEXT,
|
|
570
733
|
backup_type TEXT NOT NULL,
|
|
734
|
+
status TEXT DEFAULT 'completed',
|
|
571
735
|
backup_path TEXT NOT NULL,
|
|
736
|
+
file_size_bytes INTEGER,
|
|
737
|
+
entities_count INTEGER,
|
|
738
|
+
relationships_count INTEGER,
|
|
739
|
+
facts_count INTEGER,
|
|
572
740
|
created_at TEXT NOT NULL,
|
|
573
|
-
expires_at TEXT NOT NULL
|
|
741
|
+
expires_at TEXT NOT NULL,
|
|
742
|
+
notes TEXT,
|
|
743
|
+
metadata TEXT
|
|
574
744
|
);
|
|
575
745
|
`);
|
|
576
746
|
// v0.5.0 namespace migration for semantic memory tables
|
|
@@ -584,8 +754,26 @@ function initializeSemanticMemorySchema() {
|
|
|
584
754
|
try {
|
|
585
755
|
connections.sqlite.exec(sql);
|
|
586
756
|
}
|
|
587
|
-
catch (_) {
|
|
757
|
+
catch (_) {
|
|
758
|
+
/* column already exists */
|
|
759
|
+
}
|
|
588
760
|
}
|
|
761
|
+
ensureColumn("conflicts", "entity_id", "TEXT");
|
|
762
|
+
ensureColumn("conflicts", "property", "TEXT");
|
|
763
|
+
ensureColumn("backup_metadata", "backup_timestamp", "TEXT");
|
|
764
|
+
ensureColumn("backup_metadata", "status", "TEXT DEFAULT 'completed'");
|
|
765
|
+
ensureColumn("backup_metadata", "file_size_bytes", "INTEGER");
|
|
766
|
+
ensureColumn("backup_metadata", "entities_count", "INTEGER");
|
|
767
|
+
ensureColumn("backup_metadata", "relationships_count", "INTEGER");
|
|
768
|
+
ensureColumn("backup_metadata", "facts_count", "INTEGER");
|
|
769
|
+
ensureColumn("backup_metadata", "notes", "TEXT");
|
|
770
|
+
ensureColumn("backup_metadata", "metadata", "TEXT");
|
|
771
|
+
connections.sqlite.exec(`
|
|
772
|
+
UPDATE backup_metadata
|
|
773
|
+
SET backup_timestamp = COALESCE(backup_timestamp, created_at),
|
|
774
|
+
status = COALESCE(status, 'completed')
|
|
775
|
+
`);
|
|
776
|
+
ensureColumn("relationships", "last_decayed_at", "TEXT");
|
|
589
777
|
connections.sqlite.exec(`
|
|
590
778
|
CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace);
|
|
591
779
|
CREATE INDEX IF NOT EXISTS idx_relationships_namespace ON relationships(namespace);
|
|
@@ -618,7 +806,9 @@ function initializeSkillLibrarySchema() {
|
|
|
618
806
|
try {
|
|
619
807
|
connections.sqlite.exec("ALTER TABLE skills ADD COLUMN namespace TEXT DEFAULT 'default'");
|
|
620
808
|
}
|
|
621
|
-
catch (_) {
|
|
809
|
+
catch (_) {
|
|
810
|
+
/* column already exists */
|
|
811
|
+
}
|
|
622
812
|
connections.sqlite.exec(`CREATE INDEX IF NOT EXISTS idx_skills_namespace ON skills(namespace);`);
|
|
623
813
|
// Apply lazy loading migration (idempotent - safe to run multiple times)
|
|
624
814
|
try {
|
|
@@ -771,56 +961,58 @@ async function handleStoreMemory(input) {
|
|
|
771
961
|
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
772
962
|
}
|
|
773
963
|
try {
|
|
774
|
-
// Validate input
|
|
775
964
|
const validatedInput = validateInput(StoreMemoryInputSchema, input);
|
|
776
|
-
const namespace = validatedInput.namespace ||
|
|
777
|
-
// Use pure UUID for Qdrant compatibility
|
|
965
|
+
const namespace = validatedInput.namespace || "default";
|
|
778
966
|
const id = randomUUID();
|
|
779
967
|
const now = Date.now();
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
console.error(`[INFO] Generated embedding for memory ${id}`);
|
|
786
|
-
}
|
|
787
|
-
catch (err) {
|
|
788
|
-
console.error("[WARN] Failed to generate embedding (continuing without):", err instanceof Error ? err.message : String(err));
|
|
789
|
-
}
|
|
968
|
+
const validFrom = toEpochMillis(validatedInput.valid_from) ?? now;
|
|
969
|
+
const validUntil = toEpochMillis(validatedInput.valid_until);
|
|
970
|
+
const expiredAt = toEpochMillis(validatedInput.expired_at);
|
|
971
|
+
if (validUntil !== undefined && validUntil <= validFrom) {
|
|
972
|
+
throw new McpError(ErrorCode.InvalidParams, "valid_until must be after valid_from");
|
|
790
973
|
}
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
974
|
+
const metadata = validatedInput.metadata || {};
|
|
975
|
+
const commit = connections.sqlite.transaction(() => {
|
|
976
|
+
connections.sqlite
|
|
977
|
+
.prepare(`
|
|
978
|
+
INSERT INTO memories (
|
|
979
|
+
id, content, memory_type, created_at, updated_at, metadata, namespace,
|
|
980
|
+
agent_id, agent_role, task_id, importance, access_count, last_accessed,
|
|
981
|
+
valid_from, valid_until, expired_at, archived_at, consolidated_at
|
|
982
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?, NULL, NULL)
|
|
983
|
+
`)
|
|
984
|
+
.run(id, validatedInput.content, validatedInput.memory_type, now, now, JSON.stringify(metadata), namespace, validatedInput.agent_id || null, validatedInput.agent_role || null, validatedInput.task_id || null, validatedInput.importance ?? 0.5, validFrom, validUntil ?? null, expiredAt ?? null);
|
|
985
|
+
connections.indexOutbox?.enqueue({
|
|
986
|
+
memoryId: id,
|
|
987
|
+
operation: "upsert",
|
|
988
|
+
namespace,
|
|
989
|
+
content: validatedInput.content,
|
|
990
|
+
memoryType: validatedInput.memory_type,
|
|
991
|
+
metadata,
|
|
992
|
+
agentId: validatedInput.agent_id,
|
|
993
|
+
taskId: validatedInput.task_id,
|
|
994
|
+
createdAt: now,
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
commit();
|
|
998
|
+
if (connections.indexOutbox) {
|
|
799
999
|
try {
|
|
800
|
-
await connections.
|
|
801
|
-
content: validatedInput.content,
|
|
802
|
-
memory_type: validatedInput.memory_type,
|
|
803
|
-
created_at: now,
|
|
804
|
-
namespace,
|
|
805
|
-
agent_id: validatedInput.agent_id,
|
|
806
|
-
task_id: validatedInput.task_id,
|
|
807
|
-
...validatedInput.metadata,
|
|
808
|
-
});
|
|
809
|
-
console.error(`[INFO] Stored embedding in Qdrant for ${id}`);
|
|
1000
|
+
await connections.indexOutbox.drain(20);
|
|
810
1001
|
}
|
|
811
|
-
catch (
|
|
812
|
-
console.error("[
|
|
813
|
-
throw new McpError(ErrorCode.InternalError, `Qdrant storage failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1002
|
+
catch (error) {
|
|
1003
|
+
console.error("[WARN] Inline index drain failed; mutation remains durable:", error instanceof Error ? error.message : String(error));
|
|
814
1004
|
}
|
|
815
1005
|
}
|
|
816
|
-
|
|
1006
|
+
const indexState = connections.indexOutbox?.getState(id) ?? "pending";
|
|
1007
|
+
console.error(`[INFO] Memory stored: ${id} (type: ${validatedInput.memory_type}, ns: ${namespace}, index: ${indexState})`);
|
|
817
1008
|
return {
|
|
818
1009
|
success: true,
|
|
819
1010
|
memory_id: id,
|
|
820
1011
|
namespace,
|
|
821
1012
|
timestamp: now,
|
|
822
1013
|
message: `Memory stored successfully (${validatedInput.memory_type})`,
|
|
823
|
-
has_embedding: !!
|
|
1014
|
+
has_embedding: indexState === "indexed" && !!connections.hybridSearch,
|
|
1015
|
+
index_state: indexState,
|
|
824
1016
|
};
|
|
825
1017
|
}
|
|
826
1018
|
catch (err) {
|
|
@@ -834,139 +1026,122 @@ async function handleRetrieveMemory(input) {
|
|
|
834
1026
|
if (!connections.sqlite) {
|
|
835
1027
|
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
836
1028
|
}
|
|
1029
|
+
const startedAt = performance.now();
|
|
837
1030
|
try {
|
|
838
|
-
// Validate input
|
|
839
1031
|
const validatedInput = validateInput(RetrieveMemoryInputSchema, input);
|
|
840
|
-
const namespace = validatedInput.namespace ||
|
|
1032
|
+
const namespace = validatedInput.namespace || "default";
|
|
841
1033
|
const maxResults = validatedInput.max_results || 5;
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (routerResults.length > 0) {
|
|
853
|
-
const classification = SmartRouter.classifyQuery(validatedInput.query);
|
|
854
|
-
console.error(`[INFO] SmartRouter hit: query="${validatedInput.query}", type=${classification.type}, ns=${namespace}, results=${routerResults.length}`);
|
|
855
|
-
return {
|
|
856
|
-
success: true,
|
|
857
|
-
query: validatedInput.query,
|
|
858
|
-
namespace,
|
|
859
|
-
routing_strategy: classification.type,
|
|
860
|
-
results: routerResults.map((r) => ({
|
|
861
|
-
id: r.id,
|
|
862
|
-
content: r.content,
|
|
863
|
-
memory_type: r.source,
|
|
864
|
-
created_at: r.timestamp instanceof Date ? r.timestamp.getTime() : r.timestamp,
|
|
865
|
-
similarity_score: r.similarity,
|
|
866
|
-
confidence: r.confidence,
|
|
867
|
-
})),
|
|
868
|
-
count: routerResults.length,
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
// Router returned empty - fall through to direct search
|
|
872
|
-
console.error(`[INFO] SmartRouter returned empty, falling back to direct search for: "${validatedInput.query}"`);
|
|
873
|
-
}
|
|
874
|
-
catch (err) {
|
|
875
|
-
console.error("[WARN] SmartRouter failed, falling back to direct search:", err instanceof Error ? err.message : String(err));
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
// Build Qdrant filter for namespace
|
|
879
|
-
const qdrantFilter = {
|
|
880
|
-
must: [
|
|
881
|
-
{ key: "namespace", match: { value: namespace } },
|
|
882
|
-
...(validatedInput.agent_id ? [{ key: "agent_id", match: { value: validatedInput.agent_id } }] : []),
|
|
883
|
-
...(validatedInput.task_id ? [{ key: "task_id", match: { value: validatedInput.task_id } }] : []),
|
|
884
|
-
],
|
|
885
|
-
...(validatedInput.exclude_agent ? {
|
|
886
|
-
must_not: [{ key: "agent_id", match: { value: validatedInput.exclude_agent } }]
|
|
887
|
-
} : {}),
|
|
1034
|
+
const commonFilters = {
|
|
1035
|
+
namespace,
|
|
1036
|
+
memoryTypes: validatedInput.memory_types,
|
|
1037
|
+
maxResults,
|
|
1038
|
+
agentId: validatedInput.agent_id,
|
|
1039
|
+
taskId: validatedInput.task_id,
|
|
1040
|
+
excludeAgent: validatedInput.exclude_agent,
|
|
1041
|
+
includeSuperseded: validatedInput.include_superseded,
|
|
1042
|
+
includeArchived: validatedInput.include_archived,
|
|
1043
|
+
asOf: toEpochMillis(validatedInput.as_of),
|
|
888
1044
|
};
|
|
889
|
-
|
|
890
|
-
|
|
1045
|
+
if (!connections.memoryRetriever) {
|
|
1046
|
+
throw new McpError(ErrorCode.InternalError, "Memory retriever not available");
|
|
1047
|
+
}
|
|
1048
|
+
// Redis can nominate recent candidates, but SQLite still authorizes and
|
|
1049
|
+
// hydrates every returned field. A cache miss falls through to FTS.
|
|
1050
|
+
if (validatedInput.routing_strategy === "fast_path" && connections.workingMemory) {
|
|
891
1051
|
try {
|
|
892
|
-
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1052
|
+
const cached = await connections.workingMemory.search(validatedInput.query, Math.min(maxResults * 4, 100), namespace);
|
|
1053
|
+
const scores = new Map(cached.map(({ memory, similarity }) => [memory.conversationId, similarity]));
|
|
1054
|
+
const results = connections.memoryRetriever.retrieveByIds(cached.map(({ memory }) => memory.conversationId), { ...commonFilters, similarityScores: scores, source: "fast_path" });
|
|
1055
|
+
if (results.length > 0) {
|
|
1056
|
+
retrievalMonitor.recordCacheHit();
|
|
1057
|
+
return formatRetrievalResponse(validatedInput.query, namespace, "fast_path", results, validatedInput.explain
|
|
1058
|
+
? {
|
|
1059
|
+
requested_strategy: "fast_path",
|
|
1060
|
+
effective_strategy: "fast_path",
|
|
1061
|
+
stages: [{ stage: "cache", candidate_count: cached.length, status: "ok" }],
|
|
1062
|
+
returned_count: results.length,
|
|
1063
|
+
}
|
|
1064
|
+
: undefined);
|
|
900
1065
|
}
|
|
901
|
-
|
|
902
|
-
return {
|
|
903
|
-
success: true,
|
|
904
|
-
query: validatedInput.query,
|
|
905
|
-
namespace,
|
|
906
|
-
routing_strategy: "semantic",
|
|
907
|
-
results: searchResults.map((r) => ({
|
|
908
|
-
id: r.id,
|
|
909
|
-
content: r.payload?.content,
|
|
910
|
-
memory_type: r.payload?.memory_type,
|
|
911
|
-
created_at: r.payload?.created_at,
|
|
912
|
-
similarity_score: r.fusedScore,
|
|
913
|
-
rank: r.rank,
|
|
914
|
-
metadata: r.payload,
|
|
915
|
-
})),
|
|
916
|
-
count: searchResults.length,
|
|
917
|
-
};
|
|
1066
|
+
retrievalMonitor.recordCacheMiss();
|
|
918
1067
|
}
|
|
919
|
-
catch (
|
|
920
|
-
|
|
1068
|
+
catch (error) {
|
|
1069
|
+
retrievalMonitor.recordCacheMiss();
|
|
1070
|
+
console.error("[WARN] Fast-path lookup failed; falling back to canonical retrieval:", error instanceof Error ? error.message : String(error));
|
|
921
1071
|
}
|
|
922
1072
|
}
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
params.push(validatedInput.agent_id);
|
|
936
|
-
}
|
|
937
|
-
if (validatedInput.task_id) {
|
|
938
|
-
query += " AND task_id = ?";
|
|
939
|
-
params.push(validatedInput.task_id);
|
|
940
|
-
}
|
|
941
|
-
if (validatedInput.exclude_agent) {
|
|
942
|
-
query += " AND (agent_id IS NULL OR agent_id != ?)";
|
|
943
|
-
params.push(validatedInput.exclude_agent);
|
|
944
|
-
}
|
|
945
|
-
query += " ORDER BY created_at DESC LIMIT ?";
|
|
946
|
-
params.push(maxResults);
|
|
947
|
-
const stmt = connections.sqlite.prepare(query);
|
|
948
|
-
const results = stmt.all(...params);
|
|
949
|
-
console.error(`[INFO] Text search (fallback): query="${validatedInput.query}", ns=${namespace}, results=${results.length}`);
|
|
950
|
-
return {
|
|
951
|
-
success: true,
|
|
1073
|
+
// The legacy graph route did not implement the advertised PPR algorithm
|
|
1074
|
+
// and returned non-memory entities. Preserve the public option as a safe
|
|
1075
|
+
// compatibility alias while keeping the memory contract truthful.
|
|
1076
|
+
const requestedStrategy = validatedInput.routing_strategy;
|
|
1077
|
+
const strategy = requestedStrategy === "semantic" ||
|
|
1078
|
+
requestedStrategy === "full_text" ||
|
|
1079
|
+
requestedStrategy === "hybrid"
|
|
1080
|
+
? requestedStrategy
|
|
1081
|
+
: requestedStrategy === "fast_path"
|
|
1082
|
+
? "full_text"
|
|
1083
|
+
: "auto";
|
|
1084
|
+
const response = await connections.memoryRetriever.retrieve({
|
|
952
1085
|
query: validatedInput.query,
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1086
|
+
...commonFilters,
|
|
1087
|
+
strategy,
|
|
1088
|
+
explain: validatedInput.explain,
|
|
1089
|
+
});
|
|
1090
|
+
const trace = response.trace
|
|
1091
|
+
? {
|
|
1092
|
+
requested_strategy: requestedStrategy ?? "auto",
|
|
1093
|
+
effective_strategy: response.strategy,
|
|
1094
|
+
compatibility_fallback: requestedStrategy === "graph" ? "hybrid_or_full_text" : undefined,
|
|
1095
|
+
candidate_limit: response.trace.candidateLimit,
|
|
1096
|
+
stages: response.trace.stages.map((stage) => ({
|
|
1097
|
+
stage: stage.stage,
|
|
1098
|
+
candidate_count: stage.candidateCount,
|
|
1099
|
+
status: stage.status,
|
|
1100
|
+
fallback: stage.fallback,
|
|
1101
|
+
})),
|
|
1102
|
+
returned_count: response.trace.returnedCount,
|
|
1103
|
+
}
|
|
1104
|
+
: undefined;
|
|
1105
|
+
console.error(`[INFO] Canonical retrieval: query="${validatedInput.query}", ns=${namespace}, ` +
|
|
1106
|
+
`strategy=${response.strategy}, results=${response.results.length}`);
|
|
1107
|
+
return formatRetrievalResponse(validatedInput.query, namespace, response.strategy, response.results, trace);
|
|
966
1108
|
}
|
|
967
1109
|
catch (err) {
|
|
1110
|
+
if (err instanceof McpError)
|
|
1111
|
+
throw err;
|
|
968
1112
|
throw new McpError(ErrorCode.InternalError, `Failed to retrieve memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
969
1113
|
}
|
|
1114
|
+
finally {
|
|
1115
|
+
retrievalMonitor.recordLatency(performance.now() - startedAt);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function formatRetrievalResponse(query, namespace, strategy, results, trace) {
|
|
1119
|
+
return {
|
|
1120
|
+
success: true,
|
|
1121
|
+
query,
|
|
1122
|
+
namespace,
|
|
1123
|
+
routing_strategy: strategy,
|
|
1124
|
+
results: results.map((result) => ({
|
|
1125
|
+
id: result.id,
|
|
1126
|
+
content: result.content,
|
|
1127
|
+
memory_type: result.memoryType,
|
|
1128
|
+
created_at: result.createdAt,
|
|
1129
|
+
updated_at: result.updatedAt,
|
|
1130
|
+
namespace: result.namespace,
|
|
1131
|
+
agent_id: result.agentId,
|
|
1132
|
+
agent_role: result.agentRole,
|
|
1133
|
+
task_id: result.taskId,
|
|
1134
|
+
similarity_score: result.similarityScore,
|
|
1135
|
+
dense_score: result.denseScore,
|
|
1136
|
+
lexical_score: result.lexicalScore,
|
|
1137
|
+
fused_score: result.fusedScore,
|
|
1138
|
+
rank: result.rank,
|
|
1139
|
+
sources: result.sources,
|
|
1140
|
+
metadata: result.metadata,
|
|
1141
|
+
})),
|
|
1142
|
+
count: results.length,
|
|
1143
|
+
...(trace ? { trace } : {}),
|
|
1144
|
+
};
|
|
970
1145
|
}
|
|
971
1146
|
/**
|
|
972
1147
|
* Handle list_recent tool call
|
|
@@ -978,10 +1153,58 @@ async function handleListRecent(input) {
|
|
|
978
1153
|
try {
|
|
979
1154
|
// Validate input
|
|
980
1155
|
const validatedInput = validateInput(ListRecentInputSchema, input);
|
|
981
|
-
const namespace = validatedInput.namespace ||
|
|
1156
|
+
const namespace = validatedInput.namespace || "default";
|
|
982
1157
|
const limit = validatedInput.limit || 5;
|
|
983
|
-
|
|
984
|
-
|
|
1158
|
+
const now = Date.now();
|
|
1159
|
+
let query = `SELECT id, content, memory_type, created_at, agent_id, task_id
|
|
1160
|
+
FROM memories
|
|
1161
|
+
WHERE namespace = ?
|
|
1162
|
+
AND (valid_from IS NULL OR ${sqliteEpochMillisExpression("valid_from")} IS NULL OR ${sqliteEpochMillisExpression("valid_from")} <= ?)
|
|
1163
|
+
AND (valid_until IS NULL OR ${sqliteEpochMillisExpression("valid_until")} IS NULL OR ${sqliteEpochMillisExpression("valid_until")} > ?)
|
|
1164
|
+
AND (expired_at IS NULL OR ${sqliteEpochMillisExpression("expired_at")} IS NULL OR ${sqliteEpochMillisExpression("expired_at")} > ?)
|
|
1165
|
+
AND (archived_at IS NULL OR ${sqliteEpochMillisExpression("archived_at")} IS NULL OR ${sqliteEpochMillisExpression("archived_at")} > ?)
|
|
1166
|
+
AND (metadata IS NULL OR json_valid(metadata) = 0 OR (
|
|
1167
|
+
COALESCE(json_extract(metadata, '$.is_valid'), 1) != 0
|
|
1168
|
+
AND COALESCE(json_extract(metadata, '$.invalid'), 0) = 0
|
|
1169
|
+
AND COALESCE(json_extract(metadata, '$.is_archived'), 0) = 0
|
|
1170
|
+
AND COALESCE(json_extract(metadata, '$.archived'), 0) = 0
|
|
1171
|
+
AND (
|
|
1172
|
+
valid_from IS NOT NULL
|
|
1173
|
+
OR
|
|
1174
|
+
json_extract(metadata, '$.valid_from') IS NULL
|
|
1175
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.valid_from')")} IS NULL
|
|
1176
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.valid_from')")} <= ?
|
|
1177
|
+
)
|
|
1178
|
+
AND (
|
|
1179
|
+
valid_until IS NOT NULL
|
|
1180
|
+
OR
|
|
1181
|
+
json_extract(metadata, '$.valid_until') IS NULL
|
|
1182
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.valid_until')")} IS NULL
|
|
1183
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.valid_until')")} > ?
|
|
1184
|
+
)
|
|
1185
|
+
AND (
|
|
1186
|
+
expired_at IS NOT NULL
|
|
1187
|
+
OR
|
|
1188
|
+
json_extract(metadata, '$.expired_at') IS NULL
|
|
1189
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.expired_at')")} IS NULL
|
|
1190
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.expired_at')")} > ?
|
|
1191
|
+
)
|
|
1192
|
+
AND (
|
|
1193
|
+
archived_at IS NOT NULL
|
|
1194
|
+
OR
|
|
1195
|
+
json_extract(metadata, '$.archived_at') IS NULL
|
|
1196
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.archived_at')")} IS NULL
|
|
1197
|
+
OR ${sqliteEpochMillisExpression("json_extract(metadata, '$.archived_at')")} > ?
|
|
1198
|
+
)
|
|
1199
|
+
AND (
|
|
1200
|
+
json_extract(metadata, '$.superseded_by') IS NULL
|
|
1201
|
+
OR (
|
|
1202
|
+
json_extract(metadata, '$.superseded_at') IS NOT NULL
|
|
1203
|
+
AND ${sqliteEpochMillisExpression("json_extract(metadata, '$.superseded_at')")} > ?
|
|
1204
|
+
)
|
|
1205
|
+
)
|
|
1206
|
+
))`;
|
|
1207
|
+
const params = [namespace, now, now, now, now, now, now, now, now, now];
|
|
985
1208
|
if (validatedInput.memory_type) {
|
|
986
1209
|
query += " AND memory_type = ?";
|
|
987
1210
|
params.push(validatedInput.memory_type);
|
|
@@ -1021,7 +1244,7 @@ async function handleGetStats(input) {
|
|
|
1021
1244
|
try {
|
|
1022
1245
|
// Validate input
|
|
1023
1246
|
const validatedInput = validateInput(GetStatsInputSchema, input);
|
|
1024
|
-
const namespace = validatedInput.namespace ||
|
|
1247
|
+
const namespace = validatedInput.namespace || "default";
|
|
1025
1248
|
const countStmt = connections.sqlite.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ?");
|
|
1026
1249
|
const countResult = countStmt.get(namespace);
|
|
1027
1250
|
const totalMemories = countResult.count;
|
|
@@ -1046,14 +1269,53 @@ async function handleGetStats(input) {
|
|
|
1046
1269
|
}
|
|
1047
1270
|
stats.memory_type_breakdown = memoryTypeBreakdown;
|
|
1048
1271
|
if (validatedInput.detailed) {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1272
|
+
const pageCount = connections.sqlite.pragma("page_count", { simple: true });
|
|
1273
|
+
const pageSize = connections.sqlite.pragma("page_size", { simple: true });
|
|
1274
|
+
const retrievalMetrics = retrievalMonitor.getMetrics();
|
|
1275
|
+
const lifecycle = connections.sqlite
|
|
1276
|
+
.prepare(`
|
|
1277
|
+
SELECT
|
|
1278
|
+
SUM(CASE WHEN valid_until IS NOT NULL THEN 1 ELSE 0 END) AS closed,
|
|
1279
|
+
SUM(CASE WHEN archived_at IS NOT NULL THEN 1 ELSE 0 END) AS archived,
|
|
1280
|
+
SUM(access_count) AS total_accesses
|
|
1281
|
+
FROM memories WHERE namespace = ?
|
|
1282
|
+
`)
|
|
1283
|
+
.get(namespace);
|
|
1284
|
+
stats.sqlite_storage_bytes = pageCount * pageSize;
|
|
1285
|
+
stats.retrieval_latency_ms = retrievalMetrics.latency;
|
|
1286
|
+
stats.retrieval_requests_observed = retrievalMetrics.totalRequests;
|
|
1287
|
+
stats.cache_hit_rate =
|
|
1288
|
+
retrievalMetrics.totalCacheRequests > 0 ? retrievalMetrics.cacheHitRate : null;
|
|
1289
|
+
stats.cache_requests_observed = retrievalMetrics.totalCacheRequests;
|
|
1290
|
+
stats.index_outbox_pending = connections.indexOutbox?.getPendingCount() ?? null;
|
|
1291
|
+
stats.lifecycle = {
|
|
1292
|
+
closed: lifecycle.closed ?? 0,
|
|
1293
|
+
archived: lifecycle.archived ?? 0,
|
|
1294
|
+
total_accesses: lifecycle.total_accesses ?? 0,
|
|
1055
1295
|
};
|
|
1056
|
-
stats.
|
|
1296
|
+
stats.consolidation_scheduler = connections.consolidationScheduler?.getStatus() ?? null;
|
|
1297
|
+
if (connections.workingMemory) {
|
|
1298
|
+
try {
|
|
1299
|
+
stats.working_memory = await connections.workingMemory.getStats(namespace);
|
|
1300
|
+
}
|
|
1301
|
+
catch (error) {
|
|
1302
|
+
stats.working_memory = {
|
|
1303
|
+
available: false,
|
|
1304
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
if (connections.hybridSearch) {
|
|
1309
|
+
try {
|
|
1310
|
+
stats.vector_index = await connections.hybridSearch.getCollectionStats();
|
|
1311
|
+
}
|
|
1312
|
+
catch (error) {
|
|
1313
|
+
stats.vector_index = {
|
|
1314
|
+
available: false,
|
|
1315
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1057
1319
|
}
|
|
1058
1320
|
console.error(`[INFO] Stats retrieved: ${totalMemories} total memories`);
|
|
1059
1321
|
return stats;
|
|
@@ -1071,45 +1333,49 @@ async function handleDeleteMemory(input) {
|
|
|
1071
1333
|
}
|
|
1072
1334
|
try {
|
|
1073
1335
|
const validatedInput = validateInput(DeleteMemoryInputSchema, input);
|
|
1074
|
-
const namespace = validatedInput.namespace ||
|
|
1336
|
+
const namespace = validatedInput.namespace || "default";
|
|
1075
1337
|
// Verify memory exists in SQLite
|
|
1076
|
-
const existing = connections.sqlite
|
|
1338
|
+
const existing = connections.sqlite
|
|
1339
|
+
.prepare(`SELECT id, content, memory_type, created_at, metadata, agent_id, task_id
|
|
1340
|
+
FROM memories WHERE id = ? AND namespace = ?`)
|
|
1341
|
+
.get(validatedInput.memory_id, namespace);
|
|
1077
1342
|
if (!existing) {
|
|
1078
1343
|
return {
|
|
1079
1344
|
success: false,
|
|
1080
1345
|
message: `Memory not found: ${validatedInput.memory_id} in namespace '${namespace}'`,
|
|
1081
1346
|
};
|
|
1082
1347
|
}
|
|
1083
|
-
//
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1348
|
+
// Canonical deletion and its projection tombstone commit together. Derived
|
|
1349
|
+
// stores can be offline; the outbox is retried on later starts/writes.
|
|
1350
|
+
const commit = connections.sqlite.transaction(() => {
|
|
1351
|
+
connections.sqlite
|
|
1352
|
+
.prepare("DELETE FROM facts WHERE source_conversation = ? AND namespace = ?")
|
|
1353
|
+
.run(validatedInput.memory_id, namespace);
|
|
1354
|
+
connections.sqlite
|
|
1355
|
+
.prepare("DELETE FROM memories WHERE id = ? AND namespace = ?")
|
|
1356
|
+
.run(validatedInput.memory_id, namespace);
|
|
1357
|
+
connections.indexOutbox?.enqueue({
|
|
1358
|
+
memoryId: existing.id,
|
|
1359
|
+
operation: "delete",
|
|
1360
|
+
namespace,
|
|
1361
|
+
content: existing.content,
|
|
1362
|
+
memoryType: existing.memory_type,
|
|
1363
|
+
metadata: safeJsonObject(existing.metadata),
|
|
1364
|
+
agentId: existing.agent_id ?? undefined,
|
|
1365
|
+
taskId: existing.task_id ?? undefined,
|
|
1366
|
+
createdAt: existing.created_at,
|
|
1367
|
+
});
|
|
1368
|
+
});
|
|
1369
|
+
commit();
|
|
1370
|
+
if (connections.indexOutbox) {
|
|
1096
1371
|
try {
|
|
1097
|
-
|
|
1098
|
-
await connections.redis.del(`${namespace}:working:${validatedInput.memory_id}`);
|
|
1099
|
-
// Remove from the recent list
|
|
1100
|
-
await connections.redis.lrem(`${namespace}:working:recent`, 0, validatedInput.memory_id);
|
|
1372
|
+
await connections.indexOutbox.drain(20);
|
|
1101
1373
|
}
|
|
1102
|
-
catch (
|
|
1103
|
-
console.error("[WARN]
|
|
1374
|
+
catch (error) {
|
|
1375
|
+
console.error("[WARN] Inline index deletion failed; tombstone remains durable:", error instanceof Error ? error.message : String(error));
|
|
1104
1376
|
}
|
|
1105
1377
|
}
|
|
1106
|
-
|
|
1107
|
-
try {
|
|
1108
|
-
connections.sqlite.prepare("DELETE FROM facts WHERE source_conversation = ? AND namespace = ?").run(validatedInput.memory_id, namespace);
|
|
1109
|
-
}
|
|
1110
|
-
catch (err) {
|
|
1111
|
-
console.error("[WARN] Failed to clean up orphaned facts (continuing):", err instanceof Error ? err.message : String(err));
|
|
1112
|
-
}
|
|
1378
|
+
const indexState = connections.indexOutbox?.getState(existing.id) ?? "pending";
|
|
1113
1379
|
console.error(`[INFO] Memory deleted: ${validatedInput.memory_id} (type: ${existing.memory_type}, ns: ${namespace})`);
|
|
1114
1380
|
return {
|
|
1115
1381
|
success: true,
|
|
@@ -1120,6 +1386,7 @@ async function handleDeleteMemory(input) {
|
|
|
1120
1386
|
created_at: existing.created_at,
|
|
1121
1387
|
},
|
|
1122
1388
|
namespace,
|
|
1389
|
+
index_state: indexState,
|
|
1123
1390
|
message: `Memory deleted successfully`,
|
|
1124
1391
|
};
|
|
1125
1392
|
}
|
|
@@ -1130,6 +1397,19 @@ async function handleDeleteMemory(input) {
|
|
|
1130
1397
|
throw new McpError(ErrorCode.InternalError, `Failed to delete memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
1131
1398
|
}
|
|
1132
1399
|
}
|
|
1400
|
+
function safeJsonObject(value) {
|
|
1401
|
+
if (!value)
|
|
1402
|
+
return {};
|
|
1403
|
+
try {
|
|
1404
|
+
const parsed = JSON.parse(value);
|
|
1405
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
1406
|
+
? parsed
|
|
1407
|
+
: {};
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
return {};
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1133
1413
|
/**
|
|
1134
1414
|
* Handle vesper_enable tool call
|
|
1135
1415
|
*/
|
|
@@ -1185,11 +1465,11 @@ async function handleRecordSkillOutcome(input) {
|
|
|
1185
1465
|
if (validatedInput.satisfaction === undefined) {
|
|
1186
1466
|
throw new McpError(ErrorCode.InvalidParams, "satisfaction is required for success outcome");
|
|
1187
1467
|
}
|
|
1188
|
-
connections.skillLibrary.recordSuccess(validatedInput.skill_id, validatedInput.satisfaction);
|
|
1468
|
+
connections.skillLibrary.recordSuccess(validatedInput.skill_id, validatedInput.satisfaction, validatedInput.namespace);
|
|
1189
1469
|
console.error(`[INFO] Recorded skill success: ${validatedInput.skill_id} (satisfaction: ${validatedInput.satisfaction})`);
|
|
1190
1470
|
}
|
|
1191
1471
|
else {
|
|
1192
|
-
connections.skillLibrary.recordFailure(validatedInput.skill_id);
|
|
1472
|
+
connections.skillLibrary.recordFailure(validatedInput.skill_id, validatedInput.namespace);
|
|
1193
1473
|
console.error(`[INFO] Recorded skill failure: ${validatedInput.skill_id}`);
|
|
1194
1474
|
}
|
|
1195
1475
|
return {
|
|
@@ -1235,7 +1515,7 @@ async function handleLoadSkill(input) {
|
|
|
1235
1515
|
let fullSkill = null;
|
|
1236
1516
|
if (connections.workingMemory) {
|
|
1237
1517
|
try {
|
|
1238
|
-
const cached = await connections.workingMemory.getCachedSkill(validatedInput.skill_id);
|
|
1518
|
+
const cached = await connections.workingMemory.getCachedSkill(validatedInput.skill_id, validatedInput.namespace);
|
|
1239
1519
|
if (cached) {
|
|
1240
1520
|
fullSkill = cached.skill;
|
|
1241
1521
|
fromCache = true;
|
|
@@ -1249,14 +1529,14 @@ async function handleLoadSkill(input) {
|
|
|
1249
1529
|
}
|
|
1250
1530
|
// Step 2: Load from database if not cached
|
|
1251
1531
|
if (!fullSkill) {
|
|
1252
|
-
fullSkill = connections.skillLibrary.loadFull(validatedInput.skill_id);
|
|
1532
|
+
fullSkill = connections.skillLibrary.loadFull(validatedInput.skill_id, validatedInput.namespace);
|
|
1253
1533
|
if (!fullSkill) {
|
|
1254
1534
|
throw new McpError(ErrorCode.InvalidParams, `Skill not found: ${validatedInput.skill_id}`);
|
|
1255
1535
|
}
|
|
1256
1536
|
// Cache the loaded skill (if working memory available)
|
|
1257
1537
|
if (connections.workingMemory) {
|
|
1258
1538
|
try {
|
|
1259
|
-
await connections.workingMemory.cacheSkill(fullSkill, 3600); // 1 hour TTL
|
|
1539
|
+
await connections.workingMemory.cacheSkill(fullSkill, 3600, validatedInput.namespace); // 1 hour TTL
|
|
1260
1540
|
}
|
|
1261
1541
|
catch (err) {
|
|
1262
1542
|
console.warn(`[WARN] Failed to cache skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1265,7 +1545,7 @@ async function handleLoadSkill(input) {
|
|
|
1265
1545
|
}
|
|
1266
1546
|
const loadTime = Date.now() - startTime;
|
|
1267
1547
|
console.error(`[INFO] Loaded full skill: ${fullSkill.name} (${validatedInput.skill_id}) ` +
|
|
1268
|
-
`in ${loadTime}ms (${fromCache ?
|
|
1548
|
+
`in ${loadTime}ms (${fromCache ? "cache" : "database"})`);
|
|
1269
1549
|
return {
|
|
1270
1550
|
success: true,
|
|
1271
1551
|
skill: {
|
|
@@ -1293,7 +1573,7 @@ async function handleLoadSkill(input) {
|
|
|
1293
1573
|
},
|
|
1294
1574
|
load_time_ms: loadTime,
|
|
1295
1575
|
from_cache: fromCache,
|
|
1296
|
-
message: `Loaded full skill: ${fullSkill.name}${fromCache ?
|
|
1576
|
+
message: `Loaded full skill: ${fullSkill.name}${fromCache ? " (cached)" : ""}`,
|
|
1297
1577
|
};
|
|
1298
1578
|
}
|
|
1299
1579
|
catch (err) {
|
|
@@ -1315,63 +1595,195 @@ async function handleShareContext(input) {
|
|
|
1315
1595
|
try {
|
|
1316
1596
|
const validatedInput = validateInput(ShareContextInputSchema, input);
|
|
1317
1597
|
const { source_namespace, target_namespace } = validatedInput;
|
|
1598
|
+
if (source_namespace === target_namespace) {
|
|
1599
|
+
throw new McpError(ErrorCode.InvalidParams, "source_namespace and target_namespace must be different");
|
|
1600
|
+
}
|
|
1318
1601
|
const maxItems = validatedInput.max_items || 10;
|
|
1319
1602
|
const now = Date.now();
|
|
1320
1603
|
const sharedItems = [];
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1604
|
+
let memoryIds;
|
|
1605
|
+
if (validatedInput.query && connections.memoryRetriever) {
|
|
1606
|
+
const selected = await connections.memoryRetriever.retrieve({
|
|
1607
|
+
query: validatedInput.query,
|
|
1608
|
+
namespace: source_namespace,
|
|
1609
|
+
taskId: validatedInput.task_id,
|
|
1610
|
+
maxResults: maxItems,
|
|
1611
|
+
strategy: "auto",
|
|
1612
|
+
});
|
|
1613
|
+
memoryIds = selected.results.map((memory) => memory.id);
|
|
1327
1614
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1615
|
+
else {
|
|
1616
|
+
let selectionSql = `
|
|
1617
|
+
SELECT id FROM memories
|
|
1618
|
+
WHERE namespace = ? AND archived_at IS NULL
|
|
1619
|
+
AND (valid_from IS NULL OR valid_from <= ?)
|
|
1620
|
+
AND (valid_until IS NULL OR valid_until > ?)
|
|
1621
|
+
AND (expired_at IS NULL OR expired_at > ?)
|
|
1622
|
+
`;
|
|
1623
|
+
const selectionParams = [source_namespace, now, now, now];
|
|
1624
|
+
if (validatedInput.task_id) {
|
|
1625
|
+
selectionSql += " AND task_id = ?";
|
|
1626
|
+
selectionParams.push(validatedInput.task_id);
|
|
1627
|
+
}
|
|
1628
|
+
selectionSql += " ORDER BY created_at DESC LIMIT ?";
|
|
1629
|
+
selectionParams.push(maxItems);
|
|
1630
|
+
memoryIds = connections.sqlite.prepare(selectionSql).all(...selectionParams).map((row) => row.id);
|
|
1339
1631
|
}
|
|
1340
|
-
|
|
1632
|
+
const memories = memoryIds.length > 0
|
|
1633
|
+
? connections.sqlite
|
|
1634
|
+
.prepare(`
|
|
1635
|
+
SELECT id, content, memory_type, created_at, metadata, agent_id,
|
|
1636
|
+
agent_role, task_id, importance, valid_from, valid_until, expired_at
|
|
1637
|
+
FROM memories WHERE namespace = ? AND id IN (${memoryIds.map(() => "?").join(",")})
|
|
1638
|
+
`)
|
|
1639
|
+
.all(source_namespace, ...memoryIds)
|
|
1640
|
+
: [];
|
|
1641
|
+
const memoryOrder = new Map(memoryIds.map((id, index) => [id, index]));
|
|
1642
|
+
memories.sort((a, b) => (memoryOrder.get(a.id) ?? 0) - (memoryOrder.get(b.id) ?? 0));
|
|
1643
|
+
const selectedSkills = validatedInput.include_skills
|
|
1644
|
+
? connections.sqlite
|
|
1645
|
+
.prepare(`
|
|
1646
|
+
SELECT * FROM skills
|
|
1647
|
+
WHERE namespace = ? AND is_archived = 0
|
|
1648
|
+
AND (? IS NULL OR name LIKE ? OR description LIKE ? OR triggers LIKE ?)
|
|
1649
|
+
ORDER BY avg_user_satisfaction DESC, success_count DESC
|
|
1650
|
+
LIMIT ?
|
|
1651
|
+
`)
|
|
1652
|
+
.all(source_namespace, validatedInput.query ?? null, `%${validatedInput.query ?? ""}%`, `%${validatedInput.query ?? ""}%`, `%${validatedInput.query ?? ""}%`, maxItems)
|
|
1653
|
+
: [];
|
|
1654
|
+
const selectedEntities = validatedInput.include_entities
|
|
1655
|
+
? connections.sqlite
|
|
1656
|
+
.prepare("SELECT * FROM entities WHERE namespace = ? LIMIT ?")
|
|
1657
|
+
.all(source_namespace, maxItems)
|
|
1658
|
+
: [];
|
|
1659
|
+
let skillsShared = 0;
|
|
1341
1660
|
let entityCount = 0;
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1661
|
+
const handoffId = randomUUID();
|
|
1662
|
+
const handoffContent = `Context handoff from namespace '${source_namespace}': ${memories.length} memories selected`;
|
|
1663
|
+
const handoffMetadata = {
|
|
1664
|
+
handoff: true,
|
|
1665
|
+
source_namespace,
|
|
1666
|
+
query: validatedInput.query ?? null,
|
|
1667
|
+
};
|
|
1668
|
+
const commit = connections.sqlite.transaction(() => {
|
|
1669
|
+
for (const memory of memories) {
|
|
1670
|
+
const newId = randomUUID();
|
|
1671
|
+
const copiedMetadata = {
|
|
1672
|
+
...safeJsonObject(memory.metadata),
|
|
1673
|
+
shared_from_namespace: source_namespace,
|
|
1674
|
+
shared_from_memory_id: memory.id,
|
|
1675
|
+
shared_at: now,
|
|
1676
|
+
};
|
|
1677
|
+
connections.sqlite
|
|
1678
|
+
.prepare(`
|
|
1679
|
+
INSERT INTO memories (
|
|
1680
|
+
id, content, memory_type, created_at, updated_at, metadata, namespace,
|
|
1681
|
+
agent_id, agent_role, task_id, importance, access_count, last_accessed,
|
|
1682
|
+
valid_from, valid_until, expired_at, archived_at, consolidated_at
|
|
1683
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, ?, ?, ?, NULL, NULL)
|
|
1684
|
+
`)
|
|
1685
|
+
.run(newId, memory.content, memory.memory_type, memory.created_at, now, JSON.stringify(copiedMetadata), target_namespace, memory.agent_id, memory.agent_role, memory.task_id, memory.importance ?? 0.5, memory.valid_from ?? memory.created_at, memory.valid_until, memory.expired_at);
|
|
1686
|
+
connections.indexOutbox?.enqueue({
|
|
1687
|
+
memoryId: newId,
|
|
1688
|
+
operation: "upsert",
|
|
1689
|
+
namespace: target_namespace,
|
|
1690
|
+
content: memory.content,
|
|
1691
|
+
memoryType: memory.memory_type,
|
|
1692
|
+
metadata: copiedMetadata,
|
|
1693
|
+
agentId: memory.agent_id ?? undefined,
|
|
1694
|
+
taskId: memory.task_id ?? undefined,
|
|
1695
|
+
createdAt: memory.created_at,
|
|
1696
|
+
});
|
|
1697
|
+
sharedItems.push({ type: "memory", id: newId, original_id: memory.id });
|
|
1698
|
+
}
|
|
1699
|
+
for (const skill of selectedSkills) {
|
|
1700
|
+
const newId = randomUUID();
|
|
1701
|
+
const targetName = `${skill.name} [${target_namespace}]`;
|
|
1702
|
+
const result = connections.sqlite
|
|
1703
|
+
.prepare(`
|
|
1704
|
+
INSERT OR IGNORE INTO skills (
|
|
1705
|
+
id, name, description, summary, category, code, code_type, triggers,
|
|
1706
|
+
prerequisites, success_count, failure_count, avg_user_satisfaction,
|
|
1707
|
+
uses_skills, used_by_skills, namespace, is_archived, created_from,
|
|
1708
|
+
created_at, last_used, last_modified, version, notes, metadata
|
|
1709
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?, ?)
|
|
1710
|
+
`)
|
|
1711
|
+
.run(newId, targetName, skill.description, skill.summary, skill.category, skill.code, skill.code_type, skill.triggers, skill.prerequisites, skill.success_count, skill.failure_count, skill.avg_user_satisfaction, skill.uses_skills, skill.used_by_skills, target_namespace, skill.created_from, skill.created_at, skill.last_used, now, skill.version, skill.notes, skill.metadata);
|
|
1712
|
+
if (result.changes > 0) {
|
|
1713
|
+
skillsShared++;
|
|
1714
|
+
sharedItems.push({ type: "skill", id: newId, original_id: skill.id });
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
for (const entity of selectedEntities) {
|
|
1718
|
+
const existing = connections.sqlite
|
|
1719
|
+
.prepare("SELECT id FROM entities WHERE namespace = ? AND name = ? AND type = ? LIMIT 1")
|
|
1720
|
+
.get(target_namespace, entity.name, entity.type);
|
|
1721
|
+
if (existing) {
|
|
1722
|
+
connections.sqlite
|
|
1723
|
+
.prepare(`UPDATE entities
|
|
1724
|
+
SET description = COALESCE(?, description),
|
|
1725
|
+
confidence = MAX(confidence, ?), last_accessed = ?
|
|
1726
|
+
WHERE id = ?`)
|
|
1727
|
+
.run(entity.description, entity.confidence ?? 1, new Date(now).toISOString(), existing.id);
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
connections.sqlite
|
|
1731
|
+
.prepare(`INSERT INTO entities (
|
|
1732
|
+
id, name, type, description, confidence, created_at,
|
|
1733
|
+
last_accessed, access_count, namespace
|
|
1734
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1735
|
+
.run(randomUUID(), entity.name, entity.type, entity.description, entity.confidence ?? 1, entity.created_at ?? new Date(now).toISOString(), new Date(now).toISOString(), entity.access_count ?? 1, target_namespace);
|
|
1348
1736
|
}
|
|
1737
|
+
entityCount++;
|
|
1738
|
+
}
|
|
1739
|
+
connections.sqlite
|
|
1740
|
+
.prepare(`
|
|
1741
|
+
INSERT INTO memories (
|
|
1742
|
+
id, content, memory_type, created_at, updated_at, metadata, namespace,
|
|
1743
|
+
importance, access_count, valid_from, consolidated_at
|
|
1744
|
+
) VALUES (?, ?, 'episodic', ?, ?, ?, ?, 0.5, 0, ?, NULL)
|
|
1745
|
+
`)
|
|
1746
|
+
.run(handoffId, handoffContent, now, now, JSON.stringify(handoffMetadata), target_namespace, now);
|
|
1747
|
+
connections.indexOutbox?.enqueue({
|
|
1748
|
+
memoryId: handoffId,
|
|
1749
|
+
operation: "upsert",
|
|
1750
|
+
namespace: target_namespace,
|
|
1751
|
+
content: handoffContent,
|
|
1752
|
+
memoryType: "episodic",
|
|
1753
|
+
metadata: handoffMetadata,
|
|
1754
|
+
createdAt: now,
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
commit();
|
|
1758
|
+
if (connections.indexOutbox) {
|
|
1759
|
+
try {
|
|
1760
|
+
await connections.indexOutbox.drain(maxItems + 1);
|
|
1761
|
+
}
|
|
1762
|
+
catch (error) {
|
|
1763
|
+
console.error("[WARN] Shared-context index drain failed; mutations remain durable:", error instanceof Error ? error.message : String(error));
|
|
1349
1764
|
}
|
|
1350
1765
|
}
|
|
1351
|
-
|
|
1352
|
-
const handoffId = randomUUID();
|
|
1353
|
-
connections.sqlite.prepare(`
|
|
1354
|
-
INSERT INTO memories (id, content, memory_type, created_at, updated_at, metadata, namespace)
|
|
1355
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1356
|
-
`).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);
|
|
1357
|
-
console.error(`[INFO] Shared ${sharedItems.length} memories + ${entityCount} entities from ${source_namespace} to ${target_namespace}`);
|
|
1766
|
+
console.error(`[INFO] Shared ${memories.length} memories + ${skillsShared} skills + ${entityCount} entities from ${source_namespace} to ${target_namespace}`);
|
|
1358
1767
|
return {
|
|
1359
1768
|
success: true,
|
|
1360
1769
|
source_namespace,
|
|
1361
1770
|
target_namespace,
|
|
1362
|
-
memories_shared:
|
|
1771
|
+
memories_shared: memories.length,
|
|
1772
|
+
skills_shared: skillsShared,
|
|
1363
1773
|
entities_shared: entityCount,
|
|
1364
1774
|
handoff_id: handoffId,
|
|
1365
1775
|
};
|
|
1366
1776
|
}
|
|
1367
1777
|
catch (err) {
|
|
1778
|
+
if (err instanceof McpError)
|
|
1779
|
+
throw err;
|
|
1368
1780
|
throw new McpError(ErrorCode.InternalError, `Failed to share context: ${err instanceof Error ? err.message : String(err)}`);
|
|
1369
1781
|
}
|
|
1370
1782
|
}
|
|
1371
1783
|
/**
|
|
1372
1784
|
* Handle store_decision tool call
|
|
1373
1785
|
*
|
|
1374
|
-
* Stores a decision with reduced decay and
|
|
1786
|
+
* Stores a versioned decision with reduced decay and explicit supersession.
|
|
1375
1787
|
*/
|
|
1376
1788
|
async function handleStoreDecision(input) {
|
|
1377
1789
|
if (!connections.sqlite) {
|
|
@@ -1379,64 +1791,130 @@ async function handleStoreDecision(input) {
|
|
|
1379
1791
|
}
|
|
1380
1792
|
try {
|
|
1381
1793
|
const validatedInput = validateInput(StoreDecisionInputSchema, input);
|
|
1382
|
-
const namespace = validatedInput.namespace ||
|
|
1794
|
+
const namespace = validatedInput.namespace || "default";
|
|
1383
1795
|
const id = randomUUID();
|
|
1384
1796
|
const now = Date.now();
|
|
1385
|
-
|
|
1797
|
+
const validFrom = toEpochMillis(validatedInput.valid_from) ?? now;
|
|
1798
|
+
const validUntil = toEpochMillis(validatedInput.valid_until);
|
|
1799
|
+
if (validUntil !== undefined && validUntil <= validFrom) {
|
|
1800
|
+
throw new McpError(ErrorCode.InvalidParams, "valid_until must be after valid_from");
|
|
1801
|
+
}
|
|
1386
1802
|
const metadata = {
|
|
1387
1803
|
...(validatedInput.metadata || {}),
|
|
1388
1804
|
decay_factor: 0.25,
|
|
1389
1805
|
supersedes: validatedInput.supersedes || null,
|
|
1390
1806
|
};
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1807
|
+
const superseded = validatedInput.supersedes
|
|
1808
|
+
? connections.sqlite
|
|
1809
|
+
.prepare(`
|
|
1810
|
+
SELECT id, content, memory_type, created_at, metadata, agent_id, task_id,
|
|
1811
|
+
valid_from, valid_until
|
|
1812
|
+
FROM memories
|
|
1813
|
+
WHERE id = ? AND namespace = ? AND memory_type = 'decision'
|
|
1814
|
+
`)
|
|
1815
|
+
.get(validatedInput.supersedes, namespace)
|
|
1816
|
+
: undefined;
|
|
1817
|
+
if (validatedInput.supersedes && !superseded) {
|
|
1818
|
+
throw new McpError(ErrorCode.InvalidParams, `Superseded decision not found in namespace '${namespace}'`);
|
|
1819
|
+
}
|
|
1820
|
+
if (superseded) {
|
|
1821
|
+
const supersededMetadata = safeJsonObject(superseded.metadata);
|
|
1822
|
+
if (typeof supersededMetadata.superseded_by === "string") {
|
|
1823
|
+
throw new McpError(ErrorCode.InvalidParams, "Decision has already been superseded");
|
|
1824
|
+
}
|
|
1825
|
+
if (superseded.valid_from !== null && validFrom < superseded.valid_from) {
|
|
1826
|
+
throw new McpError(ErrorCode.InvalidParams, "A replacement decision cannot become effective before the decision it supersedes");
|
|
1827
|
+
}
|
|
1828
|
+
if (superseded.valid_until !== null && validFrom > superseded.valid_until) {
|
|
1829
|
+
throw new McpError(ErrorCode.InvalidParams, "Replacement valid_from cannot be after the superseded decision's valid_until");
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
const commit = connections.sqlite.transaction(() => {
|
|
1833
|
+
connections.sqlite
|
|
1834
|
+
.prepare(`
|
|
1835
|
+
INSERT INTO memories (
|
|
1836
|
+
id, content, memory_type, created_at, updated_at, metadata, namespace,
|
|
1837
|
+
agent_id, agent_role, task_id, importance, access_count, last_accessed,
|
|
1838
|
+
valid_from, valid_until, expired_at, archived_at, consolidated_at
|
|
1839
|
+
) VALUES (?, ?, 'decision', ?, ?, ?, ?, ?, ?, ?, 0.9, 0, NULL, ?, ?, NULL, NULL, NULL)
|
|
1840
|
+
`)
|
|
1841
|
+
.run(id, validatedInput.content, now, now, JSON.stringify(metadata), namespace, validatedInput.agent_id || null, validatedInput.agent_role || null, validatedInput.task_id || null, validFrom, validUntil ?? null);
|
|
1842
|
+
connections.indexOutbox?.enqueue({
|
|
1843
|
+
memoryId: id,
|
|
1844
|
+
operation: "upsert",
|
|
1845
|
+
namespace,
|
|
1846
|
+
content: validatedInput.content,
|
|
1847
|
+
memoryType: "decision",
|
|
1848
|
+
metadata,
|
|
1849
|
+
agentId: validatedInput.agent_id,
|
|
1850
|
+
taskId: validatedInput.task_id,
|
|
1851
|
+
createdAt: now,
|
|
1852
|
+
});
|
|
1853
|
+
if (superseded) {
|
|
1854
|
+
const oldMetadata = {
|
|
1855
|
+
...safeJsonObject(superseded.metadata),
|
|
1856
|
+
superseded_by: id,
|
|
1857
|
+
superseded_at: validFrom,
|
|
1858
|
+
superseded_recorded_at: now,
|
|
1859
|
+
};
|
|
1860
|
+
const supersessionUpdate = connections.sqlite
|
|
1861
|
+
.prepare(`
|
|
1862
|
+
UPDATE memories
|
|
1863
|
+
SET metadata = ?, updated_at = ?, valid_until = ?
|
|
1864
|
+
WHERE id = ? AND namespace = ?
|
|
1865
|
+
AND metadata IS ? AND valid_until IS ?
|
|
1866
|
+
`)
|
|
1867
|
+
.run(JSON.stringify(oldMetadata), now, validFrom, superseded.id, namespace, superseded.metadata, superseded.valid_until);
|
|
1868
|
+
if (supersessionUpdate.changes !== 1) {
|
|
1869
|
+
throw new McpError(ErrorCode.InvalidParams, "Superseded decision changed concurrently; retry with the current decision");
|
|
1870
|
+
}
|
|
1871
|
+
connections.indexOutbox?.enqueue({
|
|
1872
|
+
memoryId: superseded.id,
|
|
1873
|
+
operation: "upsert",
|
|
1405
1874
|
namespace,
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1875
|
+
content: superseded.content,
|
|
1876
|
+
memoryType: superseded.memory_type,
|
|
1877
|
+
metadata: oldMetadata,
|
|
1878
|
+
agentId: superseded.agent_id ?? undefined,
|
|
1879
|
+
taskId: superseded.task_id ?? undefined,
|
|
1880
|
+
createdAt: superseded.created_at,
|
|
1409
1881
|
});
|
|
1410
|
-
hasEmbedding = true;
|
|
1411
1882
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1883
|
+
});
|
|
1884
|
+
commit();
|
|
1885
|
+
if (connections.indexOutbox) {
|
|
1886
|
+
try {
|
|
1887
|
+
await connections.indexOutbox.drain(20);
|
|
1414
1888
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (validatedInput.supersedes) {
|
|
1418
|
-
const oldMem = connections.sqlite.prepare("SELECT metadata FROM memories WHERE id = ? AND namespace = ?").get(validatedInput.supersedes, namespace);
|
|
1419
|
-
if (oldMem) {
|
|
1420
|
-
const oldMeta = JSON.parse(oldMem.metadata || '{}');
|
|
1421
|
-
oldMeta.superseded_by = id;
|
|
1422
|
-
connections.sqlite.prepare("UPDATE memories SET metadata = ?, updated_at = ? WHERE id = ?").run(JSON.stringify(oldMeta), now, validatedInput.supersedes);
|
|
1889
|
+
catch (error) {
|
|
1890
|
+
console.error("[WARN] Decision index drain failed; mutation remains durable:", error instanceof Error ? error.message : String(error));
|
|
1423
1891
|
}
|
|
1424
1892
|
}
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1893
|
+
const indexState = connections.indexOutbox?.getState(id) ?? "pending";
|
|
1894
|
+
// This is intentionally a count of candidates, not a claim that semantic
|
|
1895
|
+
// conflicts were detected. Conflict classification requires a verifier.
|
|
1896
|
+
const existingDecisions = connections.sqlite
|
|
1897
|
+
.prepare(`SELECT id FROM memories
|
|
1898
|
+
WHERE namespace = ? AND memory_type = 'decision' AND id != ?
|
|
1899
|
+
AND valid_until IS NULL
|
|
1900
|
+
ORDER BY created_at DESC LIMIT 10`)
|
|
1901
|
+
.all(namespace, id);
|
|
1902
|
+
console.error(`[INFO] Decision stored: ${id} (ns: ${namespace}, supersedes: ${validatedInput.supersedes || "none"})`);
|
|
1428
1903
|
return {
|
|
1429
1904
|
success: true,
|
|
1430
1905
|
decision_id: id,
|
|
1431
1906
|
namespace,
|
|
1432
1907
|
timestamp: now,
|
|
1433
|
-
has_embedding:
|
|
1908
|
+
has_embedding: indexState === "indexed" && !!connections.hybridSearch,
|
|
1909
|
+
index_state: indexState,
|
|
1434
1910
|
supersedes: validatedInput.supersedes || null,
|
|
1435
|
-
|
|
1911
|
+
potential_conflict_candidates: existingDecisions.length,
|
|
1436
1912
|
message: `Decision stored successfully`,
|
|
1437
1913
|
};
|
|
1438
1914
|
}
|
|
1439
1915
|
catch (err) {
|
|
1916
|
+
if (err instanceof McpError)
|
|
1917
|
+
throw err;
|
|
1440
1918
|
throw new McpError(ErrorCode.InternalError, `Failed to store decision: ${err instanceof Error ? err.message : String(err)}`);
|
|
1441
1919
|
}
|
|
1442
1920
|
}
|
|
@@ -1448,7 +1926,8 @@ async function handleListNamespaces() {
|
|
|
1448
1926
|
throw new McpError(ErrorCode.InternalError, "SQLite database not available");
|
|
1449
1927
|
}
|
|
1450
1928
|
try {
|
|
1451
|
-
const namespaces = connections.sqlite
|
|
1929
|
+
const namespaces = connections.sqlite
|
|
1930
|
+
.prepare(`
|
|
1452
1931
|
SELECT namespace, COUNT(*) as memory_count,
|
|
1453
1932
|
COUNT(DISTINCT agent_id) as agent_count,
|
|
1454
1933
|
MAX(created_at) as last_activity
|
|
@@ -1456,10 +1935,11 @@ async function handleListNamespaces() {
|
|
|
1456
1935
|
WHERE namespace IS NOT NULL
|
|
1457
1936
|
GROUP BY namespace
|
|
1458
1937
|
ORDER BY last_activity DESC
|
|
1459
|
-
`)
|
|
1938
|
+
`)
|
|
1939
|
+
.all();
|
|
1460
1940
|
return {
|
|
1461
1941
|
success: true,
|
|
1462
|
-
namespaces: namespaces.map(ns => ({
|
|
1942
|
+
namespaces: namespaces.map((ns) => ({
|
|
1463
1943
|
namespace: ns.namespace,
|
|
1464
1944
|
memory_count: ns.memory_count,
|
|
1465
1945
|
agent_count: ns.agent_count,
|
|
@@ -1482,21 +1962,33 @@ async function handleNamespaceStats(input) {
|
|
|
1482
1962
|
try {
|
|
1483
1963
|
const validatedInput = validateInput(NamespaceStatsInputSchema, input);
|
|
1484
1964
|
const namespace = validatedInput.namespace;
|
|
1485
|
-
const memoryCount = connections.sqlite
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
const
|
|
1489
|
-
|
|
1490
|
-
|
|
1965
|
+
const memoryCount = connections.sqlite
|
|
1966
|
+
.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ?")
|
|
1967
|
+
.get(namespace).count;
|
|
1968
|
+
const typeBreakdown = connections.sqlite
|
|
1969
|
+
.prepare("SELECT memory_type, COUNT(*) as count FROM memories WHERE namespace = ? GROUP BY memory_type")
|
|
1970
|
+
.all(namespace);
|
|
1971
|
+
const entityCount = connections.sqlite
|
|
1972
|
+
.prepare("SELECT COUNT(*) as count FROM entities WHERE namespace = ?")
|
|
1973
|
+
.get(namespace).count;
|
|
1974
|
+
const skillCount = connections.sqlite
|
|
1975
|
+
.prepare("SELECT COUNT(*) as count FROM skills WHERE namespace = ?")
|
|
1976
|
+
.get(namespace).count;
|
|
1977
|
+
const agents = connections.sqlite
|
|
1978
|
+
.prepare("SELECT DISTINCT agent_id, agent_role FROM memories WHERE namespace = ? AND agent_id IS NOT NULL")
|
|
1979
|
+
.all(namespace);
|
|
1980
|
+
const decisionCount = connections.sqlite
|
|
1981
|
+
.prepare("SELECT COUNT(*) as count FROM memories WHERE namespace = ? AND memory_type = 'decision'")
|
|
1982
|
+
.get(namespace).count;
|
|
1491
1983
|
return {
|
|
1492
1984
|
success: true,
|
|
1493
1985
|
namespace,
|
|
1494
1986
|
memories: memoryCount,
|
|
1495
|
-
memory_type_breakdown: Object.fromEntries(typeBreakdown.map(t => [t.memory_type, t.count])),
|
|
1987
|
+
memory_type_breakdown: Object.fromEntries(typeBreakdown.map((t) => [t.memory_type, t.count])),
|
|
1496
1988
|
entities: entityCount,
|
|
1497
1989
|
skills: skillCount,
|
|
1498
1990
|
decisions: decisionCount,
|
|
1499
|
-
agents: agents.map(a => ({ agent_id: a.agent_id, agent_role: a.agent_role })),
|
|
1991
|
+
agents: agents.map((a) => ({ agent_id: a.agent_id, agent_role: a.agent_role })),
|
|
1500
1992
|
agent_count: agents.length,
|
|
1501
1993
|
};
|
|
1502
1994
|
}
|
|
@@ -1507,7 +1999,7 @@ async function handleNamespaceStats(input) {
|
|
|
1507
1999
|
/**
|
|
1508
2000
|
* Process tool calls
|
|
1509
2001
|
*/
|
|
1510
|
-
async function processTool(name, input) {
|
|
2002
|
+
export async function processTool(name, input) {
|
|
1511
2003
|
console.error(`[INFO] Processing tool: ${name}`);
|
|
1512
2004
|
// Handle toggle tools first (these always work regardless of enabled state)
|
|
1513
2005
|
switch (name) {
|
|
@@ -1520,7 +2012,7 @@ async function processTool(name, input) {
|
|
|
1520
2012
|
}
|
|
1521
2013
|
// Check if Vesper is disabled (pass-through mode)
|
|
1522
2014
|
if (!vesperEnabled) {
|
|
1523
|
-
console.error(
|
|
2015
|
+
console.error("[INFO] Vesper is disabled (pass-through mode)");
|
|
1524
2016
|
return {
|
|
1525
2017
|
success: true,
|
|
1526
2018
|
disabled: true,
|
|
@@ -1565,7 +2057,7 @@ async function main() {
|
|
|
1565
2057
|
// Create MCP server
|
|
1566
2058
|
const server = new Server({
|
|
1567
2059
|
name: "vesper",
|
|
1568
|
-
version: "0.5.
|
|
2060
|
+
version: "0.5.4",
|
|
1569
2061
|
}, {
|
|
1570
2062
|
capabilities: {
|
|
1571
2063
|
tools: {},
|
|
@@ -1593,11 +2085,7 @@ async function main() {
|
|
|
1593
2085
|
};
|
|
1594
2086
|
}
|
|
1595
2087
|
catch (err) {
|
|
1596
|
-
const errorMessage = err instanceof McpError
|
|
1597
|
-
? err.message
|
|
1598
|
-
: err instanceof Error
|
|
1599
|
-
? err.message
|
|
1600
|
-
: String(err);
|
|
2088
|
+
const errorMessage = err instanceof McpError ? err.message : err instanceof Error ? err.message : String(err);
|
|
1601
2089
|
console.error(`[ERROR] Tool error for ${name}: ${errorMessage}`);
|
|
1602
2090
|
return {
|
|
1603
2091
|
content: [
|
|
@@ -1623,21 +2111,37 @@ async function main() {
|
|
|
1623
2111
|
}
|
|
1624
2112
|
// Cleanup handler for graceful shutdown
|
|
1625
2113
|
function cleanup() {
|
|
1626
|
-
console.error(
|
|
2114
|
+
console.error("[INFO] Shutting down Vesper...");
|
|
2115
|
+
connections.consolidationScheduler?.stop();
|
|
2116
|
+
if (connections.indexOutboxTimer)
|
|
2117
|
+
clearInterval(connections.indexOutboxTimer);
|
|
1627
2118
|
if (connections.redis) {
|
|
1628
2119
|
connections.redis.disconnect();
|
|
1629
2120
|
}
|
|
1630
2121
|
if (connections.sqlite) {
|
|
1631
2122
|
connections.sqlite.close();
|
|
1632
2123
|
}
|
|
1633
|
-
console.error(
|
|
2124
|
+
console.error("[INFO] Vesper shutdown complete");
|
|
1634
2125
|
process.exit(0);
|
|
1635
2126
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
2127
|
+
export function closeConnectionsForTesting() {
|
|
2128
|
+
connections.consolidationScheduler?.stop();
|
|
2129
|
+
if (connections.indexOutboxTimer)
|
|
2130
|
+
clearInterval(connections.indexOutboxTimer);
|
|
2131
|
+
connections.redis?.disconnect();
|
|
2132
|
+
if (connections.sqlite?.open)
|
|
2133
|
+
connections.sqlite.close();
|
|
2134
|
+
for (const key of Object.keys(connections)) {
|
|
2135
|
+
delete connections[key];
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
process.on("SIGINT", cleanup);
|
|
2139
|
+
process.on("SIGTERM", cleanup);
|
|
2140
|
+
// Run the server unless a test imports the production handlers directly.
|
|
2141
|
+
if (process.env.VESPER_SKIP_AUTO_START !== "1") {
|
|
2142
|
+
main().catch((err) => {
|
|
2143
|
+
console.error("[FATAL] Server error:", err instanceof Error ? err.message : String(err));
|
|
2144
|
+
process.exit(1);
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
1643
2147
|
//# sourceMappingURL=server.js.map
|