prism-mcp-server 16.0.0 → 16.1.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.
@@ -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 = ?",
@@ -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 rows = await supabaseGet("session_ledger", queryParams);
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 (Array.isArray(rows) ? rows : [])) {
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
- `${scored.length} above threshold`);
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)
@@ -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 cacheKey = `${trimmedText.substring(0, 500)}|L${trimmedText.length}`;
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.0.0",
3
+ "version": "16.1.0",
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",