prism-mcp-server 2.3.12 → 2.5.1
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/README.md +274 -18
- package/dist/config.js +20 -3
- package/dist/dashboard/server.js +2 -2
- package/dist/dashboard/ui.js +2 -2
- package/dist/server.js +13 -2
- package/dist/storage/sqlite.js +70 -3
- package/dist/storage/supabase.js +35 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +88 -0
- package/dist/tools/sessionMemoryHandlers.js +285 -42
- package/dist/utils/embeddingApi.js +11 -4
- package/dist/utils/tracing.js +139 -0
- package/package.json +1 -1
|
@@ -20,9 +20,17 @@ import { getStorage } from "../storage/index.js";
|
|
|
20
20
|
import { toKeywordArray } from "../utils/keywordExtractor.js";
|
|
21
21
|
import { generateEmbedding } from "../utils/embeddingApi.js";
|
|
22
22
|
import { getCurrentGitState, getGitDrift } from "../utils/git.js";
|
|
23
|
+
// ─── Phase 1: Explainability & Memory Lineage ────────────────
|
|
24
|
+
// These utilities provide structured tracing metadata for search operations.
|
|
25
|
+
// When `enable_trace: true` is passed to session_search_memory or knowledge_search,
|
|
26
|
+
// a separate MCP content block (content[1]) is returned with a MemoryTrace object
|
|
27
|
+
// containing: strategy, scores, latency breakdown (embedding/storage/total), and metadata.
|
|
28
|
+
// See src/utils/tracing.ts for full type definitions and design decisions.
|
|
29
|
+
import { createMemoryTrace, traceToContentBlock } from "../utils/tracing.js";
|
|
23
30
|
import { GOOGLE_API_KEY, PRISM_USER_ID, PRISM_AUTO_CAPTURE, PRISM_CAPTURE_PORTS } from "../config.js";
|
|
24
31
|
import { captureLocalEnvironment } from "../utils/autoCapture.js";
|
|
25
32
|
import { isSessionSaveLedgerArgs, isSessionSaveHandoffArgs, isSessionLoadContextArgs, isKnowledgeSearchArgs, isKnowledgeForgetArgs, isSessionSearchMemoryArgs, isBackfillEmbeddingsArgs, isMemoryHistoryArgs, isMemoryCheckoutArgs, isSessionHealthCheckArgs, // v2.2.0: health check type guard
|
|
33
|
+
isSessionForgetMemoryArgs, // Phase 2: GDPR-compliant memory deletion type guard
|
|
26
34
|
} from "./sessionMemoryDefinitions.js";
|
|
27
35
|
import { notifyResourceUpdate } from "../server.js";
|
|
28
36
|
// ─── Save Ledger Handler ──────────────────────────────────────
|
|
@@ -471,15 +479,43 @@ export async function sessionLoadContextHandler(args) {
|
|
|
471
479
|
// ─── Knowledge Search Handler ─────────────────────────────────
|
|
472
480
|
/**
|
|
473
481
|
* Searches accumulated knowledge across all past sessions.
|
|
482
|
+
*
|
|
483
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
484
|
+
* PHASE 1 CHANGES (Explainability & Memory Lineage):
|
|
485
|
+
*
|
|
486
|
+
* Added `enable_trace` optional parameter (default: false).
|
|
487
|
+
* When enabled, appends a MemoryTrace content block to the response
|
|
488
|
+
* with strategy="keyword", timing data, and result metadata.
|
|
489
|
+
*
|
|
490
|
+
* TIMING INSTRUMENTATION:
|
|
491
|
+
* - totalStart: captured before any work begins
|
|
492
|
+
* - storageStart/storageMs: isolates database query time
|
|
493
|
+
* - embeddingMs: always 0 for keyword search (no embedding needed)
|
|
494
|
+
* - totalMs: end-to-end including keyword extraction overhead
|
|
495
|
+
*
|
|
496
|
+
* BACKWARD COMPATIBILITY:
|
|
497
|
+
* When enable_trace is false (default), the response is identical
|
|
498
|
+
* to the pre-Phase 1 implementation. Zero breaking changes.
|
|
499
|
+
*
|
|
500
|
+
* MCP OUTPUT ARRAY:
|
|
501
|
+
* content[0] = human-readable search results (unchanged)
|
|
502
|
+
* content[1] = machine-readable MemoryTrace JSON (only when enable_trace=true)
|
|
503
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
474
504
|
*/
|
|
475
505
|
export async function knowledgeSearchHandler(args) {
|
|
476
506
|
if (!isKnowledgeSearchArgs(args)) {
|
|
477
507
|
throw new Error("Invalid arguments for knowledge_search");
|
|
478
508
|
}
|
|
479
|
-
|
|
509
|
+
// Phase 1: destructure enable_trace (defaults to false for backward compat)
|
|
510
|
+
const { project, query, category, limit = 10, enable_trace = false } = args;
|
|
480
511
|
debugLog(`[knowledge_search] Searching: project=${project || "all"}, query="${query || ""}", category=${category || "any"}, limit=${limit}`);
|
|
512
|
+
// Phase 1: Capture total start time for latency measurement
|
|
513
|
+
const totalStart = performance.now();
|
|
481
514
|
const searchKeywords = query ? toKeywordArray(query) : [];
|
|
482
515
|
const storage = await getStorage();
|
|
516
|
+
// Phase 1: Capture storage-specific start time to isolate DB latency
|
|
517
|
+
// from keyword extraction and other overhead
|
|
518
|
+
const storageStart = performance.now();
|
|
483
519
|
const data = await storage.searchKnowledge({
|
|
484
520
|
project: project || null,
|
|
485
521
|
keywords: searchKeywords,
|
|
@@ -488,27 +524,60 @@ export async function knowledgeSearchHandler(args) {
|
|
|
488
524
|
limit: Math.min(limit, 50),
|
|
489
525
|
userId: PRISM_USER_ID,
|
|
490
526
|
});
|
|
527
|
+
const storageMs = performance.now() - storageStart;
|
|
528
|
+
const totalMs = performance.now() - totalStart;
|
|
491
529
|
if (!data) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
text: `🔍 No knowledge found matching your search.\n` +
|
|
496
|
-
(query ? `Query: "${query}"\n` : "") +
|
|
497
|
-
(category ? `Category: ${category}\n` : "") +
|
|
498
|
-
(project ? `Project: ${project}\n` : "") +
|
|
499
|
-
`\nTip: Try session_search_memory for semantic (meaning-based) search ` +
|
|
500
|
-
`if keyword search doesn't find what you need.`,
|
|
501
|
-
}],
|
|
502
|
-
isError: false,
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
return {
|
|
506
|
-
content: [{
|
|
530
|
+
// Phase 1: Use contentBlocks array instead of inline object
|
|
531
|
+
// so we can conditionally push the trace block at content[1]
|
|
532
|
+
const contentBlocks = [{
|
|
507
533
|
type: "text",
|
|
508
|
-
text:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
534
|
+
text: `🔍 No knowledge found matching your search.\n` +
|
|
535
|
+
(query ? `Query: "${query}"\n` : "") +
|
|
536
|
+
(category ? `Category: ${category}\n` : "") +
|
|
537
|
+
(project ? `Project: ${project}\n` : "") +
|
|
538
|
+
`\nTip: Try session_search_memory for semantic (meaning-based) search ` +
|
|
539
|
+
`if keyword search doesn't find what you need.`,
|
|
540
|
+
}];
|
|
541
|
+
// Phase 1: Append trace block even on empty results — this tells
|
|
542
|
+
// the developer the search DID execute, it just found nothing.
|
|
543
|
+
// topScore and threshold are null for keyword search (no scoring system).
|
|
544
|
+
if (enable_trace) {
|
|
545
|
+
const trace = createMemoryTrace({
|
|
546
|
+
strategy: "keyword",
|
|
547
|
+
query: query || "",
|
|
548
|
+
resultCount: 0,
|
|
549
|
+
topScore: null, // keyword search doesn't produce similarity scores
|
|
550
|
+
threshold: null, // keyword search has no threshold concept
|
|
551
|
+
embeddingMs: 0, // no embedding needed for keyword search
|
|
552
|
+
storageMs,
|
|
553
|
+
totalMs,
|
|
554
|
+
project: project || null,
|
|
555
|
+
});
|
|
556
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
557
|
+
}
|
|
558
|
+
return { content: contentBlocks, isError: false };
|
|
559
|
+
}
|
|
560
|
+
// Phase 1: Wrap in contentBlocks array for optional trace attachment
|
|
561
|
+
const contentBlocks = [{
|
|
562
|
+
type: "text",
|
|
563
|
+
text: `🧠 Found ${data.count} knowledge entries:\n\n${JSON.stringify(data, null, 2)}`,
|
|
564
|
+
}];
|
|
565
|
+
// Phase 1: Attach MemoryTrace with strategy="keyword" and timing data
|
|
566
|
+
if (enable_trace) {
|
|
567
|
+
const trace = createMemoryTrace({
|
|
568
|
+
strategy: "keyword",
|
|
569
|
+
query: query || "",
|
|
570
|
+
resultCount: data.count,
|
|
571
|
+
topScore: null, // keyword search doesn't produce similarity scores
|
|
572
|
+
threshold: null, // keyword search has no threshold concept
|
|
573
|
+
embeddingMs: 0, // no embedding needed for keyword search
|
|
574
|
+
storageMs,
|
|
575
|
+
totalMs,
|
|
576
|
+
project: project || null,
|
|
577
|
+
});
|
|
578
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
579
|
+
}
|
|
580
|
+
return { content: contentBlocks, isError: false };
|
|
512
581
|
}
|
|
513
582
|
// ─── Knowledge Forget Handler ─────────────────────────────────
|
|
514
583
|
/**
|
|
@@ -581,15 +650,55 @@ export async function knowledgeForgetHandler(args) {
|
|
|
581
650
|
}
|
|
582
651
|
// ─── Semantic Search Handler ──────────────────────────────────
|
|
583
652
|
/**
|
|
584
|
-
* Searches session history semantically using embeddings.
|
|
653
|
+
* Searches session history semantically using vector embeddings.
|
|
654
|
+
*
|
|
655
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
656
|
+
* PHASE 1 CHANGES (Explainability & Memory Lineage):
|
|
657
|
+
*
|
|
658
|
+
* Added `enable_trace` optional parameter (default: false).
|
|
659
|
+
* When enabled, appends a MemoryTrace content block to the response.
|
|
660
|
+
*
|
|
661
|
+
* TIMING INSTRUMENTATION (3 checkpoints):
|
|
662
|
+
* 1. totalStart: before any work begins
|
|
663
|
+
* 2. embeddingStart/embeddingMs: isolates Gemini API call latency
|
|
664
|
+
* (this is the most variable — 50ms to 2000ms depending on load)
|
|
665
|
+
* 3. storageStart/storageMs: isolates pgvector/SQLite query time
|
|
666
|
+
*
|
|
667
|
+
* WHY SEPARATE EMBEDDING FROM STORAGE:
|
|
668
|
+
* A single latency_ms number is misleading. Example:
|
|
669
|
+
* - 500ms total could be 480ms Gemini API + 20ms pgvector
|
|
670
|
+
* → Fix: cache embeddings or switch to a faster model
|
|
671
|
+
* - 500ms total could be 20ms Gemini API + 480ms pgvector
|
|
672
|
+
* → Fix: add an index or reduce vector dimensions
|
|
673
|
+
*
|
|
674
|
+
* SCORE BUBBLING:
|
|
675
|
+
* The `topScore` in the trace comes from results[0].similarity,
|
|
676
|
+
* which is the cosine distance returned by SemanticSearchResult
|
|
677
|
+
* (see src/storage/interface.ts L104-112). No storage layer
|
|
678
|
+
* modifications were needed — the score was already there.
|
|
679
|
+
*
|
|
680
|
+
* MCP OUTPUT ARRAY:
|
|
681
|
+
* content[0] = human-readable search results (unchanged)
|
|
682
|
+
* content[1] = machine-readable MemoryTrace JSON (only when enable_trace=true)
|
|
683
|
+
*
|
|
684
|
+
* BACKWARD COMPATIBILITY:
|
|
685
|
+
* When enable_trace is false (default), the response is byte-for-byte
|
|
686
|
+
* identical to the pre-Phase 1 implementation. Zero breaking changes.
|
|
687
|
+
* Existing tests pass without modification.
|
|
688
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
585
689
|
*/
|
|
586
690
|
export async function sessionSearchMemoryHandler(args) {
|
|
587
691
|
if (!isSessionSearchMemoryArgs(args)) {
|
|
588
692
|
throw new Error("Invalid arguments for session_search_memory");
|
|
589
693
|
}
|
|
590
|
-
const { query, project, limit = 5, similarity_threshold = 0.7,
|
|
694
|
+
const { query, project, limit = 5, similarity_threshold = 0.7,
|
|
695
|
+
// Phase 1: enable_trace defaults to false for full backward compatibility.
|
|
696
|
+
// When true, a MemoryTrace JSON block is appended as content[1].
|
|
697
|
+
enable_trace = false, } = args;
|
|
591
698
|
debugLog(`[session_search_memory] Semantic search: query="${query}", ` +
|
|
592
699
|
`project=${project || "all"}, limit=${limit}, threshold=${similarity_threshold}`);
|
|
700
|
+
// Phase 1: Start total latency timer BEFORE any work (embedding + storage)
|
|
701
|
+
const totalStart = performance.now();
|
|
593
702
|
// Step 1: Generate embedding for the search query
|
|
594
703
|
if (!GOOGLE_API_KEY) {
|
|
595
704
|
return {
|
|
@@ -603,6 +712,9 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
603
712
|
};
|
|
604
713
|
}
|
|
605
714
|
let queryEmbedding;
|
|
715
|
+
// Phase 1: Start embedding latency timer — isolates Gemini API call time.
|
|
716
|
+
// This is the most variable component: 50ms on a good day, 2000ms under load.
|
|
717
|
+
const embeddingStart = performance.now();
|
|
606
718
|
try {
|
|
607
719
|
queryEmbedding = await generateEmbedding(query);
|
|
608
720
|
}
|
|
@@ -616,9 +728,15 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
616
728
|
isError: true,
|
|
617
729
|
};
|
|
618
730
|
}
|
|
731
|
+
// Phase 1: Capture embedding API latency
|
|
732
|
+
const embeddingMs = performance.now() - embeddingStart;
|
|
619
733
|
// Step 2: Search via storage backend
|
|
620
734
|
try {
|
|
621
735
|
const storage = await getStorage();
|
|
736
|
+
// Phase 1: Start storage latency timer — isolates DB query time.
|
|
737
|
+
// For Supabase: this measures the pgvector cosine distance RPC call.
|
|
738
|
+
// For SQLite: this measures the local sqlite-vec similarity search.
|
|
739
|
+
const storageStart = performance.now();
|
|
622
740
|
const results = await storage.searchMemory({
|
|
623
741
|
queryEmbedding: JSON.stringify(queryEmbedding),
|
|
624
742
|
project: project || null,
|
|
@@ -626,20 +744,38 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
626
744
|
similarityThreshold: similarity_threshold,
|
|
627
745
|
userId: PRISM_USER_ID,
|
|
628
746
|
});
|
|
747
|
+
// Phase 1: Capture storage query latency and compute total
|
|
748
|
+
const storageMs = performance.now() - storageStart;
|
|
749
|
+
const totalMs = performance.now() - totalStart;
|
|
629
750
|
if (results.length === 0) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
751
|
+
// Phase 1: Use contentBlocks array so we can optionally push trace at [1]
|
|
752
|
+
const contentBlocks = [{
|
|
753
|
+
type: "text",
|
|
754
|
+
text: `🔍 No semantically similar sessions found for: "${query}"\n` +
|
|
755
|
+
(project ? `Project: ${project}\n` : "") +
|
|
756
|
+
`Similarity threshold: ${similarity_threshold}\n\n` +
|
|
757
|
+
`Tips:\n` +
|
|
758
|
+
`• Lower the similarity_threshold (e.g., 0.5) for broader results\n` +
|
|
759
|
+
`• Try knowledge_search for keyword-based matching\n` +
|
|
760
|
+
`• Ensure sessions have been saved with embeddings (requires GOOGLE_API_KEY)`,
|
|
761
|
+
}];
|
|
762
|
+
// Phase 1: Trace is still valuable on empty results — it proves the search
|
|
763
|
+
// executed and reveals whether the bottleneck was embedding or storage.
|
|
764
|
+
if (enable_trace) {
|
|
765
|
+
const trace = createMemoryTrace({
|
|
766
|
+
strategy: "semantic",
|
|
767
|
+
query,
|
|
768
|
+
resultCount: 0,
|
|
769
|
+
topScore: null, // no results = no top score
|
|
770
|
+
threshold: similarity_threshold,
|
|
771
|
+
embeddingMs,
|
|
772
|
+
storageMs,
|
|
773
|
+
totalMs,
|
|
774
|
+
project: project || null,
|
|
775
|
+
});
|
|
776
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
777
|
+
}
|
|
778
|
+
return { content: contentBlocks, isError: false };
|
|
643
779
|
}
|
|
644
780
|
// Format results with similarity scores
|
|
645
781
|
const formatted = results.map((r, i) => {
|
|
@@ -652,13 +788,33 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
652
788
|
(r.decisions?.length ? ` Decisions: ${r.decisions.join("; ")}\n` : "") +
|
|
653
789
|
(r.files_changed?.length ? ` Files: ${r.files_changed.join(", ")}\n` : "");
|
|
654
790
|
}).join("\n");
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
791
|
+
// Phase 1: content[0] = human-readable results (unchanged from pre-Phase 1)
|
|
792
|
+
const contentBlocks = [{
|
|
793
|
+
type: "text",
|
|
794
|
+
text: `🧠 Found ${results.length} semantically similar sessions:\n\n${formatted}`,
|
|
795
|
+
}];
|
|
796
|
+
// Phase 1: content[1] = machine-readable MemoryTrace (only when enable_trace=true)
|
|
797
|
+
// topScore is read from results[0].similarity — this is the cosine distance
|
|
798
|
+
// already returned by SemanticSearchResult in the storage interface.
|
|
799
|
+
// No storage layer modifications were needed ("Score Bubbling" reviewer level-up).
|
|
800
|
+
if (enable_trace) {
|
|
801
|
+
const topScore = results.length > 0 && typeof results[0].similarity === "number"
|
|
802
|
+
? results[0].similarity
|
|
803
|
+
: null;
|
|
804
|
+
const trace = createMemoryTrace({
|
|
805
|
+
strategy: "semantic",
|
|
806
|
+
query,
|
|
807
|
+
resultCount: results.length,
|
|
808
|
+
topScore,
|
|
809
|
+
threshold: similarity_threshold,
|
|
810
|
+
embeddingMs,
|
|
811
|
+
storageMs,
|
|
812
|
+
totalMs,
|
|
813
|
+
project: project || null,
|
|
814
|
+
});
|
|
815
|
+
contentBlocks.push(traceToContentBlock(trace));
|
|
816
|
+
}
|
|
817
|
+
return { content: contentBlocks, isError: false };
|
|
662
818
|
}
|
|
663
819
|
catch (err) {
|
|
664
820
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -1194,3 +1350,90 @@ export async function sessionHealthCheckHandler(args) {
|
|
|
1194
1350
|
};
|
|
1195
1351
|
}
|
|
1196
1352
|
}
|
|
1353
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1354
|
+
// Phase 2: GDPR-Compliant Memory Deletion Handler
|
|
1355
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1356
|
+
//
|
|
1357
|
+
// This handler implements the session_forget_memory MCP tool.
|
|
1358
|
+
// It provides SURGICAL deletion of individual memory entries by ID,
|
|
1359
|
+
// supporting both soft-delete (tombstoning) and hard-delete (physical removal).
|
|
1360
|
+
//
|
|
1361
|
+
// WHY THIS IS SEPARATE FROM knowledgeForgetHandler:
|
|
1362
|
+
// knowledgeForgetHandler operates on BULK criteria (project, category, age).
|
|
1363
|
+
// sessionForgetMemoryHandler operates on a SINGLE entry by ID.
|
|
1364
|
+
// This surgical approach is required for GDPR Article 17 compliance,
|
|
1365
|
+
// where a data subject requests deletion of specific personal data.
|
|
1366
|
+
//
|
|
1367
|
+
// THE TOP-K HOLE PROBLEM (Solved):
|
|
1368
|
+
// Without deleted_at filtering inside the database queries (both SQL and RPCs),
|
|
1369
|
+
// a LIMIT 5 query might return 5 rows where 4 are soft-deleted. Post-filtering
|
|
1370
|
+
// in TypeScript would strip them, leaving only 1 result. This destroys the
|
|
1371
|
+
// agent's recall capability. By adding "AND deleted_at IS NULL" to ALL
|
|
1372
|
+
// search queries (done in sqlite.ts and Supabase RPCs), the filtering
|
|
1373
|
+
// happens BEFORE the LIMIT is applied, guaranteeing full Top-K results.
|
|
1374
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1375
|
+
export async function sessionForgetMemoryHandler(args) {
|
|
1376
|
+
try {
|
|
1377
|
+
// ─── Input Validation ───
|
|
1378
|
+
if (!isSessionForgetMemoryArgs(args)) {
|
|
1379
|
+
return {
|
|
1380
|
+
content: [{
|
|
1381
|
+
type: "text",
|
|
1382
|
+
text: "Invalid arguments. Required: memory_id (string). Optional: hard_delete (boolean), reason (string).",
|
|
1383
|
+
}],
|
|
1384
|
+
isError: true,
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
const { memory_id, hard_delete = false, reason } = args;
|
|
1388
|
+
// ─── Get Storage Backend ───
|
|
1389
|
+
const storage = await getStorage();
|
|
1390
|
+
// ─── Execute Deletion ───
|
|
1391
|
+
// The storage methods verify user_id ownership internally,
|
|
1392
|
+
// preventing cross-user deletion attacks.
|
|
1393
|
+
if (hard_delete) {
|
|
1394
|
+
// IRREVERSIBLE: Physical removal from the database.
|
|
1395
|
+
// FTS5 triggers (SQLite) or Supabase cascades clean up indexes.
|
|
1396
|
+
await storage.hardDeleteLedger(memory_id, PRISM_USER_ID);
|
|
1397
|
+
debugLog(`[session_forget_memory] Hard-deleted entry ${memory_id}`);
|
|
1398
|
+
return {
|
|
1399
|
+
content: [{
|
|
1400
|
+
type: "text",
|
|
1401
|
+
text: `🗑️ **Hard Deleted** memory entry \`${memory_id}\`.\n\n` +
|
|
1402
|
+
`This entry has been permanently removed from the database. ` +
|
|
1403
|
+
`It cannot be recovered. All associated embeddings and FTS indexes ` +
|
|
1404
|
+
`have been cleaned up.`,
|
|
1405
|
+
}],
|
|
1406
|
+
isError: false,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
// REVERSIBLE: Soft-delete (tombstone) — sets deleted_at + deleted_reason.
|
|
1411
|
+
// The entry remains in the database but is excluded from ALL search
|
|
1412
|
+
// queries (vector, FTS5, and context loading).
|
|
1413
|
+
await storage.softDeleteLedger(memory_id, PRISM_USER_ID, reason);
|
|
1414
|
+
debugLog(`[session_forget_memory] Soft-deleted entry ${memory_id} (reason: ${reason || "none"})`);
|
|
1415
|
+
return {
|
|
1416
|
+
content: [{
|
|
1417
|
+
type: "text",
|
|
1418
|
+
text: `🔇 **Soft Deleted** memory entry \`${memory_id}\`.\n\n` +
|
|
1419
|
+
`The entry has been tombstoned (deleted_at = NOW()). ` +
|
|
1420
|
+
`It will no longer appear in any search results, but remains ` +
|
|
1421
|
+
`in the database for audit trail purposes.\n\n` +
|
|
1422
|
+
(reason ? `📋 **Reason**: ${reason}\n\n` : "") +
|
|
1423
|
+
`To permanently remove this entry, call again with \`hard_delete: true\`.`,
|
|
1424
|
+
}],
|
|
1425
|
+
isError: false,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
console.error(`[session_forget_memory] Error: ${error}`);
|
|
1431
|
+
return {
|
|
1432
|
+
content: [{
|
|
1433
|
+
type: "text",
|
|
1434
|
+
text: `Error forgetting memory: ${error instanceof Error ? error.message : String(error)}`,
|
|
1435
|
+
}],
|
|
1436
|
+
isError: true,
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
@@ -85,13 +85,20 @@ export async function generateEmbedding(text) {
|
|
|
85
85
|
const model = genAI.getGenerativeModel({ model: "gemini-embedding-001" }, { apiVersion: "v1beta" } // gemini-embedding-001 requires v1beta
|
|
86
86
|
);
|
|
87
87
|
debugLog(`[embedding] Generating 768-dim embedding for ${inputText.length} chars`);
|
|
88
|
-
const
|
|
88
|
+
const request = {
|
|
89
89
|
content: {
|
|
90
90
|
role: "user",
|
|
91
91
|
parts: [{ text: inputText }],
|
|
92
92
|
},
|
|
93
93
|
taskType: TaskType.SEMANTIC_SIMILARITY,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// SDK runtime supports this for gemini-embedding-001 even when older
|
|
95
|
+
// type defs may lag; keep cast localized at request boundary.
|
|
96
|
+
...{ outputDimensionality: 768 },
|
|
97
|
+
};
|
|
98
|
+
const result = await model.embedContent(request);
|
|
99
|
+
const values = result.embedding.values;
|
|
100
|
+
if (!Array.isArray(values) || values.length !== 768) {
|
|
101
|
+
throw new Error(`Embedding dimension mismatch: expected 768, got ${values?.length ?? 'unknown'}`);
|
|
102
|
+
}
|
|
103
|
+
return values;
|
|
97
104
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Trace — Phase 1 Explainability & Lineage
|
|
3
|
+
*
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* PURPOSE:
|
|
6
|
+
* Provides structured tracing metadata for every search/recall
|
|
7
|
+
* operation in Prism MCP. When `enable_trace: true` is passed to
|
|
8
|
+
* `session_search_memory` or `knowledge_search`, the response
|
|
9
|
+
* includes a separate MCP content block with a MemoryTrace object.
|
|
10
|
+
*
|
|
11
|
+
* WHY THIS EXISTS:
|
|
12
|
+
* Without tracing, developers have no visibility into *why* a
|
|
13
|
+
* memory was returned — was it a semantic match? A keyword hit?
|
|
14
|
+
* How confident was the score? Was the 500ms latency caused by
|
|
15
|
+
* the embedding API or the database query?
|
|
16
|
+
*
|
|
17
|
+
* This module answers all of those questions by providing:
|
|
18
|
+
* - strategy: "semantic" | "keyword" → which search path was used
|
|
19
|
+
* - top_score: the cosine similarity / relevance score of the best result
|
|
20
|
+
* - latency: { embedding_ms, storage_ms, total_ms } → pinpoints bottlenecks
|
|
21
|
+
* - result_count, threshold, project, query, timestamp → full context
|
|
22
|
+
*
|
|
23
|
+
* DESIGN DECISIONS:
|
|
24
|
+
*
|
|
25
|
+
* 1. NO OPENTELEMETRY SDK IN PHASE 1
|
|
26
|
+
* We get the data structures right in-memory first. OTel
|
|
27
|
+
* integration (W3C traceparent headers, span export to
|
|
28
|
+
* Datadog/LangSmith) layers on top in a follow-up without
|
|
29
|
+
* any code changes to the MemoryTrace types.
|
|
30
|
+
*
|
|
31
|
+
* 2. SEPARATE MCP CONTENT BLOCK (The "Output Array Trick")
|
|
32
|
+
* Instead of concatenating trace JSON into the human-readable
|
|
33
|
+
* text response (content[0]), we return it as content[1].
|
|
34
|
+
*
|
|
35
|
+
* Why?
|
|
36
|
+
* - Prevents LLMs from accidentally blending trace JSON into
|
|
37
|
+
* their reasoning (they sometimes try to "interpret" inline JSON)
|
|
38
|
+
* - Programmatic MCP clients can grab content[1] directly
|
|
39
|
+
* without parsing/splitting string output
|
|
40
|
+
* - Clean separation of concerns: content[0] = human-readable,
|
|
41
|
+
* content[1] = machine-readable trace metadata
|
|
42
|
+
*
|
|
43
|
+
* 3. LATENCY BREAKDOWN (Not just total)
|
|
44
|
+
* A single `latency_ms` number is misleading. A 500ms total could
|
|
45
|
+
* be 480ms embedding API + 20ms DB, or 20ms embedding + 480ms DB.
|
|
46
|
+
* These are very different problems requiring different fixes.
|
|
47
|
+
*
|
|
48
|
+
* We capture three timestamps:
|
|
49
|
+
* - Before embedding API call → after = embedding_ms
|
|
50
|
+
* - Before storage.searchMemory() → after = storage_ms
|
|
51
|
+
* - Start to finish = total_ms (includes overhead, serialization, etc.)
|
|
52
|
+
*
|
|
53
|
+
* 4. SCORE BUBBLING (No storage layer changes needed)
|
|
54
|
+
* The existing SemanticSearchResult interface (interface.ts L104-112)
|
|
55
|
+
* already includes `similarity: number`. We read this directly from
|
|
56
|
+
* results[0].similarity — no modifications to the storage layer.
|
|
57
|
+
* For keyword search, top_score is null since keyword search doesn't
|
|
58
|
+
* return relevance scores in the current implementation.
|
|
59
|
+
*
|
|
60
|
+
* 5. BACKWARD COMPATIBILITY
|
|
61
|
+
* When `enable_trace` is not set (default: false), the response
|
|
62
|
+
* is identical to pre-Phase 1 output. Zero breaking changes.
|
|
63
|
+
* Existing tests pass without modification.
|
|
64
|
+
*
|
|
65
|
+
* USAGE:
|
|
66
|
+
* This module is imported by sessionMemoryHandlers.ts. It is NOT
|
|
67
|
+
* imported by the storage layer, server.ts, or any other module.
|
|
68
|
+
*
|
|
69
|
+
* FILES THAT IMPORT THIS:
|
|
70
|
+
* - src/tools/sessionMemoryHandlers.ts (search handlers)
|
|
71
|
+
*
|
|
72
|
+
* RELATED FILES:
|
|
73
|
+
* - src/tools/sessionMemoryDefinitions.ts (enable_trace param definition)
|
|
74
|
+
* - src/storage/interface.ts (SemanticSearchResult with similarity score)
|
|
75
|
+
*
|
|
76
|
+
* FUTURE EXTENSIONS (Phase 1.5+):
|
|
77
|
+
* - Add OpenTelemetry span creation using these same trace objects
|
|
78
|
+
* - Add `reranked_score` field when re-ranking is implemented
|
|
79
|
+
* - Add `graph_hops` field when graph-based recall is added
|
|
80
|
+
* - Add PII sanitization flags for GDPR-strict deployments
|
|
81
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
82
|
+
*/
|
|
83
|
+
// ─── Factory ──────────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Create a MemoryTrace object from search operation metrics.
|
|
86
|
+
*
|
|
87
|
+
* This is a pure factory function — no side effects, no I/O.
|
|
88
|
+
* Called by the search handlers after both the embedding API call
|
|
89
|
+
* and storage query have completed.
|
|
90
|
+
*
|
|
91
|
+
* Latency values are rounded to nearest integer for cleaner output
|
|
92
|
+
* (sub-millisecond precision is noise, not signal).
|
|
93
|
+
*
|
|
94
|
+
* @param params.strategy - "semantic" or "keyword"
|
|
95
|
+
* @param params.query - Original search query string
|
|
96
|
+
* @param params.resultCount - Number of results returned
|
|
97
|
+
* @param params.topScore - Best similarity score, or null for keyword
|
|
98
|
+
* @param params.threshold - Threshold used, or null for keyword
|
|
99
|
+
* @param params.embeddingMs - Time for embedding API call (0 for keyword)
|
|
100
|
+
* @param params.storageMs - Time for database query
|
|
101
|
+
* @param params.totalMs - Total end-to-end time
|
|
102
|
+
* @param params.project - Project filter, or null for all
|
|
103
|
+
* @returns A complete MemoryTrace object ready for serialization
|
|
104
|
+
*/
|
|
105
|
+
export function createMemoryTrace(params) {
|
|
106
|
+
return {
|
|
107
|
+
strategy: params.strategy,
|
|
108
|
+
query: params.query,
|
|
109
|
+
result_count: params.resultCount,
|
|
110
|
+
top_score: params.topScore,
|
|
111
|
+
threshold: params.threshold,
|
|
112
|
+
latency: {
|
|
113
|
+
embedding_ms: Math.round(params.embeddingMs),
|
|
114
|
+
storage_ms: Math.round(params.storageMs),
|
|
115
|
+
total_ms: Math.round(params.totalMs),
|
|
116
|
+
},
|
|
117
|
+
timestamp: new Date().toISOString(),
|
|
118
|
+
project: params.project,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Format a MemoryTrace into an MCP content block.
|
|
123
|
+
*
|
|
124
|
+
* Returns a single content block to push into the content[] array
|
|
125
|
+
* at index [1]. The "=== MEMORY TRACE ===" header makes it visually
|
|
126
|
+
* distinct from the human-readable search results at content[0].
|
|
127
|
+
*
|
|
128
|
+
* The trace is pretty-printed (2-space indent) for readability in
|
|
129
|
+
* console output and MCP inspector tools.
|
|
130
|
+
*
|
|
131
|
+
* @param trace - A MemoryTrace object from createMemoryTrace()
|
|
132
|
+
* @returns An MCP content block: { type: "text", text: "..." }
|
|
133
|
+
*/
|
|
134
|
+
export function traceToContentBlock(trace) {
|
|
135
|
+
return {
|
|
136
|
+
type: "text",
|
|
137
|
+
text: `=== MEMORY TRACE ===\n${JSON.stringify(trace, null, 2)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.1",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-mcp",
|
|
5
5
|
"description": "The Mind Palace for AI Agents — local-first MCP server with persistent memory (SQLite/Supabase), visual dashboard, time travel, multi-agent sync, Morning Briefings, reality drift detection, code mode templates, semantic vector search, and Brave Search + Gemini analysis. Zero-config local mode.",
|
|
6
6
|
"module": "index.ts",
|