@velvetmonkey/flywheel-memory 2.0.53 → 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 +171 -61
- 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();
|
|
@@ -4192,6 +4197,75 @@ function getExtendedDashboardData(stateDb2) {
|
|
|
4192
4197
|
};
|
|
4193
4198
|
}
|
|
4194
4199
|
|
|
4200
|
+
// src/core/write/corrections.ts
|
|
4201
|
+
function recordCorrection(stateDb2, type, description, source = "user", entity, notePath) {
|
|
4202
|
+
const result = stateDb2.db.prepare(`
|
|
4203
|
+
INSERT INTO corrections (entity, note_path, correction_type, description, source)
|
|
4204
|
+
VALUES (?, ?, ?, ?, ?)
|
|
4205
|
+
`).run(entity ?? null, notePath ?? null, type, description, source);
|
|
4206
|
+
return stateDb2.db.prepare(
|
|
4207
|
+
"SELECT * FROM corrections WHERE id = ?"
|
|
4208
|
+
).get(result.lastInsertRowid);
|
|
4209
|
+
}
|
|
4210
|
+
function listCorrections(stateDb2, status, entity, limit = 50) {
|
|
4211
|
+
const conditions = [];
|
|
4212
|
+
const params = [];
|
|
4213
|
+
if (status) {
|
|
4214
|
+
conditions.push("status = ?");
|
|
4215
|
+
params.push(status);
|
|
4216
|
+
}
|
|
4217
|
+
if (entity) {
|
|
4218
|
+
conditions.push("entity = ? COLLATE NOCASE");
|
|
4219
|
+
params.push(entity);
|
|
4220
|
+
}
|
|
4221
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
4222
|
+
params.push(limit);
|
|
4223
|
+
return stateDb2.db.prepare(
|
|
4224
|
+
`SELECT * FROM corrections ${where} ORDER BY created_at DESC LIMIT ?`
|
|
4225
|
+
).all(...params);
|
|
4226
|
+
}
|
|
4227
|
+
function resolveCorrection(stateDb2, id, newStatus) {
|
|
4228
|
+
const result = stateDb2.db.prepare(`
|
|
4229
|
+
UPDATE corrections
|
|
4230
|
+
SET status = ?, resolved_at = datetime('now')
|
|
4231
|
+
WHERE id = ?
|
|
4232
|
+
`).run(newStatus, id);
|
|
4233
|
+
return result.changes > 0;
|
|
4234
|
+
}
|
|
4235
|
+
function getCorrectedEntityNotePairs(stateDb2) {
|
|
4236
|
+
const rows = stateDb2.db.prepare(`
|
|
4237
|
+
SELECT entity, note_path FROM corrections
|
|
4238
|
+
WHERE correction_type = 'wrong_link'
|
|
4239
|
+
AND entity IS NOT NULL AND note_path IS NOT NULL
|
|
4240
|
+
AND status IN ('pending', 'applied')
|
|
4241
|
+
`).all();
|
|
4242
|
+
const map = /* @__PURE__ */ new Map();
|
|
4243
|
+
for (const row of rows) {
|
|
4244
|
+
const key = row.entity.toLowerCase();
|
|
4245
|
+
if (!map.has(key)) map.set(key, /* @__PURE__ */ new Set());
|
|
4246
|
+
map.get(key).add(row.note_path);
|
|
4247
|
+
}
|
|
4248
|
+
return map;
|
|
4249
|
+
}
|
|
4250
|
+
function processPendingCorrections(stateDb2) {
|
|
4251
|
+
const pending = listCorrections(stateDb2, "pending");
|
|
4252
|
+
let processed = 0;
|
|
4253
|
+
for (const correction of pending) {
|
|
4254
|
+
if (!correction.entity) {
|
|
4255
|
+
resolveCorrection(stateDb2, correction.id, "dismissed");
|
|
4256
|
+
continue;
|
|
4257
|
+
}
|
|
4258
|
+
if (correction.correction_type === "wrong_link") {
|
|
4259
|
+
recordFeedback(stateDb2, correction.entity, "correction:wrong_link", correction.note_path || "", false, 1);
|
|
4260
|
+
} else if (correction.correction_type === "wrong_category") {
|
|
4261
|
+
recordFeedback(stateDb2, correction.entity, "correction:wrong_category", "", false, 0.5);
|
|
4262
|
+
}
|
|
4263
|
+
resolveCorrection(stateDb2, correction.id, "applied");
|
|
4264
|
+
processed++;
|
|
4265
|
+
}
|
|
4266
|
+
return processed;
|
|
4267
|
+
}
|
|
4268
|
+
|
|
4195
4269
|
// src/core/write/git.ts
|
|
4196
4270
|
import { simpleGit, CheckRepoActions } from "simple-git";
|
|
4197
4271
|
import path8 from "path";
|
|
@@ -5427,13 +5501,13 @@ var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
|
|
|
5427
5501
|
".claude",
|
|
5428
5502
|
".git"
|
|
5429
5503
|
]);
|
|
5430
|
-
function noteContainsEntity(content, entityName) {
|
|
5504
|
+
function noteContainsEntity(content, entityName, contentTokens) {
|
|
5431
5505
|
const entityTokens = tokenize(entityName);
|
|
5432
5506
|
if (entityTokens.length === 0) return false;
|
|
5433
|
-
const
|
|
5507
|
+
const tokens = contentTokens ?? new Set(tokenize(content));
|
|
5434
5508
|
let matchCount = 0;
|
|
5435
5509
|
for (const token of entityTokens) {
|
|
5436
|
-
if (
|
|
5510
|
+
if (tokens.has(token)) {
|
|
5437
5511
|
matchCount++;
|
|
5438
5512
|
}
|
|
5439
5513
|
}
|
|
@@ -5478,9 +5552,10 @@ async function mineCooccurrences(vaultPath2, entities, options = {}) {
|
|
|
5478
5552
|
try {
|
|
5479
5553
|
const content = await readFile2(file.path, "utf-8");
|
|
5480
5554
|
notesScanned++;
|
|
5555
|
+
const contentTokens = new Set(tokenize(content));
|
|
5481
5556
|
const mentionedEntities = [];
|
|
5482
5557
|
for (const entity of validEntities) {
|
|
5483
|
-
if (noteContainsEntity(content, entity)) {
|
|
5558
|
+
if (noteContainsEntity(content, entity, contentTokens)) {
|
|
5484
5559
|
mentionedEntities.push(entity);
|
|
5485
5560
|
}
|
|
5486
5561
|
}
|
|
@@ -5986,6 +6061,14 @@ function processWikilinks(content, notePath, existingContent) {
|
|
|
5986
6061
|
return !isSuppressed(moduleStateDb5, name, folder);
|
|
5987
6062
|
});
|
|
5988
6063
|
}
|
|
6064
|
+
if (moduleStateDb5 && notePath) {
|
|
6065
|
+
const correctedPairs = getCorrectedEntityNotePairs(moduleStateDb5);
|
|
6066
|
+
entities = entities.filter((e) => {
|
|
6067
|
+
const name = getEntityName2(e).toLowerCase();
|
|
6068
|
+
const paths = correctedPairs.get(name);
|
|
6069
|
+
return !paths || !paths.has(notePath);
|
|
6070
|
+
});
|
|
6071
|
+
}
|
|
5989
6072
|
const sortedEntities = sortEntitiesByPriority(entities, notePath);
|
|
5990
6073
|
const resolved = resolveAliasWikilinks(content, sortedEntities, {
|
|
5991
6074
|
caseInsensitive: true
|
|
@@ -6470,6 +6553,7 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6470
6553
|
const noteFolder = notePath ? notePath.split("/")[0] : void 0;
|
|
6471
6554
|
const feedbackBoosts = moduleStateDb5 ? getAllFeedbackBoosts(moduleStateDb5, noteFolder) : /* @__PURE__ */ new Map();
|
|
6472
6555
|
const suppressionPenalties = moduleStateDb5 ? getAllSuppressionPenalties(moduleStateDb5) : /* @__PURE__ */ new Map();
|
|
6556
|
+
const correctedPairs = moduleStateDb5 ? getCorrectedEntityNotePairs(moduleStateDb5) : /* @__PURE__ */ new Map();
|
|
6473
6557
|
const edgeWeightMap = moduleStateDb5 ? getEntityEdgeWeightMap(moduleStateDb5) : /* @__PURE__ */ new Map();
|
|
6474
6558
|
const scoredEntities = [];
|
|
6475
6559
|
const directlyMatchedEntities = /* @__PURE__ */ new Set();
|
|
@@ -6486,6 +6570,10 @@ async function suggestRelatedLinks(content, options = {}) {
|
|
|
6486
6570
|
if (linkedEntities.has(entityName.toLowerCase())) {
|
|
6487
6571
|
continue;
|
|
6488
6572
|
}
|
|
6573
|
+
if (notePath && correctedPairs.has(entityName.toLowerCase())) {
|
|
6574
|
+
const paths = correctedPairs.get(entityName.toLowerCase());
|
|
6575
|
+
if (paths.has(notePath)) continue;
|
|
6576
|
+
}
|
|
6489
6577
|
const contentScore = disabled.has("exact_match") && disabled.has("stem_match") ? 0 : scoreEntity(entity, contentTokens, contentStems, config, cooccurrenceIndex);
|
|
6490
6578
|
let score = contentScore;
|
|
6491
6579
|
if (contentScore > 0) {
|
|
@@ -8804,6 +8892,20 @@ function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
|
|
|
8804
8892
|
).run(cutoff);
|
|
8805
8893
|
return result.changes;
|
|
8806
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
|
+
}
|
|
8807
8909
|
|
|
8808
8910
|
// src/core/read/sweep.ts
|
|
8809
8911
|
var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
@@ -16352,62 +16454,6 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
16352
16454
|
|
|
16353
16455
|
// src/tools/write/corrections.ts
|
|
16354
16456
|
import { z as z22 } from "zod";
|
|
16355
|
-
|
|
16356
|
-
// src/core/write/corrections.ts
|
|
16357
|
-
function recordCorrection(stateDb2, type, description, source = "user", entity, notePath) {
|
|
16358
|
-
const result = stateDb2.db.prepare(`
|
|
16359
|
-
INSERT INTO corrections (entity, note_path, correction_type, description, source)
|
|
16360
|
-
VALUES (?, ?, ?, ?, ?)
|
|
16361
|
-
`).run(entity ?? null, notePath ?? null, type, description, source);
|
|
16362
|
-
return stateDb2.db.prepare(
|
|
16363
|
-
"SELECT * FROM corrections WHERE id = ?"
|
|
16364
|
-
).get(result.lastInsertRowid);
|
|
16365
|
-
}
|
|
16366
|
-
function listCorrections(stateDb2, status, entity, limit = 50) {
|
|
16367
|
-
const conditions = [];
|
|
16368
|
-
const params = [];
|
|
16369
|
-
if (status) {
|
|
16370
|
-
conditions.push("status = ?");
|
|
16371
|
-
params.push(status);
|
|
16372
|
-
}
|
|
16373
|
-
if (entity) {
|
|
16374
|
-
conditions.push("entity = ? COLLATE NOCASE");
|
|
16375
|
-
params.push(entity);
|
|
16376
|
-
}
|
|
16377
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
16378
|
-
params.push(limit);
|
|
16379
|
-
return stateDb2.db.prepare(
|
|
16380
|
-
`SELECT * FROM corrections ${where} ORDER BY created_at DESC LIMIT ?`
|
|
16381
|
-
).all(...params);
|
|
16382
|
-
}
|
|
16383
|
-
function resolveCorrection(stateDb2, id, newStatus) {
|
|
16384
|
-
const result = stateDb2.db.prepare(`
|
|
16385
|
-
UPDATE corrections
|
|
16386
|
-
SET status = ?, resolved_at = datetime('now')
|
|
16387
|
-
WHERE id = ?
|
|
16388
|
-
`).run(newStatus, id);
|
|
16389
|
-
return result.changes > 0;
|
|
16390
|
-
}
|
|
16391
|
-
function processPendingCorrections(stateDb2) {
|
|
16392
|
-
const pending = listCorrections(stateDb2, "pending");
|
|
16393
|
-
let processed = 0;
|
|
16394
|
-
for (const correction of pending) {
|
|
16395
|
-
if (!correction.entity) {
|
|
16396
|
-
resolveCorrection(stateDb2, correction.id, "dismissed");
|
|
16397
|
-
continue;
|
|
16398
|
-
}
|
|
16399
|
-
if (correction.correction_type === "wrong_link") {
|
|
16400
|
-
recordFeedback(stateDb2, correction.entity, "correction:wrong_link", correction.note_path || "", false, 1);
|
|
16401
|
-
} else if (correction.correction_type === "wrong_category") {
|
|
16402
|
-
recordFeedback(stateDb2, correction.entity, "correction:wrong_category", "", false, 0.5);
|
|
16403
|
-
}
|
|
16404
|
-
resolveCorrection(stateDb2, correction.id, "applied");
|
|
16405
|
-
processed++;
|
|
16406
|
-
}
|
|
16407
|
-
return processed;
|
|
16408
|
-
}
|
|
16409
|
-
|
|
16410
|
-
// src/tools/write/corrections.ts
|
|
16411
16457
|
function registerCorrectionTools(server2, getStateDb) {
|
|
16412
16458
|
server2.tool(
|
|
16413
16459
|
"vault_record_correction",
|
|
@@ -16724,6 +16770,60 @@ function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
|
|
|
16724
16770
|
"SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
|
|
16725
16771
|
).all(limit);
|
|
16726
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
|
+
}
|
|
16727
16827
|
function findContradictions2(stateDb2, entity) {
|
|
16728
16828
|
const conditions = ["superseded_by IS NULL"];
|
|
16729
16829
|
const params = [];
|
|
@@ -19207,6 +19307,11 @@ async function runPostIndexWork(index) {
|
|
|
19207
19307
|
purgeOldMetrics(stateDb, 90);
|
|
19208
19308
|
purgeOldIndexEvents(stateDb, 90);
|
|
19209
19309
|
purgeOldInvocations(stateDb, 90);
|
|
19310
|
+
purgeOldSuggestionEvents(stateDb, 30);
|
|
19311
|
+
purgeOldNoteLinkHistory(stateDb, 90);
|
|
19312
|
+
sweepExpiredMemories(stateDb);
|
|
19313
|
+
decayMemoryConfidence(stateDb);
|
|
19314
|
+
pruneSupersededMemories(stateDb, 90);
|
|
19210
19315
|
serverLog("server", "Growth metrics recorded");
|
|
19211
19316
|
} catch (err) {
|
|
19212
19317
|
serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
|
|
@@ -19801,6 +19906,9 @@ async function runPostIndexWork(index) {
|
|
|
19801
19906
|
tracker.end({ tracked: trackedLinks, mentions: mentionResults });
|
|
19802
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`);
|
|
19803
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
|
+
);
|
|
19804
19912
|
const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
|
|
19805
19913
|
const feedbackResults = [];
|
|
19806
19914
|
if (stateDb) {
|
|
@@ -19816,6 +19924,7 @@ async function runPostIndexWork(index) {
|
|
|
19816
19924
|
}
|
|
19817
19925
|
if (stateDb && linkDiffs.length > 0) {
|
|
19818
19926
|
for (const diff of linkDiffs) {
|
|
19927
|
+
if (deletedFiles.has(diff.file)) continue;
|
|
19819
19928
|
for (const target of diff.removed) {
|
|
19820
19929
|
if (feedbackResults.some((r) => r.entity === target && r.file === diff.file)) continue;
|
|
19821
19930
|
const entity = entitiesAfter.find(
|
|
@@ -19834,6 +19943,7 @@ async function runPostIndexWork(index) {
|
|
|
19834
19943
|
`SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
|
|
19835
19944
|
);
|
|
19836
19945
|
for (const diff of linkDiffs) {
|
|
19946
|
+
if (deletedFiles.has(diff.file)) continue;
|
|
19837
19947
|
for (const target of diff.added) {
|
|
19838
19948
|
if (checkApplication.get(target, diff.file)) continue;
|
|
19839
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",
|