prism-mcp-server 4.6.1 → 5.2.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/README.md +408 -1306
- package/dist/dashboard/server.js +391 -22
- package/dist/dashboard/ui.js +363 -17
- package/dist/server.js +15 -2
- package/dist/storage/sqlite.js +277 -6
- package/dist/storage/supabase.js +58 -0
- package/dist/storage/supabaseMigrations.js +104 -1
- package/dist/tools/compactionHandler.js +17 -7
- package/dist/tools/index.js +2 -2
- package/dist/tools/sessionMemoryDefinitions.js +70 -0
- package/dist/tools/sessionMemoryHandlers.js +167 -9
- package/dist/utils/migration/claudeAdapter.js +131 -0
- package/dist/utils/migration/geminiAdapter.js +87 -0
- package/dist/utils/migration/openaiAdapter.js +88 -0
- package/dist/utils/migration/types.js +18 -0
- package/dist/utils/migration/utils.js +99 -0
- package/dist/utils/testUniversalImporter.js +10 -0
- package/dist/utils/turboquant.js +730 -0
- package/dist/utils/universalImporter.js +295 -0
- package/package.json +8 -4
package/dist/storage/sqlite.js
CHANGED
|
@@ -333,6 +333,66 @@ export class SqliteStorage {
|
|
|
333
333
|
// Composite indexes for behavioral queries (idempotent via IF NOT EXISTS)
|
|
334
334
|
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_event_type ON session_ledger(event_type)`);
|
|
335
335
|
await this.db.execute(`CREATE INDEX IF NOT EXISTS idx_ledger_importance ON session_ledger(importance DESC)`);
|
|
336
|
+
// ─── v5.0 Migration: TurboQuant Compressed Embeddings ─────
|
|
337
|
+
//
|
|
338
|
+
// REVIEWER NOTE: v5.0 introduces a DUAL-STORAGE strategy for embeddings:
|
|
339
|
+
// 1. `embedding` (F32_BLOB) — float32 for native vector search (Tier 1)
|
|
340
|
+
// 2. `embedding_compressed` (TEXT) — base64 TurboQuant blob for JS fallback (Tier 2)
|
|
341
|
+
// 3. `embedding_format` (TEXT) — 'turbo3', 'turbo4', or 'float32'
|
|
342
|
+
// 4. `embedding_turbo_radius` (REAL) — original vector magnitude
|
|
343
|
+
//
|
|
344
|
+
// WHY DUAL-STORAGE (not replace)?
|
|
345
|
+
// - Backward compatibility: existing installations with sqlite-vec
|
|
346
|
+
// continue using Tier-1 native vector search (fastest).
|
|
347
|
+
// - Graceful degradation: installations WITHOUT sqlite-vec fall back
|
|
348
|
+
// to Tier-2 JS-side asymmetric search using compressed blobs.
|
|
349
|
+
// - The compressed column is TEXT (base64) not BLOB because SQLite's
|
|
350
|
+
// TEXT type handles base64 more reliably across @libsql/client versions.
|
|
351
|
+
//
|
|
352
|
+
// STORAGE OVERHEAD: The compressed blob adds ~535 bytes per entry
|
|
353
|
+
// (400 bytes * 4/3 base64 expansion ≈ 535 chars). At 10K entries,
|
|
354
|
+
// this is ~5 MB — negligible compared to the 23 MB saved by not
|
|
355
|
+
// needing float32 vectors when sqlite-vec is unavailable.
|
|
356
|
+
// Stores compressed embedding alongside float32 for backward compat.
|
|
357
|
+
// Uses base64 TEXT (not F32_BLOB) — asymmetric search runs in JS.
|
|
358
|
+
try {
|
|
359
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN embedding_compressed TEXT DEFAULT NULL`);
|
|
360
|
+
debugLog("[SqliteStorage] v5.0 migration: added embedding_compressed column");
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
if (!e.message?.includes("duplicate column name"))
|
|
364
|
+
throw e;
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN embedding_format TEXT DEFAULT NULL`);
|
|
368
|
+
debugLog("[SqliteStorage] v5.0 migration: added embedding_format column");
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
if (!e.message?.includes("duplicate column name"))
|
|
372
|
+
throw e;
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN embedding_turbo_radius REAL DEFAULT NULL`);
|
|
376
|
+
debugLog("[SqliteStorage] v5.0 migration: added embedding_turbo_radius column");
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
if (!e.message?.includes("duplicate column name"))
|
|
380
|
+
throw e;
|
|
381
|
+
}
|
|
382
|
+
// ─── v5.2 Migration: Cognitive Memory — Last Accessed Tracking ───
|
|
383
|
+
//
|
|
384
|
+
// REVIEWER NOTE: last_accessed_at enables dynamic importance decay
|
|
385
|
+
// computed at retrieval time: effective = base * 0.95^days_since_access.
|
|
386
|
+
// No background workers needed — decay is a pure function of time.
|
|
387
|
+
// This column is updated fire-and-forget on each search hit.
|
|
388
|
+
try {
|
|
389
|
+
await this.db.execute(`ALTER TABLE session_ledger ADD COLUMN last_accessed_at TEXT DEFAULT NULL`);
|
|
390
|
+
debugLog("[SqliteStorage] v5.2 migration: added last_accessed_at column");
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
if (!e.message?.includes("duplicate column name"))
|
|
394
|
+
throw e;
|
|
395
|
+
}
|
|
336
396
|
}
|
|
337
397
|
// ─── PostgREST Filter Parser ───────────────────────────────
|
|
338
398
|
//
|
|
@@ -466,8 +526,9 @@ export class SqliteStorage {
|
|
|
466
526
|
(id, project, conversation_id, user_id, role, summary, todos, files_changed,
|
|
467
527
|
decisions, keywords, is_rollup, rollup_count, title, agent_name,
|
|
468
528
|
event_type, confidence_score, importance,
|
|
529
|
+
embedding_compressed, embedding_format, embedding_turbo_radius,
|
|
469
530
|
created_at, session_date)
|
|
470
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
531
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
471
532
|
args: [
|
|
472
533
|
id,
|
|
473
534
|
entry.project,
|
|
@@ -486,6 +547,9 @@ export class SqliteStorage {
|
|
|
486
547
|
entry.event_type || "session", // v4.0: default to 'session'
|
|
487
548
|
entry.confidence_score ?? null, // v4.0: nullable
|
|
488
549
|
entry.importance || 0, // v4.0: default to 0
|
|
550
|
+
entry.embedding_compressed || null, // v5.0: TurboQuant
|
|
551
|
+
entry.embedding_format || null, // v5.0: turbo3/turbo4/float32
|
|
552
|
+
entry.embedding_turbo_radius ?? null, // v5.0: original vector magnitude
|
|
489
553
|
now,
|
|
490
554
|
now,
|
|
491
555
|
],
|
|
@@ -494,9 +558,25 @@ export class SqliteStorage {
|
|
|
494
558
|
return [{ id, project: entry.project, created_at: now }];
|
|
495
559
|
}
|
|
496
560
|
async patchLedger(id, data) {
|
|
561
|
+
// ── Column Allowlist (Defense-in-Depth) ────────────────────────
|
|
562
|
+
// Column names are interpolated directly into SQL (not parameterizable).
|
|
563
|
+
// This allowlist prevents accidental or malicious injection via the key.
|
|
564
|
+
// Currently, patchLedger is only called from internal handler code,
|
|
565
|
+
// but this guard protects against future misuse if the method is
|
|
566
|
+
// exposed to less-controlled callers.
|
|
567
|
+
const ALLOWED_COLUMNS = new Set([
|
|
568
|
+
'embedding', 'embedding_compressed', 'embedding_format', 'embedding_turbo_radius',
|
|
569
|
+
'archived_at', 'deleted_at', 'deleted_reason', 'is_rollup', 'rollup_count',
|
|
570
|
+
'importance', 'last_accessed_at', 'keywords', 'todos', 'files_changed', 'decisions',
|
|
571
|
+
'summary', 'confidence_score', 'event_type', 'role',
|
|
572
|
+
]);
|
|
497
573
|
const sets = [];
|
|
498
574
|
const args = [];
|
|
499
575
|
for (const [key, value] of Object.entries(data)) {
|
|
576
|
+
if (!ALLOWED_COLUMNS.has(key)) {
|
|
577
|
+
debugLog(`[SqliteStorage] patchLedger: rejected unknown column "${key}" — skipping`);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
500
580
|
if (key === "embedding") {
|
|
501
581
|
// Use libSQL's native vector() function for F32_BLOB columns.
|
|
502
582
|
// The value is a JSON-stringified number[] from the handler.
|
|
@@ -951,11 +1031,102 @@ export class SqliteStorage {
|
|
|
951
1031
|
}));
|
|
952
1032
|
}
|
|
953
1033
|
catch (err) {
|
|
954
|
-
//
|
|
955
|
-
//
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
1034
|
+
// ─── TIER 2 FALLBACK: Asymmetric TurboQuant search in JS ───
|
|
1035
|
+
//
|
|
1036
|
+
// REVIEWER NOTE: THREE-TIER SEARCH ARCHITECTURE
|
|
1037
|
+
//
|
|
1038
|
+
// Tier 1: Native vector search via libSQL's vector_distance_cos()
|
|
1039
|
+
// - Uses the F32_BLOB `embedding` column with DiskANN index
|
|
1040
|
+
// - FASTEST: O(log n) approximate nearest neighbor
|
|
1041
|
+
// - Requires: libSQL ≥ 0.4.0 with sqlite-vec extension
|
|
1042
|
+
//
|
|
1043
|
+
// Tier 2: TurboQuant asymmetric search in JavaScript
|
|
1044
|
+
// - Fetches ALL compressed embeddings, scores each in JS
|
|
1045
|
+
// - Uses asymmetricCosineSimilarity(float32_query, compressed_target)
|
|
1046
|
+
// - O(n) linear scan, but n is typically < 10K entries
|
|
1047
|
+
// - Activated when: Tier 1 throws (older libSQL, no F32_BLOB)
|
|
1048
|
+
//
|
|
1049
|
+
// Tier 3: FTS5 keyword search (handled by searchKnowledge)
|
|
1050
|
+
// - Pure text matching, no vectors needed
|
|
1051
|
+
// - Last resort when both Tier 1 and Tier 2 fail
|
|
1052
|
+
//
|
|
1053
|
+
// WHY JS-SIDE SCORING (not SQLite UDF)?
|
|
1054
|
+
// @libsql/client doesn't support custom user-defined functions.
|
|
1055
|
+
// The TurboQuant math (matrix multiply, bit unpacking) requires
|
|
1056
|
+
// Float64Array operations that can't be expressed in SQL.
|
|
1057
|
+
// For typical Prism datasets (< 10K entries), linear scan
|
|
1058
|
+
// completes in < 100ms — acceptable for a memory search.
|
|
1059
|
+
debugLog(`[SqliteStorage] Tier-1 vector search failed, trying Tier-2 TurboQuant fallback: ${err}`);
|
|
1060
|
+
try {
|
|
1061
|
+
const { getDefaultCompressor, deserialize } = await import("../utils/turboquant.js");
|
|
1062
|
+
const compressor = getDefaultCompressor();
|
|
1063
|
+
// Parse query embedding from JSON string
|
|
1064
|
+
const queryVec = JSON.parse(params.queryEmbedding);
|
|
1065
|
+
// Fetch all entries that have compressed embeddings
|
|
1066
|
+
let fallbackSql;
|
|
1067
|
+
const fallbackArgs = [];
|
|
1068
|
+
if (params.project) {
|
|
1069
|
+
fallbackSql = `
|
|
1070
|
+
SELECT id, project, summary, decisions, files_changed,
|
|
1071
|
+
session_date, created_at, embedding_compressed, embedding_turbo_radius
|
|
1072
|
+
FROM session_ledger
|
|
1073
|
+
WHERE embedding_compressed IS NOT NULL
|
|
1074
|
+
AND user_id = ?
|
|
1075
|
+
AND project = ?
|
|
1076
|
+
AND archived_at IS NULL
|
|
1077
|
+
AND deleted_at IS NULL
|
|
1078
|
+
`;
|
|
1079
|
+
fallbackArgs.push(params.userId, params.project);
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
fallbackSql = `
|
|
1083
|
+
SELECT id, project, summary, decisions, files_changed,
|
|
1084
|
+
session_date, created_at, embedding_compressed, embedding_turbo_radius
|
|
1085
|
+
FROM session_ledger
|
|
1086
|
+
WHERE embedding_compressed IS NOT NULL
|
|
1087
|
+
AND user_id = ?
|
|
1088
|
+
AND archived_at IS NULL
|
|
1089
|
+
AND deleted_at IS NULL
|
|
1090
|
+
`;
|
|
1091
|
+
fallbackArgs.push(params.userId);
|
|
1092
|
+
}
|
|
1093
|
+
const fallbackResult = await this.db.execute({ sql: fallbackSql, args: fallbackArgs });
|
|
1094
|
+
// Score each entry using asymmetric cosine similarity
|
|
1095
|
+
const scored = [];
|
|
1096
|
+
for (const row of fallbackResult.rows) {
|
|
1097
|
+
try {
|
|
1098
|
+
const compressedBase64 = row.embedding_compressed;
|
|
1099
|
+
const buf = Buffer.from(compressedBase64, "base64");
|
|
1100
|
+
const compressed = deserialize(buf);
|
|
1101
|
+
const similarity = compressor.asymmetricCosineSimilarity(queryVec, compressed);
|
|
1102
|
+
if (similarity >= params.similarityThreshold) {
|
|
1103
|
+
scored.push({
|
|
1104
|
+
id: row.id,
|
|
1105
|
+
project: row.project,
|
|
1106
|
+
summary: row.summary,
|
|
1107
|
+
similarity,
|
|
1108
|
+
session_date: (row.session_date || row.created_at),
|
|
1109
|
+
decisions: this.parseJsonColumn(row.decisions),
|
|
1110
|
+
files_changed: this.parseJsonColumn(row.files_changed),
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
// Skip entries with corrupt compressed data
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
// Sort by similarity descending and limit
|
|
1119
|
+
scored.sort((a, b) => b.similarity - a.similarity);
|
|
1120
|
+
debugLog(`[SqliteStorage] Tier-2 TurboQuant fallback: scored ${fallbackResult.rows.length} entries, ` +
|
|
1121
|
+
`${scored.length} above threshold`);
|
|
1122
|
+
return scored.slice(0, params.limit);
|
|
1123
|
+
}
|
|
1124
|
+
catch (fallbackErr) {
|
|
1125
|
+
// Both tiers failed — return empty
|
|
1126
|
+
console.error(`[SqliteStorage] Both Tier-1 and Tier-2 search failed: ${fallbackErr}`);
|
|
1127
|
+
console.error("[SqliteStorage] Tip: Ensure you're using libSQL ≥ 0.4.0 for native vector support.");
|
|
1128
|
+
return [];
|
|
1129
|
+
}
|
|
959
1130
|
}
|
|
960
1131
|
}
|
|
961
1132
|
// ─── Compaction ────────────────────────────────────────────
|
|
@@ -1443,4 +1614,104 @@ export class SqliteStorage {
|
|
|
1443
1614
|
debugLog(`[SqliteStorage] decayImportance: reduced ${decayed} entries for "${project}" (>${decayDays}d old)`);
|
|
1444
1615
|
}
|
|
1445
1616
|
}
|
|
1617
|
+
// ─── v5.1: Deep Storage Mode ("The Purge") ────────────────────
|
|
1618
|
+
//
|
|
1619
|
+
// WHAT THIS DOES:
|
|
1620
|
+
// NULLs out bulky float32 `embedding` columns (3KB each) for entries
|
|
1621
|
+
// that already have TurboQuant `embedding_compressed` blobs (~400B each).
|
|
1622
|
+
// This reclaims ~90% of vector storage while maintaining Tier-2 search
|
|
1623
|
+
// accuracy at 95%+ via asymmetric TurboQuant cosine estimation.
|
|
1624
|
+
//
|
|
1625
|
+
// WHY IT'S SAFE:
|
|
1626
|
+
// 1. Only purges entries where embedding_compressed IS NOT NULL (guard clause)
|
|
1627
|
+
// — the compressed blob is the surviving search index
|
|
1628
|
+
// 2. Minimum age of 7 days enforced — recent entries keep full precision
|
|
1629
|
+
// so Tier-1 native sqlite-vec search can still use them
|
|
1630
|
+
// 3. Skips soft-deleted entries (deleted_at IS NULL filter)
|
|
1631
|
+
// 4. Multi-tenant user_id guard prevents cross-user purges
|
|
1632
|
+
// 5. Dry-run mode lets users preview the impact before executing
|
|
1633
|
+
//
|
|
1634
|
+
// SQL STRATEGY:
|
|
1635
|
+
// Two queries: one SELECT COUNT/SUM for preview stats, one conditional
|
|
1636
|
+
// UPDATE SET embedding = NULL for the actual purge. Both queries use
|
|
1637
|
+
// identical WHERE clauses built from the same conditions/args arrays.
|
|
1638
|
+
//
|
|
1639
|
+
// AFTER PURGE:
|
|
1640
|
+
// - Tier-1 (sqlite-vec DiskANN): entries without float32 are invisible
|
|
1641
|
+
// to native vector search — this is expected and harmless
|
|
1642
|
+
// - Tier-2 (TurboQuant JS-side): unaffected — uses embedding_compressed
|
|
1643
|
+
// - Tier-3 (FTS5 keyword): unaffected — uses text columns
|
|
1644
|
+
//
|
|
1645
|
+
// REVIEWER NOTE: We intentionally do NOT run VACUUM after purge.
|
|
1646
|
+
// VACUUM rewrites the entire database file and can be very slow
|
|
1647
|
+
// on large databases. Users who want to reclaim physical disk
|
|
1648
|
+
// space can run VACUUM manually via SQLite CLI. The NULLed columns
|
|
1649
|
+
// free up logical space that SQLite's b-tree allocator will reuse
|
|
1650
|
+
// for future writes.
|
|
1651
|
+
async purgeHighPrecisionEmbeddings(params) {
|
|
1652
|
+
// ── Safety guard: prevent purging entries younger than 7 days ──
|
|
1653
|
+
// Entries younger than 7 days may still benefit from Tier-1 native
|
|
1654
|
+
// sqlite-vec search (which requires float32 embeddings). Purging them
|
|
1655
|
+
// would silently degrade search quality for active projects.
|
|
1656
|
+
if (params.olderThanDays < 7) {
|
|
1657
|
+
throw new Error("olderThanDays must be at least 7 to prevent purging recent entries. " +
|
|
1658
|
+
"Entries younger than 7 days may still benefit from Tier-1 native vector search.");
|
|
1659
|
+
}
|
|
1660
|
+
// ── Build the WHERE clause dynamically ──
|
|
1661
|
+
// Each condition narrows the eligible set. The conditions array and args
|
|
1662
|
+
// array are kept in sync — condition[i] uses args[i] as its parameter.
|
|
1663
|
+
const conditions = [
|
|
1664
|
+
"embedding IS NOT NULL", // only entries that actually have float32 vectors
|
|
1665
|
+
"embedding_compressed IS NOT NULL", // CRITICAL: only entries that have a TurboQuant fallback
|
|
1666
|
+
"deleted_at IS NULL", // skip tombstoned entries
|
|
1667
|
+
`created_at < datetime('now', ?)`, // age filter using SQLite datetime modifier
|
|
1668
|
+
];
|
|
1669
|
+
// SQLite datetime modifier syntax: '-30 days', '-7 days', etc.
|
|
1670
|
+
const args = [`-${params.olderThanDays} days`];
|
|
1671
|
+
// Multi-tenant guard: always scope to userId to prevent cross-user purges
|
|
1672
|
+
if (params.userId) {
|
|
1673
|
+
conditions.push("user_id = ?");
|
|
1674
|
+
args.push(params.userId);
|
|
1675
|
+
}
|
|
1676
|
+
// Optional project filter: when omitted, purge spans all projects
|
|
1677
|
+
if (params.project) {
|
|
1678
|
+
conditions.push("project = ?");
|
|
1679
|
+
args.push(params.project);
|
|
1680
|
+
}
|
|
1681
|
+
const whereClause = conditions.join(" AND ");
|
|
1682
|
+
// ── Step 1: Count eligible entries and estimate bytes to reclaim ──
|
|
1683
|
+
// SUM(LENGTH(embedding)) gives the exact byte count of the float32 blobs
|
|
1684
|
+
// that will be freed. This is the number shown to the user in the response.
|
|
1685
|
+
const countResult = await this.db.execute({
|
|
1686
|
+
sql: `SELECT COUNT(*) as eligible,
|
|
1687
|
+
COALESCE(SUM(LENGTH(embedding)), 0) as bytes
|
|
1688
|
+
FROM session_ledger
|
|
1689
|
+
WHERE ${whereClause}`,
|
|
1690
|
+
args,
|
|
1691
|
+
});
|
|
1692
|
+
const eligible = Number(countResult.rows[0]?.eligible) || 0;
|
|
1693
|
+
const reclaimedBytes = Number(countResult.rows[0]?.bytes) || 0;
|
|
1694
|
+
// ── Dry run: return stats without modifying any data ──
|
|
1695
|
+
if (params.dryRun) {
|
|
1696
|
+
debugLog(`[SqliteStorage] purgeHighPrecisionEmbeddings DRY RUN: ` +
|
|
1697
|
+
`${eligible} eligible entries, ~${(reclaimedBytes / 1024 / 1024).toFixed(2)} MB reclaimable` +
|
|
1698
|
+
(params.project ? ` (project: ${params.project})` : " (all projects)"));
|
|
1699
|
+
return { purged: 0, eligible, reclaimedBytes };
|
|
1700
|
+
}
|
|
1701
|
+
// ── Step 2: Execute the purge — NULL out the float32 column ──
|
|
1702
|
+
// A single UPDATE is atomic — either all eligible entries are purged
|
|
1703
|
+
// or none are (in case of a database error). No partial state.
|
|
1704
|
+
if (eligible > 0) {
|
|
1705
|
+
await this.db.execute({
|
|
1706
|
+
sql: `UPDATE session_ledger
|
|
1707
|
+
SET embedding = NULL
|
|
1708
|
+
WHERE ${whereClause}`,
|
|
1709
|
+
args,
|
|
1710
|
+
});
|
|
1711
|
+
debugLog(`[SqliteStorage] purgeHighPrecisionEmbeddings: purged ${eligible} entries, ` +
|
|
1712
|
+
`reclaimed ~${(reclaimedBytes / 1024 / 1024).toFixed(2)} MB` +
|
|
1713
|
+
(params.project ? ` (project: ${params.project})` : " (all projects)"));
|
|
1714
|
+
}
|
|
1715
|
+
return { purged: eligible, eligible, reclaimedBytes };
|
|
1716
|
+
}
|
|
1446
1717
|
}
|
package/dist/storage/supabase.js
CHANGED
|
@@ -53,6 +53,10 @@ export class SupabaseStorage {
|
|
|
53
53
|
event_type: entry.event_type || "session",
|
|
54
54
|
...(entry.confidence_score !== undefined && { confidence_score: entry.confidence_score }),
|
|
55
55
|
importance: entry.importance || 0,
|
|
56
|
+
// v5.0: TurboQuant Compressed Embedding fields
|
|
57
|
+
...(entry.embedding_compressed !== undefined && { embedding_compressed: entry.embedding_compressed }),
|
|
58
|
+
...(entry.embedding_format !== undefined && { embedding_format: entry.embedding_format }),
|
|
59
|
+
...(entry.embedding_turbo_radius !== undefined && { embedding_turbo_radius: entry.embedding_turbo_radius }),
|
|
56
60
|
};
|
|
57
61
|
return supabasePost("session_ledger", record);
|
|
58
62
|
}
|
|
@@ -442,4 +446,58 @@ export class SupabaseStorage {
|
|
|
442
446
|
throw e;
|
|
443
447
|
}
|
|
444
448
|
}
|
|
449
|
+
// ─── v5.1: Deep Storage Mode ("The Purge") ────────────────────
|
|
450
|
+
//
|
|
451
|
+
// REVIEWER NOTE: This calls the prism_purge_embeddings RPC created
|
|
452
|
+
// by migration 030. The RPC runs server-side in Postgres with
|
|
453
|
+
// SECURITY DEFINER privileges, enforcing all safety guards:
|
|
454
|
+
// - p_older_than_days >= 7 (raises exception otherwise)
|
|
455
|
+
// - Only purges entries with embedding_compressed IS NOT NULL
|
|
456
|
+
// - Multi-tenant: scoped to p_user_id
|
|
457
|
+
// - Optional project filter (NULL = all projects)
|
|
458
|
+
// - Dry-run mode (preview without modifying)
|
|
459
|
+
//
|
|
460
|
+
// GRACEFUL DEGRADATION:
|
|
461
|
+
// If the RPC doesn't exist (PGRST202 — migration 030 not applied),
|
|
462
|
+
// we throw a clear error directing users to apply the migration.
|
|
463
|
+
// This matches the pattern used by other Supabase RPC calls
|
|
464
|
+
// (e.g., prism_adjust_importance in adjustImportance()).
|
|
465
|
+
//
|
|
466
|
+
// RETURN VALUE:
|
|
467
|
+
// The RPC returns a single-row TABLE with (eligible, purged, reclaimed_bytes).
|
|
468
|
+
// We parse this into the same TypeScript shape as the SQLite implementation.
|
|
469
|
+
async purgeHighPrecisionEmbeddings(params) {
|
|
470
|
+
// Safety guard: enforce minimum age (also enforced server-side, but
|
|
471
|
+
// catch early to avoid RPC roundtrip for obviously invalid requests)
|
|
472
|
+
if (params.olderThanDays < 7) {
|
|
473
|
+
throw new Error("olderThanDays must be at least 7 to prevent purging recent entries. " +
|
|
474
|
+
"Entries younger than 7 days may still benefit from Tier-1 native vector search.");
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
const result = await supabaseRpc("prism_purge_embeddings", {
|
|
478
|
+
p_project: params.project || null, // NULL = all projects
|
|
479
|
+
p_user_id: params.userId,
|
|
480
|
+
p_older_than_days: params.olderThanDays,
|
|
481
|
+
p_dry_run: params.dryRun,
|
|
482
|
+
});
|
|
483
|
+
// RPC returns TABLE(eligible, purged, reclaimed_bytes) — parse the first row
|
|
484
|
+
const data = Array.isArray(result) ? result[0] : result;
|
|
485
|
+
return {
|
|
486
|
+
eligible: Number(data?.eligible) || 0,
|
|
487
|
+
purged: Number(data?.purged) || 0,
|
|
488
|
+
reclaimedBytes: Number(data?.reclaimed_bytes) || 0,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
catch (e) {
|
|
492
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
493
|
+
// PGRST202 = function not found — migration 030 not applied yet
|
|
494
|
+
if (msg.includes("PGRST202") || msg.includes("Could not find the function")) {
|
|
495
|
+
throw new Error("Deep Storage Purge requires migration 030 (prism_purge_embeddings RPC). " +
|
|
496
|
+
"Apply the migration via: supabase db push, or run " +
|
|
497
|
+
"supabase/migrations/030_deep_storage_purge.sql in your SQL Editor.");
|
|
498
|
+
}
|
|
499
|
+
debugLog("[SupabaseStorage] purgeHighPrecisionEmbeddings failed: " + msg);
|
|
500
|
+
throw e;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
445
503
|
}
|
|
@@ -97,7 +97,110 @@ export const MIGRATIONS = [
|
|
|
97
97
|
$$;
|
|
98
98
|
`,
|
|
99
99
|
},
|
|
100
|
-
|
|
100
|
+
{
|
|
101
|
+
version: 29,
|
|
102
|
+
name: "turboquant_compressed_embeddings",
|
|
103
|
+
sql: `
|
|
104
|
+
-- v5.0: TurboQuant Compressed Embedding columns
|
|
105
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS embedding_compressed TEXT DEFAULT NULL;
|
|
106
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS embedding_format TEXT DEFAULT NULL;
|
|
107
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS embedding_turbo_radius REAL DEFAULT NULL;
|
|
108
|
+
`,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
// ─── v5.1: Deep Storage Mode — Purge RPC ──────────────────────
|
|
112
|
+
//
|
|
113
|
+
// REVIEWER NOTE: This creates a Postgres function that NULLs out
|
|
114
|
+
// the float32 `embedding` column for entries that already have
|
|
115
|
+
// TurboQuant `embedding_compressed` blobs. This is the Supabase
|
|
116
|
+
// counterpart to SqliteStorage.purgeHighPrecisionEmbeddings().
|
|
117
|
+
//
|
|
118
|
+
// The function enforces the same safety guards as the SQLite impl:
|
|
119
|
+
// - p_older_than_days >= 7 (recent entries keep full precision)
|
|
120
|
+
// - embedding_compressed IS NOT NULL (never destroys last copy)
|
|
121
|
+
// - deleted_at IS NULL (skip tombstoned entries)
|
|
122
|
+
// - user_id scoping (multi-tenant guard)
|
|
123
|
+
// - Optional project filter (NULL = all projects)
|
|
124
|
+
// - Dry-run mode (preview without modifying)
|
|
125
|
+
//
|
|
126
|
+
// After this migration, SupabaseStorage.purgeHighPrecisionEmbeddings()
|
|
127
|
+
// calls this RPC instead of throwing "not supported".
|
|
128
|
+
version: 30,
|
|
129
|
+
name: "deep_storage_purge",
|
|
130
|
+
sql: `
|
|
131
|
+
CREATE OR REPLACE FUNCTION prism_purge_embeddings(
|
|
132
|
+
p_project TEXT DEFAULT NULL,
|
|
133
|
+
p_user_id TEXT DEFAULT 'default',
|
|
134
|
+
p_older_than_days INTEGER DEFAULT 30,
|
|
135
|
+
p_dry_run BOOLEAN DEFAULT false
|
|
136
|
+
)
|
|
137
|
+
RETURNS TABLE(eligible INTEGER, purged INTEGER, reclaimed_bytes BIGINT)
|
|
138
|
+
LANGUAGE plpgsql
|
|
139
|
+
SECURITY DEFINER
|
|
140
|
+
SET search_path = public
|
|
141
|
+
AS $$
|
|
142
|
+
DECLARE
|
|
143
|
+
v_eligible INTEGER;
|
|
144
|
+
v_bytes BIGINT;
|
|
145
|
+
v_cutoff TIMESTAMPTZ;
|
|
146
|
+
BEGIN
|
|
147
|
+
IF p_older_than_days < 7 THEN
|
|
148
|
+
RAISE EXCEPTION 'p_older_than_days must be at least 7 to prevent purging recent entries';
|
|
149
|
+
END IF;
|
|
150
|
+
|
|
151
|
+
v_cutoff := now() - (p_older_than_days || ' days')::interval;
|
|
152
|
+
|
|
153
|
+
SELECT COUNT(*)::INTEGER,
|
|
154
|
+
COALESCE(SUM(octet_length(embedding::text)), 0)::BIGINT
|
|
155
|
+
INTO v_eligible, v_bytes
|
|
156
|
+
FROM session_ledger
|
|
157
|
+
WHERE embedding IS NOT NULL
|
|
158
|
+
AND embedding_compressed IS NOT NULL
|
|
159
|
+
AND deleted_at IS NULL
|
|
160
|
+
AND created_at < v_cutoff
|
|
161
|
+
AND user_id = p_user_id
|
|
162
|
+
AND (p_project IS NULL OR project = p_project);
|
|
163
|
+
|
|
164
|
+
IF p_dry_run THEN
|
|
165
|
+
RETURN QUERY SELECT v_eligible, 0::INTEGER, v_bytes;
|
|
166
|
+
RETURN;
|
|
167
|
+
END IF;
|
|
168
|
+
|
|
169
|
+
IF v_eligible > 0 THEN
|
|
170
|
+
UPDATE session_ledger
|
|
171
|
+
SET embedding = NULL
|
|
172
|
+
WHERE embedding IS NOT NULL
|
|
173
|
+
AND embedding_compressed IS NOT NULL
|
|
174
|
+
AND deleted_at IS NULL
|
|
175
|
+
AND created_at < v_cutoff
|
|
176
|
+
AND user_id = p_user_id
|
|
177
|
+
AND (p_project IS NULL OR project = p_project);
|
|
178
|
+
END IF;
|
|
179
|
+
|
|
180
|
+
RETURN QUERY SELECT v_eligible, v_eligible, v_bytes;
|
|
181
|
+
END;
|
|
182
|
+
$$;
|
|
183
|
+
`,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
// ─── v5.2: Cognitive Memory — Last Accessed Tracking ──────────
|
|
187
|
+
//
|
|
188
|
+
// REVIEWER NOTE: This column enables the Ebbinghaus Importance Decay
|
|
189
|
+
// feature (effective = base * 0.95^days_since_accessed) computed at
|
|
190
|
+
// retrieval time in sessionMemoryHandlers.ts. No background workers
|
|
191
|
+
// needed — decay is a pure function of time.
|
|
192
|
+
//
|
|
193
|
+
// The column is updated fire-and-forget via patchLedger() on every
|
|
194
|
+
// search hit. NULLs are expected (entries never retrieved yet) and
|
|
195
|
+
// the decay formula falls back to created_at when last_accessed_at
|
|
196
|
+
// is NULL.
|
|
197
|
+
version: 31,
|
|
198
|
+
name: "cognitive_memory_last_accessed",
|
|
199
|
+
sql: `
|
|
200
|
+
ALTER TABLE session_ledger ADD COLUMN IF NOT EXISTS last_accessed_at TIMESTAMPTZ DEFAULT NULL;
|
|
201
|
+
`,
|
|
202
|
+
},
|
|
203
|
+
// Future migrations go here (version 32+)
|
|
101
204
|
];
|
|
102
205
|
/**
|
|
103
206
|
* Current schema version — derived from the MIGRATIONS array.
|
|
@@ -23,13 +23,23 @@ async function summarizeEntries(entries) {
|
|
|
23
23
|
const entriesText = entries.map((e, i) => `[${i + 1}] ${e.session_date || "unknown date"}: ${e.summary || "no summary"}\n` +
|
|
24
24
|
(e.decisions?.length ? ` Decisions: ${e.decisions.join("; ")}\n` : "") +
|
|
25
25
|
(e.files_changed?.length ? ` Files: ${e.files_changed.join(", ")}\n` : "")).join("\n");
|
|
26
|
-
const prompt = (`You are compressing a session history log
|
|
27
|
-
`
|
|
28
|
-
`
|
|
29
|
-
`
|
|
30
|
-
`
|
|
31
|
-
`
|
|
32
|
-
`
|
|
26
|
+
const prompt = (`You are compressing a session history log for an AI agent's persistent memory.\n\n` +
|
|
27
|
+
`Analyze these ${entries.length} work sessions and produce THREE sections:\n\n` +
|
|
28
|
+
`1. SUMMARY (max 300 words): A concise paragraph preserving key decisions, ` +
|
|
29
|
+
`important file changes, error resolutions, and architecture changes. ` +
|
|
30
|
+
`Omit routine operations and intermediate debugging steps.\n\n` +
|
|
31
|
+
`2. PRINCIPLES (1-3 bullet points): Reusable lessons extracted from these sessions. ` +
|
|
32
|
+
`These should be actionable engineering insights the agent can apply to future work. ` +
|
|
33
|
+
`Format: "- [principle]"\n\n` +
|
|
34
|
+
`3. PATTERNS (1-3 bullet points): Recurring behaviors, tools, or workflows observed. ` +
|
|
35
|
+
`Format: "- [pattern]"\n\n` +
|
|
36
|
+
`Sessions to analyze:\n${entriesText}\n\n` +
|
|
37
|
+
`Output format (follow exactly):\n` +
|
|
38
|
+
`[summary paragraph]\n\n` +
|
|
39
|
+
`Principles:\n` +
|
|
40
|
+
`- ...\n\n` +
|
|
41
|
+
`Patterns:\n` +
|
|
42
|
+
`- ...`).substring(0, 30000);
|
|
33
43
|
return llm.generateText(prompt);
|
|
34
44
|
}
|
|
35
45
|
// ─── Main Handler ─────────────────────────────────────────────
|
package/dist/tools/index.js
CHANGED
|
@@ -26,8 +26,8 @@ export { webSearchHandler, braveWebSearchCodeModeHandler, localSearchHandler, br
|
|
|
26
26
|
// This file always exports them — server.ts decides whether to include them in the tool list.
|
|
27
27
|
//
|
|
28
28
|
// v0.4.0: Added SESSION_COMPACT_LEDGER_TOOL and SESSION_SEARCH_MEMORY_TOOL
|
|
29
|
-
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL, SESSION_EXPORT_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL } from "./sessionMemoryDefinitions.js";
|
|
30
|
-
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler, sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler, knowledgeSyncRulesHandler, sessionExportMemoryHandler } from "./sessionMemoryHandlers.js";
|
|
29
|
+
export { SESSION_SAVE_LEDGER_TOOL, SESSION_SAVE_HANDOFF_TOOL, SESSION_LOAD_CONTEXT_TOOL, KNOWLEDGE_SEARCH_TOOL, KNOWLEDGE_FORGET_TOOL, SESSION_COMPACT_LEDGER_TOOL, SESSION_SEARCH_MEMORY_TOOL, MEMORY_HISTORY_TOOL, MEMORY_CHECKOUT_TOOL, SESSION_SAVE_IMAGE_TOOL, SESSION_VIEW_IMAGE_TOOL, SESSION_HEALTH_CHECK_TOOL, SESSION_FORGET_MEMORY_TOOL, SESSION_EXPORT_MEMORY_TOOL, KNOWLEDGE_SET_RETENTION_TOOL, SESSION_SAVE_EXPERIENCE_TOOL, KNOWLEDGE_UPVOTE_TOOL, KNOWLEDGE_DOWNVOTE_TOOL, KNOWLEDGE_SYNC_RULES_TOOL, DEEP_STORAGE_PURGE_TOOL, isDeepStoragePurgeArgs } from "./sessionMemoryDefinitions.js";
|
|
30
|
+
export { sessionSaveLedgerHandler, sessionSaveHandoffHandler, sessionLoadContextHandler, knowledgeSearchHandler, knowledgeForgetHandler, sessionSearchMemoryHandler, backfillEmbeddingsHandler, memoryHistoryHandler, memoryCheckoutHandler, sessionSaveImageHandler, sessionViewImageHandler, sessionHealthCheckHandler, sessionForgetMemoryHandler, knowledgeSetRetentionHandler, sessionSaveExperienceHandler, knowledgeUpvoteHandler, knowledgeDownvoteHandler, knowledgeSyncRulesHandler, sessionExportMemoryHandler, deepStoragePurgeHandler } from "./sessionMemoryHandlers.js";
|
|
31
31
|
// ── Compaction Handler (v0.4.0 — Enhancement #2) ──
|
|
32
32
|
// The compaction handler is in a separate file because it's significantly
|
|
33
33
|
// more complex than the other session memory handlers (chunked Gemini
|
|
@@ -303,6 +303,13 @@ export const SESSION_SEARCH_MEMORY_TOOL = {
|
|
|
303
303
|
description: "If true, returns a separate MEMORY TRACE content block with search strategy, " +
|
|
304
304
|
"latency breakdown (embedding vs storage), and scoring metadata. Default: false.",
|
|
305
305
|
},
|
|
306
|
+
// v5.2: Context-Weighted Retrieval — biases search toward active work context
|
|
307
|
+
context_boost: {
|
|
308
|
+
type: "boolean",
|
|
309
|
+
description: "If true, appends current project and working context to the search query " +
|
|
310
|
+
"before embedding generation, naturally biasing results toward contextually relevant memories. " +
|
|
311
|
+
"Useful when searching within a specific project context. Default: false.",
|
|
312
|
+
},
|
|
306
313
|
},
|
|
307
314
|
required: ["query"],
|
|
308
315
|
},
|
|
@@ -836,3 +843,66 @@ export function isKnowledgeSyncRulesArgs(args) {
|
|
|
836
843
|
"project" in args &&
|
|
837
844
|
typeof args.project === "string");
|
|
838
845
|
}
|
|
846
|
+
// ─── v5.1: Deep Storage Mode (The Purge) ──────────────────────
|
|
847
|
+
//
|
|
848
|
+
// REVIEWER NOTE: This tool is the storage optimization follow-up to v5.0's
|
|
849
|
+
// TurboQuant integration. Now that compressed blobs provide Tier-2 search,
|
|
850
|
+
// the original float32 embeddings (3KB each) for OLD entries are redundant.
|
|
851
|
+
//
|
|
852
|
+
// DESIGN DECISIONS:
|
|
853
|
+
// - dry_run defaults to false (consistent with session_compact_ledger)
|
|
854
|
+
// - older_than_days defaults to 30 and has a minimum of 7 (enforced at storage layer)
|
|
855
|
+
// - project is optional: omit to purge across all projects
|
|
856
|
+
// - No required fields — tool works with zero args (purges all projects, 30+ day old entries)
|
|
857
|
+
//
|
|
858
|
+
// SAFETY NET:
|
|
859
|
+
// - Storage layer throws if olderThanDays < 7
|
|
860
|
+
// - Only entries with BOTH embedding AND embedding_compressed are eligible
|
|
861
|
+
// - Multi-tenant user_id guard is injected by the handler (not user-facing)
|
|
862
|
+
export const DEEP_STORAGE_PURGE_TOOL = {
|
|
863
|
+
name: "deep_storage_purge",
|
|
864
|
+
description: "v5.1 Deep Storage Mode: Purge high-precision float32 embedding vectors for entries " +
|
|
865
|
+
"that already have TurboQuant compressed blobs, reclaiming ~90% of vector storage. " +
|
|
866
|
+
"Only affects entries older than the specified threshold (default: 30 days, minimum: 7). " +
|
|
867
|
+
"Entries without compressed blobs are NEVER touched. " +
|
|
868
|
+
"Use dry_run=true to preview the impact before executing.\n\n" +
|
|
869
|
+
"**When to use:** After running TurboQuant backfill (session_backfill_embeddings), " +
|
|
870
|
+
"call this tool to reclaim disk space from legacy float32 vectors that are no longer " +
|
|
871
|
+
"needed for search.\n\n" +
|
|
872
|
+
"**Safety:** Tier-2 search (TurboQuant) maintains 95%+ accuracy with compressed blobs. " +
|
|
873
|
+
"Tier-3 (FTS5 keyword) search is completely unaffected.",
|
|
874
|
+
inputSchema: {
|
|
875
|
+
type: "object",
|
|
876
|
+
properties: {
|
|
877
|
+
project: {
|
|
878
|
+
type: "string",
|
|
879
|
+
description: "Optional project filter. When omitted, purges across all projects.",
|
|
880
|
+
},
|
|
881
|
+
older_than_days: {
|
|
882
|
+
type: "integer",
|
|
883
|
+
description: "Only purge entries older than this many days. " +
|
|
884
|
+
"Default: 30. Minimum: 7 (enforced). " +
|
|
885
|
+
"Entries younger than this threshold keep full float32 precision " +
|
|
886
|
+
"for Tier-1 native vector search.",
|
|
887
|
+
},
|
|
888
|
+
dry_run: {
|
|
889
|
+
type: "boolean",
|
|
890
|
+
description: "If true, reports eligible count and estimated byte savings " +
|
|
891
|
+
"without purging any data. Default: false.",
|
|
892
|
+
},
|
|
893
|
+
},
|
|
894
|
+
// No required fields — tool works with sensible defaults (30 days, all projects)
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
export function isDeepStoragePurgeArgs(args) {
|
|
898
|
+
if (typeof args !== "object" || args === null)
|
|
899
|
+
return false;
|
|
900
|
+
const a = args;
|
|
901
|
+
if (a.project !== undefined && typeof a.project !== "string")
|
|
902
|
+
return false;
|
|
903
|
+
if (a.older_than_days !== undefined && typeof a.older_than_days !== "number")
|
|
904
|
+
return false;
|
|
905
|
+
if (a.dry_run !== undefined && typeof a.dry_run !== "boolean")
|
|
906
|
+
return false;
|
|
907
|
+
return true;
|
|
908
|
+
}
|