@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.
- package/README.md +20 -9
- package/dist/cjs/mcp/tools/memory.d.ts.map +1 -1
- package/dist/cjs/mcp/tools/memory.js +123 -16
- package/dist/cjs/mcp/tools/memory.js.map +1 -1
- package/dist/cjs/mcp/tools.js +1 -1
- package/dist/cjs/mcp/tools.js.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.d.ts +46 -0
- package/dist/cjs/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/cjs/subconscious/TheBrainV2.js +113 -10
- package/dist/cjs/subconscious/TheBrainV2.js.map +1 -1
- package/dist/cjs/utils/PromptPatternCache.d.ts.map +1 -1
- package/dist/cjs/utils/PromptPatternCache.js +115 -4
- package/dist/cjs/utils/PromptPatternCache.js.map +1 -1
- package/dist/esm/mcp/tools/memory.d.ts.map +1 -1
- package/dist/esm/mcp/tools/memory.js +123 -16
- package/dist/esm/mcp/tools/memory.js.map +1 -1
- package/dist/esm/mcp/tools.js +1 -1
- package/dist/esm/mcp/tools.js.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.d.ts +46 -0
- package/dist/esm/subconscious/TheBrainV2.d.ts.map +1 -1
- package/dist/esm/subconscious/TheBrainV2.js +112 -10
- package/dist/esm/subconscious/TheBrainV2.js.map +1 -1
- package/dist/esm/utils/PromptPatternCache.d.ts.map +1 -1
- package/dist/esm/utils/PromptPatternCache.js +115 -4
- package/dist/esm/utils/PromptPatternCache.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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":";;;;;
|
|
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,
|
|
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:
|
|
249
|
-
description: "
|
|
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: "
|
|
254
|
-
sample: { type: "number", description: "
|
|
255
|
-
deep: { type: "boolean", description: "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
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,
|