@nxuss/lemma 1.24.1 → 1.26.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.
@@ -8,10 +8,18 @@ exports.storePattern = storePattern;
8
8
  exports.getPatternStats = getPatternStats;
9
9
  const fs_1 = __importDefault(require("fs"));
10
10
  const path_1 = __importDefault(require("path"));
11
+ const scrubForStorage_1 = require("../security/scrubForStorage");
11
12
  const CACHE_FILE = '.lemma/prompt-patterns.json';
13
+ /** A lock older than this is assumed to belong to a process that died holding it. */
14
+ const LOCK_STALE_MS = 10000;
15
+ /** How long a writer waits for another process's lock before proceeding regardless. */
16
+ const LOCK_WAIT_MS = 2000;
12
17
  function getCachePath(projectRoot) {
13
18
  return path_1.default.join(projectRoot, CACHE_FILE);
14
19
  }
20
+ function getLockPath(projectRoot) {
21
+ return `${getCachePath(projectRoot)}.lock`;
22
+ }
15
23
  function loadCache(projectRoot) {
16
24
  try {
17
25
  const p = getCachePath(projectRoot);
@@ -31,14 +39,113 @@ function loadCache(projectRoot) {
31
39
  catch { }
32
40
  return { patterns: [], lastUpdated: Date.now() };
33
41
  }
42
+ /** Blocking sleep — callers here are synchronous and must not observe a half-merged cache. */
43
+ function sleepSync(ms) {
44
+ try {
45
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
46
+ }
47
+ catch {
48
+ const until = Date.now() + ms;
49
+ while (Date.now() < until) { /* busy wait */ }
50
+ }
51
+ }
52
+ /**
53
+ * Advisory cross-process write lock, same shape as TheBrainV2's: fail-open, never blocks
54
+ * forever. Every path through the loop body sleeps or returns before looping again — an
55
+ * `ENOENT` because the parent directory doesn't exist yet (as opposed to `EEXIST`, the only
56
+ * case that means "someone else holds it") must never be read as "retry immediately", or
57
+ * this spins at 100% CPU forever instead of backing off. Callers must create the parent
58
+ * directory before calling this.
59
+ */
60
+ function acquireLock(projectRoot) {
61
+ const lockPath = getLockPath(projectRoot);
62
+ const deadline = Date.now() + LOCK_WAIT_MS;
63
+ for (;;) {
64
+ try {
65
+ return fs_1.default.openSync(lockPath, 'wx');
66
+ }
67
+ catch (err) {
68
+ if (err?.code === 'EEXIST') {
69
+ try {
70
+ if (Date.now() - fs_1.default.statSync(lockPath).mtimeMs > LOCK_STALE_MS) {
71
+ fs_1.default.unlinkSync(lockPath);
72
+ continue; // freed a stale lock — retry immediately against the now-open path
73
+ }
74
+ }
75
+ catch {
76
+ continue; // lock vanished between open and stat — the holder just released it
77
+ }
78
+ }
79
+ if (Date.now() >= deadline)
80
+ return null;
81
+ sleepSync(15);
82
+ }
83
+ }
84
+ }
85
+ function releaseLock(projectRoot, fd) {
86
+ if (fd === null)
87
+ return;
88
+ try {
89
+ fs_1.default.closeSync(fd);
90
+ }
91
+ catch { }
92
+ try {
93
+ fs_1.default.unlinkSync(getLockPath(projectRoot));
94
+ }
95
+ catch { }
96
+ }
97
+ function writeFileAtomic(target, data) {
98
+ const tmp = `${target}.tmp.${process.pid}.${Date.now().toString(36)}`;
99
+ try {
100
+ fs_1.default.writeFileSync(tmp, data, 'utf8');
101
+ fs_1.default.renameSync(tmp, target);
102
+ }
103
+ catch (err) {
104
+ try {
105
+ fs_1.default.unlinkSync(tmp);
106
+ }
107
+ catch { }
108
+ throw err;
109
+ }
110
+ }
111
+ /**
112
+ * Merge `patterns` (the caller's in-memory view, already mutated) onto whatever is on disk
113
+ * right now, under the write lock — a blind overwrite would erase patterns a concurrent
114
+ * session stored since this process last loaded. Same fix TheBrainV2 applies to entries.ndjson.
115
+ */
116
+ function mergePatterns(base, onDisk) {
117
+ const byId = new Map();
118
+ for (const p of onDisk)
119
+ byId.set(p.id, p);
120
+ for (const p of base)
121
+ byId.set(p.id, p); // in-memory wins for ids both sides know about
122
+ return Array.from(byId.values())
123
+ .sort((a, b) => (b.hitCount * b.avgTokensSaved) - (a.hitCount * a.avgTokensSaved))
124
+ .slice(0, 50);
125
+ }
34
126
  function saveCache(projectRoot, data) {
127
+ const dir = path_1.default.join(projectRoot, '.lemma');
128
+ let lockFd = null;
35
129
  try {
36
- const dir = path_1.default.join(projectRoot, '.lemma');
130
+ // Must exist before acquireLock: opening a lock file under a missing directory throws
131
+ // ENOENT on every attempt, not just the first, which is exactly the busy-loop acquireLock
132
+ // is written to avoid mistaking for lock contention.
37
133
  if (!fs_1.default.existsSync(dir))
38
134
  fs_1.default.mkdirSync(dir, { recursive: true });
39
- fs_1.default.writeFileSync(getCachePath(projectRoot), JSON.stringify(data, null, 2), 'utf8');
135
+ lockFd = acquireLock(projectRoot);
136
+ const onDisk = loadCache(projectRoot);
137
+ const merged = {
138
+ patterns: mergePatterns(data.patterns, onDisk.patterns),
139
+ lastUpdated: Date.now(),
140
+ };
141
+ writeFileAtomic(getCachePath(projectRoot), JSON.stringify(merged, null, 2));
142
+ }
143
+ catch {
144
+ // Persistence is best-effort — a failed save must not break find/store for the caller.
145
+ }
146
+ finally {
147
+ releaseLock(projectRoot, lockFd);
40
148
  }
41
- catch { }
42
149
  }
43
150
  function extractKeywords(text) {
44
151
  return text.toLowerCase()
@@ -79,17 +186,21 @@ function storePattern(query, template, tokensSaved, projectRoot) {
79
186
  const cache = loadCache(projectRoot);
80
187
  const keywords = extractKeywords(query);
81
188
  const id = `pattern-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
189
+ // Templates and their source query can carry pasted secrets — redact before they ever
190
+ // touch disk, the same policy TheBrainV2 applies to every stored query/response/claim.
191
+ const scrubbedTemplate = (0, scrubForStorage_1.scrubSecretsForStorage)(template).text;
82
192
  const existing = cache.patterns.find(p => matchScore(keywords, p.keywords) > 0.7);
83
193
  if (existing) {
84
194
  existing.hitCount++;
85
195
  existing.avgTokensSaved = (existing.avgTokensSaved + tokensSaved) / 2;
196
+ existing.template = scrubbedTemplate;
86
197
  saveCache(projectRoot, cache);
87
198
  return existing;
88
199
  }
89
200
  const pattern = {
90
201
  id,
91
202
  keywords,
92
- template,
203
+ template: scrubbedTemplate,
93
204
  hitCount: 1,
94
205
  avgTokensSaved: tokensSaved,
95
206
  };
@@ -1 +1 @@
1
- {"version":3,"file":"PromptPatternCache.js","sourceRoot":"","sources":["../../../src/utils/PromptPatternCache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PromptPatternCache.js","sourceRoot":"","sources":["../../../src/utils/PromptPatternCache.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tools/memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhE,eAAO,MAAM,uBAAuB,EAAE,cAAc,EAoTnD,CAAC;AAgrBF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAe5D,CAAC"}
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../../../src/mcp/tools/memory.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAYH,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhE,eAAO,MAAM,uBAAuB,EAAE,cAAc,EAoUnD,CAAC;AAmxBF,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAgB5D,CAAC"}
@@ -14,6 +14,21 @@ import { logError, safeResolvePath, DEFAULT_MEMORY_SIMILARITY_FLOOR, tryElicitCo
14
14
  import { deriveDomainForFile } from "../cognitiveMap.js";
15
15
  import { getRecentEvidence } from "../../utils/SessionEvidence.js";
16
16
  export const MEMORY_TOOL_DEFINITIONS = [
17
+ {
18
+ name: "list_memories",
19
+ annotations: { readOnlyHint: true, openWorldHint: false },
20
+ description: "Browse stored memories by project/domain/outcome, newest first — no query string, no BM25 ranking, no similarity floor. Use this for 'what do you have stored about X project' instead of forcing that into a search_memory phrasing, or to see the ids a prior brain_upkeep batch referred to. Returns short summaries (id, truncated query, hits, tokensSaved) — call get_memory for a full entry.",
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: {
24
+ projectId: { type: "string", description: "Only list memories from this project. Defaults to the current project; pass an empty string to list across all projects." },
25
+ domain: { type: "string", description: "Only list memories tagged with this domain." },
26
+ outcome: { type: "string", enum: ["confirmed", "failed"], description: "Only list memories with this outcome." },
27
+ limit: { type: "number", description: "Max entries to return (1-200).", default: 25 },
28
+ offset: { type: "number", description: "Skip this many entries (for paging past `limit`).", default: 0 },
29
+ },
30
+ },
31
+ },
17
32
  {
18
33
  name: "search_memory",
19
34
  annotations: { readOnlyHint: true, openWorldHint: false },
@@ -245,14 +260,15 @@ export const MEMORY_TOOL_DEFINITIONS = [
245
260
  },
246
261
  {
247
262
  name: "brain_upkeep",
248
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
249
- description: "Tend the Brain in one read-only call: revalidates the most-reused memories, lists duplicates/merge candidates/dead weight/savings, each with its fix (refresh_memory, brain_merge, forget_memory). Never deletes or rewrites anything. Run when returning to a repo after a while.",
263
+ annotations: { readOnlyHint: false, idempotentHint: false, openWorldHint: false },
264
+ description: "AUTO-ANCHORS evidence-free memories naming real repo files: snapshots first, then re-anchors them (undo via brain_trash restore-snapshot); autoAnchor:false = read-only. Lists duplicates, dead weight, savings with fixes. Run on returning to a repo.",
250
265
  inputSchema: {
251
266
  type: "object",
252
267
  properties: {
253
- topN: { type: "number", description: "How many of the most-reused memories to revalidate. Default 20.", default: 20 },
254
- sample: { type: "number", description: "How many entries to sweep for duplicates. Default 50 (bounded: each issues one internal search).", default: 50 },
255
- deep: { type: "boolean", description: "Also re-hash all tracked evidence for a corpus-wide stale ratio. Slow on large Brains.", default: false },
268
+ topN: { type: "number", description: "Top memories to revalidate. Default 20.", default: 20 },
269
+ sample: { type: "number", description: "Entries to sweep for duplicates. Default 50.", default: 50 },
270
+ deep: { type: "boolean", description: "Re-hash all evidence for a stale ratio. Slow.", default: false },
271
+ autoAnchor: { type: "boolean", description: "Re-anchor untracked memories naming real repo files (snapshot first). Default true.", default: true },
256
272
  },
257
273
  },
258
274
  },
@@ -264,6 +280,7 @@ export const MEMORY_TOOL_DEFINITIONS = [
264
280
  type: "object",
265
281
  properties: {
266
282
  id: { type: "string", description: "The memory's id, from a search_memory or verify_memory result." },
283
+ ids: { type: "array", items: { type: "string" }, description: "Verified ids to refresh in one call, instead of id." },
267
284
  filePaths: { type: "array", items: { type: "string" }, description: "New files to track instead of the current ones. Omit to re-hash whatever it already tracks." },
268
285
  symbols: {
269
286
  type: "array",
@@ -278,7 +295,6 @@ export const MEMORY_TOOL_DEFINITIONS = [
278
295
  description: "New symbols to track instead of the current ones (e.g. the function was renamed or moved).",
279
296
  },
280
297
  },
281
- required: ["id"],
282
298
  },
283
299
  },
284
300
  {
@@ -349,8 +365,12 @@ async function handleSearchMemory(args) {
349
365
  }],
350
366
  };
351
367
  }
352
- const fresh = results.filter((r) => r.fresh);
368
+ // An entry that tracks nothing reports `fresh: true` vacuously (checkEntryFreshness has
369
+ // nothing to compare) — it must NOT land in the verified-fresh bucket, be credited as a
370
+ // cache hit, or be told "from cache". It gets its own UNTRACKED bucket instead.
371
+ const fresh = results.filter((r) => r.fresh && !r.untracked);
353
372
  const stale = results.filter((r) => !r.fresh);
373
+ const untracked = results.filter((r) => r.fresh && r.untracked);
354
374
  const formatClaims = (r) => {
355
375
  if (!Array.isArray(r.claims) || r.claims.length === 0)
356
376
  return "";
@@ -389,6 +409,12 @@ async function handleSearchMemory(args) {
389
409
  memoryId: bestFresh.id,
390
410
  });
391
411
  }
412
+ else if (untracked.length > 0) {
413
+ recordReceiptEvent("reasoning", query.substring(0, 100), {
414
+ tool: "search_memory",
415
+ reason: `${untracked.length} similar but UNTRACKED — no evidence to verify, not credited as a cache hit`,
416
+ });
417
+ }
392
418
  else {
393
419
  recordReceiptEvent("reasoning", query.substring(0, 100), {
394
420
  tool: "search_memory",
@@ -399,6 +425,12 @@ async function handleSearchMemory(args) {
399
425
  if (fresh.length > 0) {
400
426
  parts.push(`Lemma found ${fresh.length} fresh ${fresh.length === 1 ? "memory" : "memories"} (from cache — say so if you use this):\n\n${fresh.map(formatResult).join("\n\n---\n\n")}`);
401
427
  }
428
+ if (untracked.length > 0) {
429
+ const untrackedText = untracked
430
+ .map((r, i) => formatResult(r, i))
431
+ .join("\n\n---\n\n");
432
+ parts.push(`⚠️ ${untracked.length} ${untracked.length === 1 ? "memory is" : "memories are"} similar but UNTRACKED — ${untracked.length === 1 ? "it tracks" : "they track"} no files or symbols, so nothing was verified against current code (${untracked.length === 1 ? "it is" : "they are"} vacuously "fresh": there was nothing to check). ${untracked.length === 1 ? "It may" : "They may"} describe state that no longer exists — treat as a hint, re-verify against the repo before acting on it, and use update_memory/refresh_memory with filePaths/symbols so it becomes verifiable next time:\n\n${untrackedText}`);
433
+ }
402
434
  if (stale.length > 0) {
403
435
  const staleText = stale
404
436
  .map((r, i) => {
@@ -747,21 +779,54 @@ async function handleBrainMerge(args) {
747
779
  }
748
780
  async function handleBrainUpkeep(args) {
749
781
  try {
782
+ const autoAnchor = args?.autoAnchor !== false;
750
783
  const report = getBrain().upkeep({
751
784
  topN: typeof args?.topN === "number" ? args.topN : undefined,
752
785
  sample: typeof args?.sample === "number" ? args.sample : undefined,
753
786
  deep: args?.deep === true,
754
787
  });
788
+ // Auto-anchor pass: an untracked memory that names real repo files is re-anchored to
789
+ // them, snapshotted first so the whole pass is reversible via brain_trash
790
+ // restore-snapshot. Anything the extractor cannot find a path for stays manual.
791
+ const anchored = [];
792
+ let snapshotName = null;
793
+ const anchorable = report.revalidated.untracked.filter((u) => u.anchorCandidates && u.anchorCandidates.length > 0);
794
+ if (autoAnchor && anchorable.length > 0) {
795
+ const brain = getBrain();
796
+ snapshotName = brain.snapshotCorpus("auto-anchor");
797
+ for (const u of anchorable) {
798
+ try {
799
+ const res = brain.refresh(u.id, { filePaths: u.anchorCandidates });
800
+ const tracked = (res.retracked || []).join(", ");
801
+ anchored.push({
802
+ ok: res.ok,
803
+ id: u.id,
804
+ detail: `${res.message}${tracked ? ` Now tracking: ${tracked}` : ""}`,
805
+ });
806
+ }
807
+ catch (e) {
808
+ anchored.push({ ok: false, id: u.id, detail: e.message });
809
+ }
810
+ }
811
+ }
812
+ const anchoredIds = new Set(anchored.filter((a) => a.ok).map((a) => a.id));
813
+ const stillUntracked = report.revalidated.untracked.filter((u) => !anchoredIds.has(u.id));
755
814
  const lines = [];
756
815
  lines.push(`# Brain upkeep (${report.totalEntries} entries, checked ${report.checkedAt})`);
757
816
  const r = report.revalidated;
758
817
  lines.push(`Revalidated top ${r.checked}: ${r.fresh.length} fresh, ${r.stale.length} stale, ${r.untracked.length} untracked, ${r.unverified.length} unverified.`);
759
818
  for (const s of r.stale)
760
819
  lines.push(` ✗ STALE ${s.id}: ${s.query} (changed: ${s.staleFiles.join(", ") || "upstream memory"}) → ${s.action}`);
761
- for (const u of r.untracked)
820
+ for (const u of stillUntracked)
762
821
  lines.push(` ? UNTRACKED ${u.id}: ${u.query} → ${u.action}`);
763
822
  for (const u of r.unverified)
764
823
  lines.push(` ? UNVERIFIED ${u.id}: ${u.query} → ${u.action}`);
824
+ if (anchored.length > 0) {
825
+ lines.push(`Auto-anchored ${anchored.length} untracked ${anchored.length === 1 ? "memory" : "memories"} → now verifiable${snapshotName ? ` (snapshot: ${snapshotName}; undo with brain_trash restore-snapshot)` : ""}:`);
826
+ for (const a of anchored) {
827
+ lines.push(` ${a.ok ? "✔" : "✗"} ${a.id}: ${a.detail}`);
828
+ }
829
+ }
765
830
  if (report.duplicatePairs.length > 0) {
766
831
  lines.push(`Exact duplicates (${report.duplicatePairs.length}):`);
767
832
  for (const d of report.duplicatePairs) {
@@ -794,20 +859,38 @@ async function handleBrainUpkeep(args) {
794
859
  }
795
860
  async function handleRefreshMemory(args) {
796
861
  const id = args?.id;
797
- if (!id)
798
- throw new Error("id is required");
862
+ const ids = Array.isArray(args?.ids) ? args.ids.filter((v) => typeof v === "string" && v) : undefined;
863
+ if (!id && (!ids || ids.length === 0))
864
+ throw new Error("id or ids is required");
865
+ const formatOne = (oneId, result) => {
866
+ if (!result.ok)
867
+ return `${oneId}: ${result.message}`;
868
+ const retrackedNote = result.retracked && result.retracked.length > 0
869
+ ? `Now tracking: ${result.retracked.join(", ")}`
870
+ : "Tracks no files or symbols, so there was nothing to re-anchor — its age clock was reset.";
871
+ return `${result.message}\n${retrackedNote}`;
872
+ };
873
+ // A batch call re-hashes current tracking only — filePaths/symbols re-target a single
874
+ // memory's anchors and have no sensible meaning applied identically across several ids.
875
+ if (ids && ids.length > 0) {
876
+ const brain = getBrain();
877
+ const lines = ids.map((oneId) => {
878
+ try {
879
+ return formatOne(oneId, brain.refresh(oneId, {}));
880
+ }
881
+ catch (e) {
882
+ return `${oneId}: failed — ${e.message}`;
883
+ }
884
+ });
885
+ return { content: [{ type: "text", text: lines.join("\n\n") }] };
886
+ }
799
887
  const filePaths = Array.isArray(args?.filePaths) ? args.filePaths.filter((p) => typeof p === "string" && p) : undefined;
800
888
  const symbols = Array.isArray(args?.symbols)
801
889
  ? args.symbols.filter((sym) => sym?.filePath && sym?.symbolName)
802
890
  : undefined;
803
891
  try {
804
892
  const result = getBrain().refresh(id, { filePaths, symbols });
805
- if (!result.ok)
806
- return { content: [{ type: "text", text: result.message }] };
807
- const retrackedNote = result.retracked && result.retracked.length > 0
808
- ? `\nNow tracking: ${result.retracked.join(", ")}`
809
- : "\nThis memory tracks no files or symbols, so there was nothing to re-anchor — its age clock was reset.";
810
- return { content: [{ type: "text", text: `${result.message}${retrackedNote}` }] };
893
+ return { content: [{ type: "text", text: formatOne(id, result) }] };
811
894
  }
812
895
  catch (e) {
813
896
  logError("refresh_memory", e);
@@ -960,7 +1043,31 @@ async function handleVerifyMemory(args) {
960
1043
  return { content: [{ type: "text", text: `Failed to verify memory: ${e.message}` }] };
961
1044
  }
962
1045
  }
1046
+ async function handleListMemories(args) {
1047
+ // "" means "every project" — deriveProjectId() never returns "", so it can't collide
1048
+ // with the default-to-current-project behavior below.
1049
+ const projectId = args?.projectId === "" ? undefined : (typeof args?.projectId === "string" ? args.projectId : deriveProjectId());
1050
+ const domain = typeof args?.domain === "string" && args.domain.trim() ? args.domain.trim() : undefined;
1051
+ const outcome = args?.outcome === "confirmed" || args?.outcome === "failed" ? args.outcome : undefined;
1052
+ const limit = typeof args?.limit === "number" ? args.limit : 25;
1053
+ const offset = typeof args?.offset === "number" ? args.offset : 0;
1054
+ try {
1055
+ const { total, items } = getBrain().list({ projectId, domain, outcome, limit, offset });
1056
+ if (items.length === 0) {
1057
+ return { content: [{ type: "text", text: `No memories found for this filter (${total} total match before paging).` }] };
1058
+ }
1059
+ const lines = items.map((it) => `${it.id} (${it.timestamp}${it.domain ? `, domain: ${it.domain}` : ""}${it.outcome === "failed" ? ", FAILED" : ""}) ` +
1060
+ `hits: ${it.hits}${it.tokensSaved ? `, tokensSaved: ${it.tokensSaved}` : ""}\n ${it.query}`);
1061
+ const pageNote = `Showing ${items.length} of ${total}${offset > 0 ? ` (offset ${offset})` : ""}.`;
1062
+ return { content: [{ type: "text", text: `${pageNote}\n\n${lines.join("\n\n")}` }] };
1063
+ }
1064
+ catch (e) {
1065
+ logError("list_memories", e);
1066
+ return { content: [{ type: "text", text: `Failed to list memories: ${e.message}` }] };
1067
+ }
1068
+ }
963
1069
  export const MEMORY_TOOL_HANDLERS = {
1070
+ list_memories: handleListMemories,
964
1071
  search_memory: handleSearchMemory,
965
1072
  get_memory: handleGetMemory,
966
1073
  store_memory: handleStoreMemory,