@velvetmonkey/flywheel-memory 2.0.54 → 2.0.55
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/dist/index.js +89 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3832,9 +3832,14 @@ function trackWikilinkApplications(stateDb2, notePath, entities) {
|
|
|
3832
3832
|
applied_at = datetime('now'),
|
|
3833
3833
|
status = 'applied'
|
|
3834
3834
|
`);
|
|
3835
|
+
const lookupCanonical = stateDb2.db.prepare(
|
|
3836
|
+
`SELECT name FROM entities WHERE LOWER(name) = LOWER(?) LIMIT 1`
|
|
3837
|
+
);
|
|
3835
3838
|
const transaction = stateDb2.db.transaction(() => {
|
|
3836
3839
|
for (const entity of entities) {
|
|
3837
|
-
|
|
3840
|
+
const row = lookupCanonical.get(entity);
|
|
3841
|
+
const canonicalName = row?.name ?? entity;
|
|
3842
|
+
upsert.run(canonicalName, notePath);
|
|
3838
3843
|
}
|
|
3839
3844
|
});
|
|
3840
3845
|
transaction();
|
|
@@ -5496,13 +5501,13 @@ var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
|
|
|
5496
5501
|
".claude",
|
|
5497
5502
|
".git"
|
|
5498
5503
|
]);
|
|
5499
|
-
function noteContainsEntity(content, entityName) {
|
|
5504
|
+
function noteContainsEntity(content, entityName, contentTokens) {
|
|
5500
5505
|
const entityTokens = tokenize(entityName);
|
|
5501
5506
|
if (entityTokens.length === 0) return false;
|
|
5502
|
-
const
|
|
5507
|
+
const tokens = contentTokens ?? new Set(tokenize(content));
|
|
5503
5508
|
let matchCount = 0;
|
|
5504
5509
|
for (const token of entityTokens) {
|
|
5505
|
-
if (
|
|
5510
|
+
if (tokens.has(token)) {
|
|
5506
5511
|
matchCount++;
|
|
5507
5512
|
}
|
|
5508
5513
|
}
|
|
@@ -5547,9 +5552,10 @@ async function mineCooccurrences(vaultPath2, entities, options = {}) {
|
|
|
5547
5552
|
try {
|
|
5548
5553
|
const content = await readFile2(file.path, "utf-8");
|
|
5549
5554
|
notesScanned++;
|
|
5555
|
+
const contentTokens = new Set(tokenize(content));
|
|
5550
5556
|
const mentionedEntities = [];
|
|
5551
5557
|
for (const entity of validEntities) {
|
|
5552
|
-
if (noteContainsEntity(content, entity)) {
|
|
5558
|
+
if (noteContainsEntity(content, entity, contentTokens)) {
|
|
5553
5559
|
mentionedEntities.push(entity);
|
|
5554
5560
|
}
|
|
5555
5561
|
}
|
|
@@ -8886,6 +8892,20 @@ function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
|
|
|
8886
8892
|
).run(cutoff);
|
|
8887
8893
|
return result.changes;
|
|
8888
8894
|
}
|
|
8895
|
+
function purgeOldSuggestionEvents(stateDb2, retentionDays = 30) {
|
|
8896
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
8897
|
+
const result = stateDb2.db.prepare(
|
|
8898
|
+
"DELETE FROM suggestion_events WHERE timestamp < ?"
|
|
8899
|
+
).run(cutoff);
|
|
8900
|
+
return result.changes;
|
|
8901
|
+
}
|
|
8902
|
+
function purgeOldNoteLinkHistory(stateDb2, retentionDays = 90) {
|
|
8903
|
+
const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
8904
|
+
const result = stateDb2.db.prepare(
|
|
8905
|
+
"DELETE FROM note_link_history WHERE last_positive_at < ?"
|
|
8906
|
+
).run(cutoff);
|
|
8907
|
+
return result.changes;
|
|
8908
|
+
}
|
|
8889
8909
|
|
|
8890
8910
|
// src/core/read/sweep.ts
|
|
8891
8911
|
var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -16750,6 +16770,60 @@ function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
|
|
|
16750
16770
|
"SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
|
|
16751
16771
|
).all(limit);
|
|
16752
16772
|
}
|
|
16773
|
+
function sweepExpiredMemories(stateDb2) {
|
|
16774
|
+
const now = Date.now();
|
|
16775
|
+
const msPerDay = 864e5;
|
|
16776
|
+
const expired = stateDb2.db.prepare(`
|
|
16777
|
+
SELECT key FROM memories
|
|
16778
|
+
WHERE ttl_days IS NOT NULL
|
|
16779
|
+
AND superseded_by IS NULL
|
|
16780
|
+
AND (created_at + (ttl_days * ?)) < ?
|
|
16781
|
+
`).all(msPerDay, now);
|
|
16782
|
+
for (const { key } of expired) {
|
|
16783
|
+
removeGraphSignals(stateDb2, key);
|
|
16784
|
+
}
|
|
16785
|
+
const result = stateDb2.db.prepare(`
|
|
16786
|
+
DELETE FROM memories
|
|
16787
|
+
WHERE ttl_days IS NOT NULL
|
|
16788
|
+
AND superseded_by IS NULL
|
|
16789
|
+
AND (created_at + (ttl_days * ?)) < ?
|
|
16790
|
+
`).run(msPerDay, now);
|
|
16791
|
+
return result.changes;
|
|
16792
|
+
}
|
|
16793
|
+
function decayMemoryConfidence(stateDb2) {
|
|
16794
|
+
const now = Date.now();
|
|
16795
|
+
const msPerDay = 864e5;
|
|
16796
|
+
const halfLifeDays = 30;
|
|
16797
|
+
const lambda = Math.LN2 / (halfLifeDays * msPerDay);
|
|
16798
|
+
const staleThreshold = now - 7 * msPerDay;
|
|
16799
|
+
const staleMemories = stateDb2.db.prepare(`
|
|
16800
|
+
SELECT id, accessed_at, confidence FROM memories
|
|
16801
|
+
WHERE accessed_at < ? AND superseded_by IS NULL AND confidence > 0.1
|
|
16802
|
+
`).all(staleThreshold);
|
|
16803
|
+
let updated = 0;
|
|
16804
|
+
const updateStmt = stateDb2.db.prepare(
|
|
16805
|
+
"UPDATE memories SET confidence = ? WHERE id = ?"
|
|
16806
|
+
);
|
|
16807
|
+
for (const mem of staleMemories) {
|
|
16808
|
+
const ageDays = (now - mem.accessed_at) / msPerDay;
|
|
16809
|
+
const decayFactor = Math.exp(-lambda * ageDays * msPerDay);
|
|
16810
|
+
const newConfidence = Math.max(0.1, mem.confidence * decayFactor);
|
|
16811
|
+
if (Math.abs(newConfidence - mem.confidence) > 0.01) {
|
|
16812
|
+
updateStmt.run(newConfidence, mem.id);
|
|
16813
|
+
updated++;
|
|
16814
|
+
}
|
|
16815
|
+
}
|
|
16816
|
+
return updated;
|
|
16817
|
+
}
|
|
16818
|
+
function pruneSupersededMemories(stateDb2, retentionDays = 90) {
|
|
16819
|
+
const cutoff = Date.now() - retentionDays * 864e5;
|
|
16820
|
+
const result = stateDb2.db.prepare(`
|
|
16821
|
+
DELETE FROM memories
|
|
16822
|
+
WHERE superseded_by IS NOT NULL
|
|
16823
|
+
AND updated_at < ?
|
|
16824
|
+
`).run(cutoff);
|
|
16825
|
+
return result.changes;
|
|
16826
|
+
}
|
|
16753
16827
|
function findContradictions2(stateDb2, entity) {
|
|
16754
16828
|
const conditions = ["superseded_by IS NULL"];
|
|
16755
16829
|
const params = [];
|
|
@@ -19233,6 +19307,11 @@ async function runPostIndexWork(index) {
|
|
|
19233
19307
|
purgeOldMetrics(stateDb, 90);
|
|
19234
19308
|
purgeOldIndexEvents(stateDb, 90);
|
|
19235
19309
|
purgeOldInvocations(stateDb, 90);
|
|
19310
|
+
purgeOldSuggestionEvents(stateDb, 30);
|
|
19311
|
+
purgeOldNoteLinkHistory(stateDb, 90);
|
|
19312
|
+
sweepExpiredMemories(stateDb);
|
|
19313
|
+
decayMemoryConfidence(stateDb);
|
|
19314
|
+
pruneSupersededMemories(stateDb, 90);
|
|
19236
19315
|
serverLog("server", "Growth metrics recorded");
|
|
19237
19316
|
} catch (err) {
|
|
19238
19317
|
serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
|
|
@@ -19827,6 +19906,9 @@ async function runPostIndexWork(index) {
|
|
|
19827
19906
|
tracker.end({ tracked: trackedLinks, mentions: mentionResults });
|
|
19828
19907
|
serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files, ${mentionResults.reduce((s, m) => s + m.entities.length, 0)} unwikified mentions`);
|
|
19829
19908
|
tracker.start("implicit_feedback", { files: filteredEvents.length });
|
|
19909
|
+
const deletedFiles = new Set(
|
|
19910
|
+
filteredEvents.filter((e) => e.type === "delete").map((e) => e.path)
|
|
19911
|
+
);
|
|
19830
19912
|
const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
|
|
19831
19913
|
const feedbackResults = [];
|
|
19832
19914
|
if (stateDb) {
|
|
@@ -19842,6 +19924,7 @@ async function runPostIndexWork(index) {
|
|
|
19842
19924
|
}
|
|
19843
19925
|
if (stateDb && linkDiffs.length > 0) {
|
|
19844
19926
|
for (const diff of linkDiffs) {
|
|
19927
|
+
if (deletedFiles.has(diff.file)) continue;
|
|
19845
19928
|
for (const target of diff.removed) {
|
|
19846
19929
|
if (feedbackResults.some((r) => r.entity === target && r.file === diff.file)) continue;
|
|
19847
19930
|
const entity = entitiesAfter.find(
|
|
@@ -19860,6 +19943,7 @@ async function runPostIndexWork(index) {
|
|
|
19860
19943
|
`SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
|
|
19861
19944
|
);
|
|
19862
19945
|
for (const diff of linkDiffs) {
|
|
19946
|
+
if (deletedFiles.has(diff.file)) continue;
|
|
19863
19947
|
for (const target of diff.added) {
|
|
19864
19948
|
if (checkApplication.get(target, diff.file)) continue;
|
|
19865
19949
|
const entity = entitiesAfter.find(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.55",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
55
|
-
"@velvetmonkey/vault-core": "^2.0.
|
|
55
|
+
"@velvetmonkey/vault-core": "^2.0.55",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|