prism-mcp-server 16.0.0 → 16.1.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/dist/storage/sqlite.js
CHANGED
|
@@ -1183,6 +1183,33 @@ export class SqliteStorage {
|
|
|
1183
1183
|
version: result.rows[0].version,
|
|
1184
1184
|
};
|
|
1185
1185
|
}
|
|
1186
|
+
async patchHandoff(project, userId, data) {
|
|
1187
|
+
const ALLOWED_COLUMNS = new Set([
|
|
1188
|
+
'embedding', 'embedding_compressed', 'embedding_format', 'embedding_turbo_radius',
|
|
1189
|
+
]);
|
|
1190
|
+
const sets = [];
|
|
1191
|
+
const args = [];
|
|
1192
|
+
for (const [key, value] of Object.entries(data)) {
|
|
1193
|
+
if (!ALLOWED_COLUMNS.has(key)) {
|
|
1194
|
+
throw new Error(`[SqliteStorage] patchHandoff: rejected unknown column "${key}".`);
|
|
1195
|
+
}
|
|
1196
|
+
if (key === "embedding") {
|
|
1197
|
+
sets.push(`${key} = vector(?)`);
|
|
1198
|
+
args.push((typeof value === "string" ? value : JSON.stringify(value)));
|
|
1199
|
+
}
|
|
1200
|
+
else {
|
|
1201
|
+
sets.push(`${key} = ?`);
|
|
1202
|
+
args.push((typeof value === "object" && value !== null ? JSON.stringify(value) : value));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
if (sets.length === 0)
|
|
1206
|
+
return;
|
|
1207
|
+
args.push(project, userId);
|
|
1208
|
+
await this.db.execute({
|
|
1209
|
+
sql: `UPDATE session_handoffs SET ${sets.join(", ")} WHERE project = ? AND user_id = ?`,
|
|
1210
|
+
args,
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1186
1213
|
async deleteHandoff(project, userId) {
|
|
1187
1214
|
await this.db.execute({
|
|
1188
1215
|
sql: "DELETE FROM session_handoffs WHERE project = ? AND user_id = ?",
|
package/dist/storage/supabase.js
CHANGED
|
@@ -161,6 +161,12 @@ export class SupabaseStorage {
|
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
+
async patchHandoff(project, userId, data) {
|
|
165
|
+
await supabasePatch("session_handoffs", data, {
|
|
166
|
+
project: `eq.${project}`,
|
|
167
|
+
user_id: `eq.${userId}`,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
164
170
|
async deleteHandoff(project, userId) {
|
|
165
171
|
await supabaseDelete("session_handoffs", {
|
|
166
172
|
project: `eq.${project}`,
|
|
@@ -285,12 +291,36 @@ export class SupabaseStorage {
|
|
|
285
291
|
queryParams.project = `eq.${params.project}`;
|
|
286
292
|
if (params.role)
|
|
287
293
|
queryParams.role = `eq.${params.role}`;
|
|
288
|
-
const
|
|
294
|
+
const ledgerRows = await supabaseGet("session_ledger", queryParams);
|
|
295
|
+
// Also fetch handoff entries with embeddings
|
|
296
|
+
const handoffParams = {
|
|
297
|
+
user_id: `eq.${params.userId}`,
|
|
298
|
+
embedding_compressed: "not.is.null",
|
|
299
|
+
select: "id,project,last_summary,active_decisions,updated_at,embedding_compressed,embedding_turbo_radius",
|
|
300
|
+
limit: "500",
|
|
301
|
+
};
|
|
302
|
+
if (params.project)
|
|
303
|
+
handoffParams.project = `eq.${params.project}`;
|
|
304
|
+
if (params.role)
|
|
305
|
+
handoffParams.role = `eq.${params.role}`;
|
|
306
|
+
const handoffRows = await supabaseGet("session_handoffs", handoffParams);
|
|
307
|
+
// Normalize handoff rows to match ledger shape for scoring
|
|
308
|
+
const normalizedHandoffs = (Array.isArray(handoffRows) ? handoffRows : []).map(h => ({
|
|
309
|
+
...h,
|
|
310
|
+
summary: h.last_summary || "",
|
|
311
|
+
decisions: h.active_decisions || [],
|
|
312
|
+
files_changed: [],
|
|
313
|
+
session_date: h.updated_at,
|
|
314
|
+
created_at: h.updated_at,
|
|
315
|
+
}));
|
|
316
|
+
const rows = [
|
|
317
|
+
...(Array.isArray(ledgerRows) ? ledgerRows : []),
|
|
318
|
+
...normalizedHandoffs,
|
|
319
|
+
];
|
|
289
320
|
const scored = [];
|
|
290
|
-
// v9.3: Import tiebreaker config for optional residualNorm ranking
|
|
291
321
|
const { PRISM_TURBOQUANT_TIEBREAKER_EPSILON } = await import("../config.js");
|
|
292
322
|
const eps = PRISM_TURBOQUANT_TIEBREAKER_EPSILON;
|
|
293
|
-
for (const row of
|
|
323
|
+
for (const row of rows) {
|
|
294
324
|
try {
|
|
295
325
|
const compressedBase64 = row.embedding_compressed;
|
|
296
326
|
const buf = Buffer.from(compressedBase64, "base64");
|
|
@@ -313,7 +343,6 @@ export class SupabaseStorage {
|
|
|
313
343
|
// Skip entries with corrupt compressed data
|
|
314
344
|
}
|
|
315
345
|
}
|
|
316
|
-
// Sort by similarity descending, with optional residualNorm tiebreaker
|
|
317
346
|
scored.sort((a, b) => {
|
|
318
347
|
const diff = b.similarity - a.similarity;
|
|
319
348
|
if (eps > 0 && Math.abs(diff) < eps && a._residualNorm != null && b._residualNorm != null) {
|
|
@@ -321,8 +350,8 @@ export class SupabaseStorage {
|
|
|
321
350
|
}
|
|
322
351
|
return diff;
|
|
323
352
|
});
|
|
324
|
-
debugLog(`[SupabaseStorage] Tier-2 TurboQuant fallback: scored ${rows.length} entries
|
|
325
|
-
|
|
353
|
+
debugLog(`[SupabaseStorage] Tier-2 TurboQuant fallback: scored ${rows.length} entries ` +
|
|
354
|
+
`(${ledgerRows.length} ledger + ${handoffRows.length} handoff), ${scored.length} above threshold`);
|
|
326
355
|
const results = scored.slice(0, params.limit);
|
|
327
356
|
// Strip internal tiebreaker field before returning
|
|
328
357
|
for (const r of results)
|
|
@@ -483,7 +483,7 @@ export async function sessionSearchMemoryHandler(args) {
|
|
|
483
483
|
// This MUST happen after re-ranking but BEFORE recording access events,
|
|
484
484
|
// so we only log access for results actually delivered to the LLM.
|
|
485
485
|
results.splice(limit);
|
|
486
|
-
if (results.length > 0) {
|
|
486
|
+
if (PRISM_HDC_ENABLED && results.length > 0) {
|
|
487
487
|
const topScore = PRISM_ACTR_ENABLED ? results[0]._actr_composite : results[0].similarity;
|
|
488
488
|
const secondScore = results.length > 1 ? (PRISM_ACTR_ENABLED ? results[1]._actr_composite : results[1].similarity) : 0;
|
|
489
489
|
const gapDistance = (topScore || 0) - (secondScore || 0);
|
|
@@ -400,6 +400,40 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
400
400
|
};
|
|
401
401
|
storage.saveHistorySnapshot(snapshotEntry).catch(err => console.error(`[session_save_handoff] History snapshot failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`));
|
|
402
402
|
}
|
|
403
|
+
// ─── Fire-and-forget embedding generation (enables semantic search on handoffs) ───
|
|
404
|
+
if (data.status === "created" || data.status === "updated") {
|
|
405
|
+
const embeddingText = [
|
|
406
|
+
last_summary || "",
|
|
407
|
+
key_context || "",
|
|
408
|
+
...(open_todos || []),
|
|
409
|
+
].filter(Boolean).join("\n");
|
|
410
|
+
if (embeddingText.trim()) {
|
|
411
|
+
getLLMProvider().generateEmbedding(embeddingText)
|
|
412
|
+
.then(async (embedding) => {
|
|
413
|
+
const patchData = {
|
|
414
|
+
embedding: JSON.stringify(embedding),
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
const { getDefaultCompressor, serialize } = await import("../utils/turboquant.js");
|
|
418
|
+
const compressor = getDefaultCompressor();
|
|
419
|
+
const compressed = compressor.compress(embedding);
|
|
420
|
+
const buf = serialize(compressed);
|
|
421
|
+
patchData.embedding_compressed = buf.toString("base64");
|
|
422
|
+
patchData.embedding_format = `turbo${compressor.bits}`;
|
|
423
|
+
patchData.embedding_turbo_radius = compressed.radius;
|
|
424
|
+
debugLog(`[session_save_handoff] TurboQuant compressed: ${buf.length} bytes`);
|
|
425
|
+
}
|
|
426
|
+
catch (turboErr) {
|
|
427
|
+
console.error(`[session_save_handoff] TurboQuant compression failed (non-fatal): ${turboErr.message}`);
|
|
428
|
+
}
|
|
429
|
+
await storage.patchHandoff(project, PRISM_USER_ID, patchData);
|
|
430
|
+
debugLog(`[session_save_handoff] Embedding saved for project "${project}"`);
|
|
431
|
+
})
|
|
432
|
+
.catch((err) => {
|
|
433
|
+
console.error(`[session_save_handoff] Embedding generation failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
403
437
|
// ─── Trigger resource subscription notification ───
|
|
404
438
|
if (server && (data.status === "created" || data.status === "updated")) {
|
|
405
439
|
try {
|
|
@@ -523,6 +557,7 @@ export async function sessionSaveHandoffHandler(args, server) {
|
|
|
523
557
|
(last_summary ? `Last summary: ${last_summary}\n` : "") +
|
|
524
558
|
(open_todos?.length ? `Open TODOs: ${open_todos.length} items\n` : "") +
|
|
525
559
|
(active_branch ? `Active branch: ${active_branch}\n` : "") +
|
|
560
|
+
`📊 Embedding generation queued for semantic search.\n` +
|
|
526
561
|
`\n🔑 Remember: pass expected_version: ${newVersion} on your next save ` +
|
|
527
562
|
`to maintain concurrency control.`;
|
|
528
563
|
return {
|
|
@@ -112,7 +112,8 @@ export class OpenAIAdapter {
|
|
|
112
112
|
throw new Error("Cannot generate embedding for empty text.");
|
|
113
113
|
}
|
|
114
114
|
const trimmedText = text.trim();
|
|
115
|
-
const
|
|
115
|
+
const model = getSettingSync("openai_embedding_model", "text-embedding-3-small");
|
|
116
|
+
const cacheKey = `${model}|${trimmedText.substring(0, 500)}|L${trimmedText.length}`;
|
|
116
117
|
const entry = OpenAIAdapter._embeddingCache.get(cacheKey);
|
|
117
118
|
if (entry && Date.now() - entry.ts < OpenAIAdapter.EMBED_CACHE_TTL_MS) {
|
|
118
119
|
debugLog(`[OpenAIAdapter] Embedding cache HIT`);
|
|
@@ -127,7 +128,7 @@ export class OpenAIAdapter {
|
|
|
127
128
|
debugLog(`[OpenAIAdapter] Embedding in-flight dedup HIT`);
|
|
128
129
|
return inflight;
|
|
129
130
|
}
|
|
130
|
-
const promise = this._generateEmbeddingImpl(trimmedText, cacheKey);
|
|
131
|
+
const promise = this._generateEmbeddingImpl(trimmedText, cacheKey, model);
|
|
131
132
|
OpenAIAdapter._inflight.set(cacheKey, promise);
|
|
132
133
|
try {
|
|
133
134
|
return await promise;
|
|
@@ -136,8 +137,7 @@ export class OpenAIAdapter {
|
|
|
136
137
|
OpenAIAdapter._inflight.delete(cacheKey);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
|
-
async _generateEmbeddingImpl(inputTextRaw, cacheKey) {
|
|
140
|
-
const model = getSettingSync("openai_embedding_model", "text-embedding-3-small");
|
|
140
|
+
async _generateEmbeddingImpl(inputTextRaw, cacheKey, model) {
|
|
141
141
|
// ── Truncation Guard ───────────────────────────────────────────────────
|
|
142
142
|
// text-embedding-3-small accepts up to 8191 tokens.
|
|
143
143
|
// We apply the same preventive truncation as GeminiAdapter so behavior
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.1.1",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 54 Agent Skills, Zero-Search HDC/HRR retrieval, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder:7b / 14b open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|