@velvetmonkey/flywheel-memory 2.0.54 → 2.0.56
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 +127 -8
- 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;
|
|
@@ -8944,13 +8964,20 @@ function runSweep(index) {
|
|
|
8944
8964
|
cachedResults = results;
|
|
8945
8965
|
return results;
|
|
8946
8966
|
}
|
|
8947
|
-
function startSweepTimer(getIndex, intervalMs) {
|
|
8967
|
+
function startSweepTimer(getIndex, intervalMs, maintenanceCallback) {
|
|
8948
8968
|
const interval = Math.max(intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS);
|
|
8949
8969
|
setTimeout(() => {
|
|
8950
8970
|
doSweep(getIndex);
|
|
8951
8971
|
}, 5e3);
|
|
8952
8972
|
sweepTimer = setInterval(() => {
|
|
8953
8973
|
doSweep(getIndex);
|
|
8974
|
+
if (maintenanceCallback) {
|
|
8975
|
+
try {
|
|
8976
|
+
maintenanceCallback();
|
|
8977
|
+
} catch (err) {
|
|
8978
|
+
console.error("[Flywheel] Maintenance error:", err);
|
|
8979
|
+
}
|
|
8980
|
+
}
|
|
8954
8981
|
}, interval);
|
|
8955
8982
|
if (sweepTimer && typeof sweepTimer === "object" && "unref" in sweepTimer) {
|
|
8956
8983
|
sweepTimer.unref();
|
|
@@ -8970,6 +8997,12 @@ function doSweep(getIndex) {
|
|
|
8970
8997
|
sweepRunning = false;
|
|
8971
8998
|
}
|
|
8972
8999
|
}
|
|
9000
|
+
function stopSweepTimer() {
|
|
9001
|
+
if (sweepTimer) {
|
|
9002
|
+
clearInterval(sweepTimer);
|
|
9003
|
+
sweepTimer = null;
|
|
9004
|
+
}
|
|
9005
|
+
}
|
|
8973
9006
|
function getSweepResults() {
|
|
8974
9007
|
return cachedResults;
|
|
8975
9008
|
}
|
|
@@ -16750,6 +16783,60 @@ function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
|
|
|
16750
16783
|
"SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
|
|
16751
16784
|
).all(limit);
|
|
16752
16785
|
}
|
|
16786
|
+
function sweepExpiredMemories(stateDb2) {
|
|
16787
|
+
const now = Date.now();
|
|
16788
|
+
const msPerDay = 864e5;
|
|
16789
|
+
const expired = stateDb2.db.prepare(`
|
|
16790
|
+
SELECT key FROM memories
|
|
16791
|
+
WHERE ttl_days IS NOT NULL
|
|
16792
|
+
AND superseded_by IS NULL
|
|
16793
|
+
AND (created_at + (ttl_days * ?)) < ?
|
|
16794
|
+
`).all(msPerDay, now);
|
|
16795
|
+
for (const { key } of expired) {
|
|
16796
|
+
removeGraphSignals(stateDb2, key);
|
|
16797
|
+
}
|
|
16798
|
+
const result = stateDb2.db.prepare(`
|
|
16799
|
+
DELETE FROM memories
|
|
16800
|
+
WHERE ttl_days IS NOT NULL
|
|
16801
|
+
AND superseded_by IS NULL
|
|
16802
|
+
AND (created_at + (ttl_days * ?)) < ?
|
|
16803
|
+
`).run(msPerDay, now);
|
|
16804
|
+
return result.changes;
|
|
16805
|
+
}
|
|
16806
|
+
function decayMemoryConfidence(stateDb2) {
|
|
16807
|
+
const now = Date.now();
|
|
16808
|
+
const msPerDay = 864e5;
|
|
16809
|
+
const halfLifeDays = 30;
|
|
16810
|
+
const lambda = Math.LN2 / (halfLifeDays * msPerDay);
|
|
16811
|
+
const staleThreshold = now - 7 * msPerDay;
|
|
16812
|
+
const staleMemories = stateDb2.db.prepare(`
|
|
16813
|
+
SELECT id, accessed_at, confidence FROM memories
|
|
16814
|
+
WHERE accessed_at < ? AND superseded_by IS NULL AND confidence > 0.1
|
|
16815
|
+
`).all(staleThreshold);
|
|
16816
|
+
let updated = 0;
|
|
16817
|
+
const updateStmt = stateDb2.db.prepare(
|
|
16818
|
+
"UPDATE memories SET confidence = ? WHERE id = ?"
|
|
16819
|
+
);
|
|
16820
|
+
for (const mem of staleMemories) {
|
|
16821
|
+
const ageDays = (now - mem.accessed_at) / msPerDay;
|
|
16822
|
+
const decayFactor = Math.exp(-lambda * ageDays * msPerDay);
|
|
16823
|
+
const newConfidence = Math.max(0.1, mem.confidence * decayFactor);
|
|
16824
|
+
if (Math.abs(newConfidence - mem.confidence) > 0.01) {
|
|
16825
|
+
updateStmt.run(newConfidence, mem.id);
|
|
16826
|
+
updated++;
|
|
16827
|
+
}
|
|
16828
|
+
}
|
|
16829
|
+
return updated;
|
|
16830
|
+
}
|
|
16831
|
+
function pruneSupersededMemories(stateDb2, retentionDays = 90) {
|
|
16832
|
+
const cutoff = Date.now() - retentionDays * 864e5;
|
|
16833
|
+
const result = stateDb2.db.prepare(`
|
|
16834
|
+
DELETE FROM memories
|
|
16835
|
+
WHERE superseded_by IS NOT NULL
|
|
16836
|
+
AND updated_at < ?
|
|
16837
|
+
`).run(cutoff);
|
|
16838
|
+
return result.changes;
|
|
16839
|
+
}
|
|
16753
16840
|
function findContradictions2(stateDb2, entity) {
|
|
16754
16841
|
const conditions = ["superseded_by IS NULL"];
|
|
16755
16842
|
const params = [];
|
|
@@ -19218,6 +19305,23 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
|
|
|
19218
19305
|
await scanDir(vaultPath2);
|
|
19219
19306
|
return events;
|
|
19220
19307
|
}
|
|
19308
|
+
var lastPurgeAt = Date.now();
|
|
19309
|
+
function runPeriodicMaintenance(db4) {
|
|
19310
|
+
sweepExpiredMemories(db4);
|
|
19311
|
+
decayMemoryConfidence(db4);
|
|
19312
|
+
pruneSupersededMemories(db4, 90);
|
|
19313
|
+
const now = Date.now();
|
|
19314
|
+
if (now - lastPurgeAt > 24 * 60 * 60 * 1e3) {
|
|
19315
|
+
purgeOldMetrics(db4, 90);
|
|
19316
|
+
purgeOldIndexEvents(db4, 90);
|
|
19317
|
+
purgeOldInvocations(db4, 90);
|
|
19318
|
+
purgeOldSuggestionEvents(db4, 30);
|
|
19319
|
+
purgeOldNoteLinkHistory(db4, 90);
|
|
19320
|
+
purgeOldSnapshots(db4, 90);
|
|
19321
|
+
lastPurgeAt = now;
|
|
19322
|
+
serverLog("server", "Daily purge complete");
|
|
19323
|
+
}
|
|
19324
|
+
}
|
|
19221
19325
|
async function runPostIndexWork(index) {
|
|
19222
19326
|
const postStart = Date.now();
|
|
19223
19327
|
serverLog("index", "Scanning entities...");
|
|
@@ -19233,6 +19337,11 @@ async function runPostIndexWork(index) {
|
|
|
19233
19337
|
purgeOldMetrics(stateDb, 90);
|
|
19234
19338
|
purgeOldIndexEvents(stateDb, 90);
|
|
19235
19339
|
purgeOldInvocations(stateDb, 90);
|
|
19340
|
+
purgeOldSuggestionEvents(stateDb, 30);
|
|
19341
|
+
purgeOldNoteLinkHistory(stateDb, 90);
|
|
19342
|
+
sweepExpiredMemories(stateDb);
|
|
19343
|
+
decayMemoryConfidence(stateDb);
|
|
19344
|
+
pruneSupersededMemories(stateDb, 90);
|
|
19236
19345
|
serverLog("server", "Growth metrics recorded");
|
|
19237
19346
|
} catch (err) {
|
|
19238
19347
|
serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
|
|
@@ -19827,6 +19936,9 @@ async function runPostIndexWork(index) {
|
|
|
19827
19936
|
tracker.end({ tracked: trackedLinks, mentions: mentionResults });
|
|
19828
19937
|
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
19938
|
tracker.start("implicit_feedback", { files: filteredEvents.length });
|
|
19939
|
+
const deletedFiles = new Set(
|
|
19940
|
+
filteredEvents.filter((e) => e.type === "delete").map((e) => e.path)
|
|
19941
|
+
);
|
|
19830
19942
|
const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
|
|
19831
19943
|
const feedbackResults = [];
|
|
19832
19944
|
if (stateDb) {
|
|
@@ -19842,6 +19954,7 @@ async function runPostIndexWork(index) {
|
|
|
19842
19954
|
}
|
|
19843
19955
|
if (stateDb && linkDiffs.length > 0) {
|
|
19844
19956
|
for (const diff of linkDiffs) {
|
|
19957
|
+
if (deletedFiles.has(diff.file)) continue;
|
|
19845
19958
|
for (const target of diff.removed) {
|
|
19846
19959
|
if (feedbackResults.some((r) => r.entity === target && r.file === diff.file)) continue;
|
|
19847
19960
|
const entity = entitiesAfter.find(
|
|
@@ -19860,6 +19973,7 @@ async function runPostIndexWork(index) {
|
|
|
19860
19973
|
`SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
|
|
19861
19974
|
);
|
|
19862
19975
|
for (const diff of linkDiffs) {
|
|
19976
|
+
if (deletedFiles.has(diff.file)) continue;
|
|
19863
19977
|
for (const target of diff.added) {
|
|
19864
19978
|
if (checkApplication.get(target, diff.file)) continue;
|
|
19865
19979
|
const entity = entitiesAfter.find(
|
|
@@ -20071,8 +20185,12 @@ async function runPostIndexWork(index) {
|
|
|
20071
20185
|
watcher.start();
|
|
20072
20186
|
serverLog("watcher", "File watcher started");
|
|
20073
20187
|
}
|
|
20074
|
-
|
|
20075
|
-
|
|
20188
|
+
if (process.env.FLYWHEEL_WATCH !== "false") {
|
|
20189
|
+
startSweepTimer(() => vaultIndex, void 0, () => {
|
|
20190
|
+
if (stateDb) runPeriodicMaintenance(stateDb);
|
|
20191
|
+
});
|
|
20192
|
+
serverLog("server", "Sweep timer started (5 min interval)");
|
|
20193
|
+
}
|
|
20076
20194
|
const postDuration = Date.now() - postStart;
|
|
20077
20195
|
serverLog("server", `Post-index work complete in ${postDuration}ms`);
|
|
20078
20196
|
}
|
|
@@ -20103,6 +20221,7 @@ if (process.argv.includes("--init-semantic")) {
|
|
|
20103
20221
|
});
|
|
20104
20222
|
}
|
|
20105
20223
|
process.on("beforeExit", async () => {
|
|
20224
|
+
stopSweepTimer();
|
|
20106
20225
|
await flushLogs();
|
|
20107
20226
|
});
|
|
20108
20227
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.56",
|
|
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",
|