@prom.codes/memory-mcp 0.7.3 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/bin.js +109 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,9 +33,10 @@ session end.
33
33
  `roots`) — no need to set `PROMETHEUS_WORKSPACE_ROOT`.
34
34
  - **Optional quality levers** (documented): `PROMETHEUS_MEMORY_RERANK_PROVIDER`
35
35
  (cross-encoder rerank — reranked recall ties full-context at ~28× fewer tokens
36
- on LoCoMo) and `PROMETHEUS_MEMORY_REWRITE_PROVIDER` (HyDE). Temporal-intent
37
- ranking ("latest/earliest" queries) is **on by default**; disable with
38
- `PROMETHEUS_MEMORY_TEMPORAL=off`.
36
+ on LoCoMo), `PROMETHEUS_MEMORY_REWRITE_PROVIDER` (HyDE), and
37
+ `PROMETHEUS_MEMORY_DEDUP=on` (collapse restatements so distinct facts fill the
38
+ top-k). Temporal-intent ranking ("latest/earliest" queries) is **on by
39
+ default**; disable with `PROMETHEUS_MEMORY_TEMPORAL=off`.
39
40
 
40
41
  If a fresh window opens with no project, the workspace falls back to the host
41
42
  cwd (often home); in that case memory still works but project memories are NOT
package/dist/bin.js CHANGED
@@ -1360,6 +1360,59 @@ function applyTemporalRanking(hits, intent, getTimestampMs, weight = DEFAULT_TEM
1360
1360
  return scored.map((s) => s.hit);
1361
1361
  }
1362
1362
 
1363
+ // dist/dedup.js
1364
+ function normalizeForDedup(value) {
1365
+ return value.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
1366
+ }
1367
+ function cosineSim(a, b) {
1368
+ if (a.length !== b.length)
1369
+ return 0;
1370
+ let dot = 0;
1371
+ let na = 0;
1372
+ let nb = 0;
1373
+ for (let i = 0; i < a.length; i++) {
1374
+ dot += a[i] * b[i];
1375
+ na += a[i] * a[i];
1376
+ nb += b[i] * b[i];
1377
+ }
1378
+ if (na === 0 || nb === 0)
1379
+ return 0;
1380
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
1381
+ }
1382
+ var DEFAULT_DEDUP_THRESHOLD = 0.92;
1383
+ function dedupe(items, getText, getVector, threshold = DEFAULT_DEDUP_THRESHOLD) {
1384
+ if (items.length <= 1)
1385
+ return { kept: [...items], merged: 0 };
1386
+ const kept = [];
1387
+ const keptNorm = [];
1388
+ const keptVec = [];
1389
+ let merged = 0;
1390
+ for (const item of items) {
1391
+ const norm = normalizeForDedup(getText(item));
1392
+ const vec = getVector(item);
1393
+ let isDup = false;
1394
+ for (let i = 0; i < kept.length; i++) {
1395
+ if (norm.length > 0 && norm === keptNorm[i]) {
1396
+ isDup = true;
1397
+ break;
1398
+ }
1399
+ const kv = keptVec[i];
1400
+ if (threshold > 0 && vec !== void 0 && kv !== void 0 && cosineSim(vec, kv) >= threshold) {
1401
+ isDup = true;
1402
+ break;
1403
+ }
1404
+ }
1405
+ if (isDup) {
1406
+ merged += 1;
1407
+ continue;
1408
+ }
1409
+ kept.push(item);
1410
+ keptNorm.push(norm);
1411
+ keptVec.push(vec);
1412
+ }
1413
+ return { kept, merged };
1414
+ }
1415
+
1363
1416
  // dist/types.js
1364
1417
  var MEMORY_SCOPES = [
1365
1418
  "system",
@@ -1535,6 +1588,8 @@ var SqliteMemoryBackend = class {
1535
1588
  rewriter;
1536
1589
  temporalEnabled;
1537
1590
  temporalWeight;
1591
+ dedupEnabled;
1592
+ dedupThreshold;
1538
1593
  /** Record ids whose vector is missing/stale, awaiting a batched embed. */
1539
1594
  pendingEmbed = /* @__PURE__ */ new Set();
1540
1595
  closed = false;
@@ -1553,6 +1608,8 @@ var SqliteMemoryBackend = class {
1553
1608
  this.rewriter = opts.rewriter;
1554
1609
  this.temporalEnabled = opts.temporal?.enabled ?? false;
1555
1610
  this.temporalWeight = opts.temporal?.weight ?? DEFAULT_TEMPORAL_WEIGHT;
1611
+ this.dedupEnabled = opts.dedup?.enabled ?? false;
1612
+ this.dedupThreshold = opts.dedup?.threshold ?? DEFAULT_DEDUP_THRESHOLD;
1556
1613
  if (this.embedder !== void 0)
1557
1614
  this.queueUnembedded();
1558
1615
  }
@@ -1597,6 +1654,35 @@ var SqliteMemoryBackend = class {
1597
1654
  const row = this.db.prepare(`SELECT * FROM agent_memory WHERE id = ?`).get(id);
1598
1655
  return rowToRecord(row);
1599
1656
  }
1657
+ /** Fetch stored vectors for a set of record ids (missing → absent from map). */
1658
+ vectorsByIds(ids) {
1659
+ const out = /* @__PURE__ */ new Map();
1660
+ if (ids.length === 0)
1661
+ return out;
1662
+ const placeholders = ids.map(() => "?").join(", ");
1663
+ const rows = this.db.prepare(`SELECT record_id, vector FROM agent_memory_vec WHERE record_id IN (${placeholders})`).all(...ids);
1664
+ for (const r of rows)
1665
+ out.set(r.record_id, blobToVector(r.vector));
1666
+ return out;
1667
+ }
1668
+ /**
1669
+ * M6: collapse near-duplicate records, keeping the first (highest-priority).
1670
+ * No-op when dedup is disabled or there is ≤1 record. Uses stored vectors for
1671
+ * the cosine signal when present (text-equality always applies).
1672
+ */
1673
+ dedupeRecords(records) {
1674
+ if (!this.dedupEnabled || records.length <= 1)
1675
+ return records;
1676
+ const vectors = this.vectorsByIds(records.map((r) => r.id));
1677
+ return dedupe(records, (r) => r.value, (r) => vectors.get(r.id), this.dedupThreshold).kept;
1678
+ }
1679
+ /** As {@link dedupeRecords}, but over search hits (caller gates on the flag). */
1680
+ dedupeHitPool(hits) {
1681
+ if (hits.length <= 1)
1682
+ return hits;
1683
+ const vectors = this.vectorsByIds(hits.map((h) => h.record.id));
1684
+ return dedupe(hits, (h) => h.record.value, (h) => vectors.get(h.record.id), this.dedupThreshold).kept;
1685
+ }
1600
1686
  async read(query) {
1601
1687
  if (query.chain.length === 0)
1602
1688
  return [];
@@ -1610,7 +1696,8 @@ var SqliteMemoryBackend = class {
1610
1696
  sql += ` ORDER BY updated_at DESC`;
1611
1697
  const rows = this.db.prepare(sql).all(...params);
1612
1698
  const resolved = resolveScopeChain(rows.map(rowToRecord), query.chain);
1613
- const limited = query.limit !== void 0 ? resolved.slice(0, query.limit) : resolved;
1699
+ const deduped = this.dedupeRecords(resolved);
1700
+ const limited = query.limit !== void 0 ? deduped.slice(0, query.limit) : deduped;
1614
1701
  const bump = this.db.prepare(`UPDATE agent_memory SET use_count = use_count + 1 WHERE id = ?`);
1615
1702
  for (const rec of limited) {
1616
1703
  bump.run(rec.id);
@@ -1675,7 +1762,9 @@ var SqliteMemoryBackend = class {
1675
1762
  { id: "vec", items: vecHits.map((h) => ({ key: h.record.id, payload: h })) }
1676
1763
  ], { limit: poolLimit }).map((f) => f.payload);
1677
1764
  }
1678
- const reranked = input.rerank === false ? pool : await this.rerankPool(input.query, pool, finalLimit);
1765
+ const dedupOn = input.dedup ?? this.dedupEnabled;
1766
+ const deduped = dedupOn ? this.dedupeHitPool(pool) : pool;
1767
+ const reranked = input.rerank === false ? deduped : await this.rerankPool(input.query, deduped, finalLimit);
1679
1768
  const temporalOn = input.temporal ?? this.temporalEnabled;
1680
1769
  const ordered = temporalOn ? this.applyTemporal(input.query, reranked) : reranked;
1681
1770
  return ordered.slice(0, finalLimit);
@@ -2168,6 +2257,18 @@ function discoverMemoryTemporal(env) {
2168
2257
  }
2169
2258
  return { enabled, weight };
2170
2259
  }
2260
+ function discoverMemoryDedup(env) {
2261
+ const raw = (env.PROMETHEUS_MEMORY_DEDUP ?? "off").toLowerCase();
2262
+ const enabled = raw === "on" || raw === "1" || raw === "true" || raw === "yes";
2263
+ const rawThreshold = env.PROMETHEUS_MEMORY_DEDUP_THRESHOLD;
2264
+ let threshold = DEFAULT_DEDUP_THRESHOLD;
2265
+ if (rawThreshold !== void 0 && rawThreshold !== "") {
2266
+ const n = Number.parseFloat(rawThreshold);
2267
+ if (Number.isFinite(n) && n > 0 && n <= 1)
2268
+ threshold = n;
2269
+ }
2270
+ return { enabled, threshold };
2271
+ }
2171
2272
  function composeFromEnv(opts) {
2172
2273
  const env = opts.env;
2173
2274
  const override = (opts.workspaceRootOverride ?? "").trim();
@@ -2183,11 +2284,13 @@ function composeFromEnv(opts) {
2183
2284
  const { id: extractorId, provider: extractor } = discoverMemoryExtractor(env);
2184
2285
  const { id: rewriterId, provider: rewriter } = discoverMemoryRewriter(env);
2185
2286
  const temporal = discoverMemoryTemporal(env);
2287
+ const dedup = discoverMemoryDedup(env);
2186
2288
  const backend = new SqliteMemoryBackend(dbPath, {
2187
2289
  ...embedder !== void 0 ? { embedder } : {},
2188
2290
  ...reranker !== null ? { reranker } : {},
2189
2291
  ...rewriter !== null ? { rewriter } : {},
2190
- temporal
2292
+ temporal,
2293
+ dedup
2191
2294
  });
2192
2295
  return {
2193
2296
  backend,
@@ -2205,6 +2308,7 @@ function composeFromEnv(opts) {
2205
2308
  rewriter,
2206
2309
  rewriterId,
2207
2310
  temporalEnabled: temporal.enabled,
2311
+ dedupEnabled: dedup.enabled,
2208
2312
  rootIsHomeOrFsRoot: isHomeOrFilesystemRoot(workspaceRoot),
2209
2313
  close: () => backend.close()
2210
2314
  };
@@ -2832,6 +2936,7 @@ ${f.value}`);
2832
2936
  rerank: deps.rerankerId,
2833
2937
  rewrite: deps.rewriterId,
2834
2938
  temporal: deps.temporalEnabled,
2939
+ dedup: deps.dedupEnabled,
2835
2940
  extract: deps.extractorId
2836
2941
  },
2837
2942
  summary
@@ -2882,7 +2987,7 @@ async function main() {
2882
2987
  env,
2883
2988
  ...override !== void 0 && override !== "" ? { workspaceRootOverride: override } : {}
2884
2989
  });
2885
- process.stderr.write(`prometheus-memory-mcp: workspace=${composed.workspaceRoot} (via ${via}) project=${composed.projectName} (${composed.projectId}) db=${composed.dbPath} embed=${composed.embedderId}${composed.embeddingsEnabled ? "" : " (keyword-only)"} rerank=${composed.rerankerId} extract=${composed.extractorId} rewrite=${composed.rewriterId} temporal=${composed.temporalEnabled ? "on" : "off"}
2990
+ process.stderr.write(`prometheus-memory-mcp: workspace=${composed.workspaceRoot} (via ${via}) project=${composed.projectName} (${composed.projectId}) db=${composed.dbPath} embed=${composed.embedderId}${composed.embeddingsEnabled ? "" : " (keyword-only)"} rerank=${composed.rerankerId} extract=${composed.extractorId} rewrite=${composed.rewriterId} temporal=${composed.temporalEnabled ? "on" : "off"} dedup=${composed.dedupEnabled ? "on" : "off"}
2886
2991
  `);
2887
2992
  if (composed.rootIsHomeOrFsRoot) {
2888
2993
  process.stderr.write(`prometheus-memory-mcp: workspace resolved to ${composed.workspaceRoot} (your home directory or a filesystem root) \u2014 project memories will NOT be mirrored to markdown there. Open a project folder (Claude Code passes it via CLAUDE_PROJECT_DIR) or set PROMETHEUS_WORKSPACE_ROOT. Call memory_status for details.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prom.codes/memory-mcp",
3
- "version": "0.7.3",
3
+ "version": "0.8.0",
4
4
  "description": "prom.codes Memory — persistent, local-first agent memory as an MCP server.",
5
5
  "type": "module",
6
6
  "bin": {