@velvetmonkey/flywheel-memory 2.0.51 → 2.0.52
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 +287 -132
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1834,6 +1834,15 @@ var embeddingsBuilding = false;
|
|
|
1834
1834
|
var embeddingCache = /* @__PURE__ */ new Map();
|
|
1835
1835
|
var EMBEDDING_CACHE_MAX = 500;
|
|
1836
1836
|
var entityEmbeddingsMap = /* @__PURE__ */ new Map();
|
|
1837
|
+
function getEmbeddingsBuildState() {
|
|
1838
|
+
if (!db) return "none";
|
|
1839
|
+
const row = db.prepare(`SELECT value FROM fts_metadata WHERE key = 'embeddings_state'`).get();
|
|
1840
|
+
return row?.value || "none";
|
|
1841
|
+
}
|
|
1842
|
+
function setEmbeddingsBuildState(state2) {
|
|
1843
|
+
if (!db) return;
|
|
1844
|
+
db.prepare(`INSERT OR REPLACE INTO fts_metadata (key, value) VALUES ('embeddings_state', ?)`).run(state2);
|
|
1845
|
+
}
|
|
1837
1846
|
function setEmbeddingsDatabase(database) {
|
|
1838
1847
|
db = database;
|
|
1839
1848
|
}
|
|
@@ -1887,6 +1896,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
|
|
|
1887
1896
|
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
1888
1897
|
}
|
|
1889
1898
|
embeddingsBuilding = true;
|
|
1899
|
+
setEmbeddingsBuildState("building_notes");
|
|
1890
1900
|
await initEmbeddings();
|
|
1891
1901
|
const files = await scanVault(vaultPath2);
|
|
1892
1902
|
const indexable = files.filter((f) => shouldIndexFile(f.path));
|
|
@@ -2037,8 +2047,13 @@ function setEmbeddingsBuilding(value) {
|
|
|
2037
2047
|
function hasEmbeddingsIndex() {
|
|
2038
2048
|
if (!db) return false;
|
|
2039
2049
|
try {
|
|
2040
|
-
const
|
|
2041
|
-
|
|
2050
|
+
const state2 = getEmbeddingsBuildState();
|
|
2051
|
+
if (state2 === "complete") return true;
|
|
2052
|
+
if (state2 === "none") {
|
|
2053
|
+
const row = db.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
|
|
2054
|
+
return row.count > 0;
|
|
2055
|
+
}
|
|
2056
|
+
return false;
|
|
2042
2057
|
} catch {
|
|
2043
2058
|
return false;
|
|
2044
2059
|
}
|
|
@@ -2090,6 +2105,7 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
|
|
|
2090
2105
|
throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
|
|
2091
2106
|
}
|
|
2092
2107
|
await initEmbeddings();
|
|
2108
|
+
setEmbeddingsBuildState("building_entities");
|
|
2093
2109
|
const existingHashes = /* @__PURE__ */ new Map();
|
|
2094
2110
|
const rows = db.prepare("SELECT entity_name, source_hash FROM entity_embeddings").all();
|
|
2095
2111
|
for (const row of rows) {
|
|
@@ -3915,6 +3931,20 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
|
|
|
3915
3931
|
if (removed.length > 0) {
|
|
3916
3932
|
updateSuppressionList(stateDb2);
|
|
3917
3933
|
}
|
|
3934
|
+
const SURVIVAL_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
|
|
3935
|
+
const getLastSurvival = stateDb2.db.prepare(
|
|
3936
|
+
`SELECT MAX(created_at) as last FROM wikilink_feedback
|
|
3937
|
+
WHERE entity = ? COLLATE NOCASE AND context = 'implicit:survived' AND note_path = ?`
|
|
3938
|
+
);
|
|
3939
|
+
for (const { entity } of trackedWithTime) {
|
|
3940
|
+
if (currentLinks.has(entity.toLowerCase())) {
|
|
3941
|
+
const lastSurvival = getLastSurvival.get(entity, notePath);
|
|
3942
|
+
const lastAt = lastSurvival?.last ? new Date(lastSurvival.last).getTime() : 0;
|
|
3943
|
+
if (Date.now() - lastAt > SURVIVAL_COOLDOWN_MS) {
|
|
3944
|
+
recordFeedback(stateDb2, entity, "implicit:survived", notePath, true, 0.8);
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3918
3948
|
return removed;
|
|
3919
3949
|
}
|
|
3920
3950
|
var TIER_LABELS = [
|
|
@@ -5534,6 +5564,68 @@ function tokenIdf(token, coocIndex) {
|
|
|
5534
5564
|
const rawIdf = Math.log((N + 1) / (df + 1));
|
|
5535
5565
|
return Math.max(0.5, Math.min(2.5, rawIdf));
|
|
5536
5566
|
}
|
|
5567
|
+
function serializeCooccurrenceIndex(index) {
|
|
5568
|
+
const serialized = {};
|
|
5569
|
+
for (const [entity, assocs] of Object.entries(index.associations)) {
|
|
5570
|
+
serialized[entity] = Object.fromEntries(assocs);
|
|
5571
|
+
}
|
|
5572
|
+
return {
|
|
5573
|
+
associations: serialized,
|
|
5574
|
+
minCount: index.minCount,
|
|
5575
|
+
documentFrequency: Object.fromEntries(index.documentFrequency),
|
|
5576
|
+
totalNotesScanned: index.totalNotesScanned,
|
|
5577
|
+
_metadata: index._metadata
|
|
5578
|
+
};
|
|
5579
|
+
}
|
|
5580
|
+
function deserializeCooccurrenceIndex(data) {
|
|
5581
|
+
try {
|
|
5582
|
+
const associations = {};
|
|
5583
|
+
const assocData = data.associations;
|
|
5584
|
+
if (!assocData) return null;
|
|
5585
|
+
for (const [entity, assocs] of Object.entries(assocData)) {
|
|
5586
|
+
associations[entity] = new Map(Object.entries(assocs));
|
|
5587
|
+
}
|
|
5588
|
+
const dfData = data.documentFrequency;
|
|
5589
|
+
const documentFrequency = dfData ? new Map(Object.entries(dfData).map(([k, v]) => [k, v])) : /* @__PURE__ */ new Map();
|
|
5590
|
+
const totalNotesScanned = data.totalNotesScanned || 0;
|
|
5591
|
+
return {
|
|
5592
|
+
associations,
|
|
5593
|
+
minCount: data.minCount || DEFAULT_MIN_COOCCURRENCE,
|
|
5594
|
+
documentFrequency,
|
|
5595
|
+
totalNotesScanned,
|
|
5596
|
+
_metadata: data._metadata
|
|
5597
|
+
};
|
|
5598
|
+
} catch {
|
|
5599
|
+
return null;
|
|
5600
|
+
}
|
|
5601
|
+
}
|
|
5602
|
+
var COOCCURRENCE_CACHE_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
5603
|
+
function saveCooccurrenceToStateDb(stateDb2, index) {
|
|
5604
|
+
const serialized = serializeCooccurrenceIndex(index);
|
|
5605
|
+
const data = JSON.stringify(serialized);
|
|
5606
|
+
const entityCount = Object.keys(index.associations).length;
|
|
5607
|
+
const associationCount = index._metadata.total_associations;
|
|
5608
|
+
stateDb2.db.prepare(`
|
|
5609
|
+
INSERT OR REPLACE INTO cooccurrence_cache (id, data, built_at, entity_count, association_count)
|
|
5610
|
+
VALUES (1, ?, ?, ?, ?)
|
|
5611
|
+
`).run(data, Date.now(), entityCount, associationCount);
|
|
5612
|
+
}
|
|
5613
|
+
function loadCooccurrenceFromStateDb(stateDb2) {
|
|
5614
|
+
const row = stateDb2.db.prepare(
|
|
5615
|
+
"SELECT data, built_at FROM cooccurrence_cache WHERE id = 1"
|
|
5616
|
+
).get();
|
|
5617
|
+
if (!row) return null;
|
|
5618
|
+
const ageMs = Date.now() - row.built_at;
|
|
5619
|
+
if (ageMs > COOCCURRENCE_CACHE_MAX_AGE_MS) return null;
|
|
5620
|
+
try {
|
|
5621
|
+
const parsed = JSON.parse(row.data);
|
|
5622
|
+
const index = deserializeCooccurrenceIndex(parsed);
|
|
5623
|
+
if (!index) return null;
|
|
5624
|
+
return { index, builtAt: row.built_at };
|
|
5625
|
+
} catch {
|
|
5626
|
+
return null;
|
|
5627
|
+
}
|
|
5628
|
+
}
|
|
5537
5629
|
|
|
5538
5630
|
// src/core/write/edgeWeights.ts
|
|
5539
5631
|
var moduleStateDb4 = null;
|
|
@@ -7909,7 +8001,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
7909
8001
|
|
|
7910
8002
|
// src/tools/read/wikilinks.ts
|
|
7911
8003
|
import { z as z2 } from "zod";
|
|
7912
|
-
import { detectImplicitEntities as detectImplicitEntities2 } from "@velvetmonkey/vault-core";
|
|
8004
|
+
import { detectImplicitEntities as detectImplicitEntities2, IMPLICIT_EXCLUDE_WORDS } from "@velvetmonkey/vault-core";
|
|
7913
8005
|
function findEntityMatches(text, entities) {
|
|
7914
8006
|
const matches = [];
|
|
7915
8007
|
const sortedEntities = Array.from(entities.entries()).filter(([name]) => name.length >= 2).sort((a, b) => b[0].length - a[0].length);
|
|
@@ -8031,6 +8123,8 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
|
|
|
8031
8123
|
if (links.length < 2) continue;
|
|
8032
8124
|
if (index.entities.has(target.toLowerCase())) continue;
|
|
8033
8125
|
if (linkedSet.has(target.toLowerCase())) continue;
|
|
8126
|
+
if (IMPLICIT_EXCLUDE_WORDS.has(target.toLowerCase())) continue;
|
|
8127
|
+
if (target.length < 4) continue;
|
|
8034
8128
|
const targetLower = target.toLowerCase();
|
|
8035
8129
|
const textLower = text.toLowerCase();
|
|
8036
8130
|
let searchPos = 0;
|
|
@@ -16294,6 +16388,24 @@ function resolveCorrection(stateDb2, id, newStatus) {
|
|
|
16294
16388
|
`).run(newStatus, id);
|
|
16295
16389
|
return result.changes > 0;
|
|
16296
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
|
+
}
|
|
16297
16409
|
|
|
16298
16410
|
// src/tools/write/corrections.ts
|
|
16299
16411
|
function registerCorrectionTools(server2, getStateDb) {
|
|
@@ -18926,6 +19038,12 @@ async function main() {
|
|
|
18926
19038
|
setWriteStateDb(stateDb);
|
|
18927
19039
|
setRecencyStateDb(stateDb);
|
|
18928
19040
|
setEdgeWeightStateDb(stateDb);
|
|
19041
|
+
const cachedCooc = loadCooccurrenceFromStateDb(stateDb);
|
|
19042
|
+
if (cachedCooc) {
|
|
19043
|
+
setCooccurrenceIndex(cachedCooc.index);
|
|
19044
|
+
lastCooccurrenceRebuildAt = cachedCooc.builtAt;
|
|
19045
|
+
serverLog("index", `Co-occurrence: loaded from cache (${Object.keys(cachedCooc.index.associations).length} entities, ${cachedCooc.index._metadata.total_associations} associations)`);
|
|
19046
|
+
}
|
|
18929
19047
|
const vaultInitRow = stateDb.getMetadataValue.get("vault_init_last_run_at");
|
|
18930
19048
|
if (!vaultInitRow) {
|
|
18931
19049
|
serverLog("server", "Vault not initialized \u2014 call vault_init to enrich legacy notes");
|
|
@@ -19154,6 +19272,7 @@ async function runPostIndexWork(index) {
|
|
|
19154
19272
|
}
|
|
19155
19273
|
}
|
|
19156
19274
|
loadEntityEmbeddingsToMemory();
|
|
19275
|
+
setEmbeddingsBuildState("complete");
|
|
19157
19276
|
serverLog("semantic", "Embeddings ready");
|
|
19158
19277
|
}).catch((err) => {
|
|
19159
19278
|
serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
@@ -19339,17 +19458,22 @@ async function runPostIndexWork(index) {
|
|
|
19339
19458
|
tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges, description_changes: descriptionChanges });
|
|
19340
19459
|
serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
|
|
19341
19460
|
tracker.start("hub_scores", { entity_count: entitiesAfter.length });
|
|
19342
|
-
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
const
|
|
19348
|
-
|
|
19461
|
+
try {
|
|
19462
|
+
const hubUpdated = await exportHubScores(vaultIndex, stateDb);
|
|
19463
|
+
const hubDiffs = [];
|
|
19464
|
+
if (stateDb) {
|
|
19465
|
+
const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
|
|
19466
|
+
for (const r of rows) {
|
|
19467
|
+
const prev = hubBefore.get(r.name) ?? 0;
|
|
19468
|
+
if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
|
|
19469
|
+
}
|
|
19349
19470
|
}
|
|
19471
|
+
tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
|
|
19472
|
+
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
19473
|
+
} catch (e) {
|
|
19474
|
+
tracker.end({ error: String(e) });
|
|
19475
|
+
serverLog("watcher", `Hub scores: failed: ${e}`, "error");
|
|
19350
19476
|
}
|
|
19351
|
-
tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
|
|
19352
|
-
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
19353
19477
|
tracker.start("recency", { entity_count: entitiesAfter.length });
|
|
19354
19478
|
try {
|
|
19355
19479
|
const cachedRecency = loadRecencyFromStateDb();
|
|
@@ -19376,6 +19500,9 @@ async function runPostIndexWork(index) {
|
|
|
19376
19500
|
const cooccurrenceIdx = await mineCooccurrences(vaultPath, entityNames);
|
|
19377
19501
|
setCooccurrenceIndex(cooccurrenceIdx);
|
|
19378
19502
|
lastCooccurrenceRebuildAt = Date.now();
|
|
19503
|
+
if (stateDb) {
|
|
19504
|
+
saveCooccurrenceToStateDb(stateDb, cooccurrenceIdx);
|
|
19505
|
+
}
|
|
19379
19506
|
tracker.end({ rebuilt: true, associations: cooccurrenceIdx._metadata.total_associations });
|
|
19380
19507
|
serverLog("watcher", `Co-occurrence: rebuilt ${cooccurrenceIdx._metadata.total_associations} associations`);
|
|
19381
19508
|
} else {
|
|
@@ -19491,87 +19618,99 @@ async function runPostIndexWork(index) {
|
|
|
19491
19618
|
}
|
|
19492
19619
|
tracker.end({ updated: taskUpdated, removed: taskRemoved });
|
|
19493
19620
|
serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
|
|
19494
|
-
tracker.start("forward_links", { files: filteredEvents.length });
|
|
19495
|
-
const eventTypeMap = new Map(filteredEvents.map((e) => [e.path, e.type]));
|
|
19496
19621
|
const forwardLinkResults = [];
|
|
19497
19622
|
let totalResolved = 0;
|
|
19498
19623
|
let totalDead = 0;
|
|
19499
|
-
for (const event of filteredEvents) {
|
|
19500
|
-
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
19501
|
-
try {
|
|
19502
|
-
const links = getForwardLinksForNote(vaultIndex, event.path);
|
|
19503
|
-
const resolved = [];
|
|
19504
|
-
const dead = [];
|
|
19505
|
-
const seen = /* @__PURE__ */ new Set();
|
|
19506
|
-
for (const link of links) {
|
|
19507
|
-
const name = link.target;
|
|
19508
|
-
if (seen.has(name.toLowerCase())) continue;
|
|
19509
|
-
seen.add(name.toLowerCase());
|
|
19510
|
-
if (link.exists) resolved.push(name);
|
|
19511
|
-
else dead.push(name);
|
|
19512
|
-
}
|
|
19513
|
-
if (resolved.length > 0 || dead.length > 0) {
|
|
19514
|
-
forwardLinkResults.push({ file: event.path, resolved, dead });
|
|
19515
|
-
}
|
|
19516
|
-
totalResolved += resolved.length;
|
|
19517
|
-
totalDead += dead.length;
|
|
19518
|
-
} catch {
|
|
19519
|
-
}
|
|
19520
|
-
}
|
|
19521
19624
|
const linkDiffs = [];
|
|
19522
19625
|
const survivedLinks = [];
|
|
19523
|
-
|
|
19524
|
-
|
|
19626
|
+
tracker.start("forward_links", { files: filteredEvents.length });
|
|
19627
|
+
try {
|
|
19628
|
+
const eventTypeMap = new Map(filteredEvents.map((e) => [e.path, e.type]));
|
|
19629
|
+
for (const event of filteredEvents) {
|
|
19630
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
19631
|
+
try {
|
|
19632
|
+
const links = getForwardLinksForNote(vaultIndex, event.path);
|
|
19633
|
+
const resolved = [];
|
|
19634
|
+
const dead = [];
|
|
19635
|
+
const seen = /* @__PURE__ */ new Set();
|
|
19636
|
+
for (const link of links) {
|
|
19637
|
+
const name = link.target;
|
|
19638
|
+
if (seen.has(name.toLowerCase())) continue;
|
|
19639
|
+
seen.add(name.toLowerCase());
|
|
19640
|
+
if (link.exists) resolved.push(name);
|
|
19641
|
+
else dead.push(name);
|
|
19642
|
+
}
|
|
19643
|
+
if (resolved.length > 0 || dead.length > 0) {
|
|
19644
|
+
forwardLinkResults.push({ file: event.path, resolved, dead });
|
|
19645
|
+
}
|
|
19646
|
+
totalResolved += resolved.length;
|
|
19647
|
+
totalDead += dead.length;
|
|
19648
|
+
} catch {
|
|
19649
|
+
}
|
|
19650
|
+
}
|
|
19651
|
+
if (stateDb) {
|
|
19652
|
+
const upsertHistory = stateDb.db.prepare(`
|
|
19525
19653
|
INSERT INTO note_link_history (note_path, target) VALUES (?, ?)
|
|
19526
19654
|
ON CONFLICT(note_path, target) DO UPDATE SET edits_survived = edits_survived + 1
|
|
19527
19655
|
`);
|
|
19528
|
-
|
|
19656
|
+
const checkThreshold = stateDb.db.prepare(`
|
|
19529
19657
|
SELECT target FROM note_link_history
|
|
19530
19658
|
WHERE note_path = ? AND target = ? AND edits_survived >= 3 AND last_positive_at IS NULL
|
|
19531
19659
|
`);
|
|
19532
|
-
|
|
19660
|
+
const markPositive = stateDb.db.prepare(`
|
|
19533
19661
|
UPDATE note_link_history SET last_positive_at = datetime('now') WHERE note_path = ? AND target = ?
|
|
19534
19662
|
`);
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
19544
|
-
|
|
19663
|
+
const getEdgeCount = stateDb.db.prepare(
|
|
19664
|
+
"SELECT edits_survived FROM note_link_history WHERE note_path=? AND target=?"
|
|
19665
|
+
);
|
|
19666
|
+
for (const entry of forwardLinkResults) {
|
|
19667
|
+
const currentSet = /* @__PURE__ */ new Set([
|
|
19668
|
+
...entry.resolved.map((n) => n.toLowerCase()),
|
|
19669
|
+
...entry.dead.map((n) => n.toLowerCase())
|
|
19670
|
+
]);
|
|
19671
|
+
const previousSet = getStoredNoteLinks(stateDb, entry.file);
|
|
19672
|
+
if (previousSet.size === 0) {
|
|
19673
|
+
updateStoredNoteLinks(stateDb, entry.file, currentSet);
|
|
19674
|
+
continue;
|
|
19675
|
+
}
|
|
19676
|
+
const diff = diffNoteLinks(previousSet, currentSet);
|
|
19677
|
+
if (diff.added.length > 0 || diff.removed.length > 0) {
|
|
19678
|
+
linkDiffs.push({ file: entry.file, ...diff });
|
|
19679
|
+
}
|
|
19545
19680
|
updateStoredNoteLinks(stateDb, entry.file, currentSet);
|
|
19546
|
-
continue;
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
19557
|
-
|
|
19558
|
-
|
|
19559
|
-
|
|
19681
|
+
if (diff.removed.length === 0) continue;
|
|
19682
|
+
for (const link of currentSet) {
|
|
19683
|
+
if (!previousSet.has(link)) continue;
|
|
19684
|
+
upsertHistory.run(entry.file, link);
|
|
19685
|
+
const countRow = getEdgeCount.get(entry.file, link);
|
|
19686
|
+
if (countRow) {
|
|
19687
|
+
survivedLinks.push({ entity: link, file: entry.file, count: countRow.edits_survived });
|
|
19688
|
+
}
|
|
19689
|
+
const hit = checkThreshold.get(entry.file, link);
|
|
19690
|
+
if (hit) {
|
|
19691
|
+
const entity = entitiesAfter.find(
|
|
19692
|
+
(e) => e.nameLower === link || (e.aliases ?? []).some((a) => a.toLowerCase() === link)
|
|
19693
|
+
);
|
|
19694
|
+
if (entity) {
|
|
19695
|
+
recordFeedback(stateDb, entity.name, "implicit:kept", entry.file, true, 0.8);
|
|
19696
|
+
markPositive.run(entry.file, link);
|
|
19697
|
+
}
|
|
19698
|
+
}
|
|
19560
19699
|
}
|
|
19561
|
-
|
|
19562
|
-
|
|
19563
|
-
|
|
19564
|
-
|
|
19565
|
-
)
|
|
19566
|
-
|
|
19567
|
-
|
|
19568
|
-
markPositive.run(entry.file, link);
|
|
19700
|
+
}
|
|
19701
|
+
for (const event of filteredEvents) {
|
|
19702
|
+
if (event.type === "delete") {
|
|
19703
|
+
const previousSet = getStoredNoteLinks(stateDb, event.path);
|
|
19704
|
+
if (previousSet.size > 0) {
|
|
19705
|
+
linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
19706
|
+
updateStoredNoteLinks(stateDb, event.path, /* @__PURE__ */ new Set());
|
|
19569
19707
|
}
|
|
19570
19708
|
}
|
|
19571
19709
|
}
|
|
19572
|
-
|
|
19573
|
-
|
|
19574
|
-
|
|
19710
|
+
const processedFiles = new Set(forwardLinkResults.map((r) => r.file));
|
|
19711
|
+
for (const event of filteredEvents) {
|
|
19712
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
19713
|
+
if (processedFiles.has(event.path)) continue;
|
|
19575
19714
|
const previousSet = getStoredNoteLinks(stateDb, event.path);
|
|
19576
19715
|
if (previousSet.size > 0) {
|
|
19577
19716
|
linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
@@ -19579,33 +19718,26 @@ async function runPostIndexWork(index) {
|
|
|
19579
19718
|
}
|
|
19580
19719
|
}
|
|
19581
19720
|
}
|
|
19582
|
-
const
|
|
19583
|
-
for (const
|
|
19584
|
-
|
|
19585
|
-
if (
|
|
19586
|
-
|
|
19587
|
-
if (previousSet.size > 0) {
|
|
19588
|
-
linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
19589
|
-
updateStoredNoteLinks(stateDb, event.path, /* @__PURE__ */ new Set());
|
|
19721
|
+
const newDeadLinks = [];
|
|
19722
|
+
for (const diff of linkDiffs) {
|
|
19723
|
+
const newDead = diff.added.filter((target) => !vaultIndex.entities.has(target.toLowerCase()));
|
|
19724
|
+
if (newDead.length > 0) {
|
|
19725
|
+
newDeadLinks.push({ file: diff.file, targets: newDead });
|
|
19590
19726
|
}
|
|
19591
19727
|
}
|
|
19728
|
+
tracker.end({
|
|
19729
|
+
total_resolved: totalResolved,
|
|
19730
|
+
total_dead: totalDead,
|
|
19731
|
+
links: forwardLinkResults,
|
|
19732
|
+
link_diffs: linkDiffs,
|
|
19733
|
+
survived: survivedLinks,
|
|
19734
|
+
new_dead_links: newDeadLinks
|
|
19735
|
+
});
|
|
19736
|
+
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead${newDeadLinks.length > 0 ? `, ${newDeadLinks.reduce((s, d) => s + d.targets.length, 0)} new dead` : ""}`);
|
|
19737
|
+
} catch (e) {
|
|
19738
|
+
tracker.end({ error: String(e) });
|
|
19739
|
+
serverLog("watcher", `Forward links: failed: ${e}`, "error");
|
|
19592
19740
|
}
|
|
19593
|
-
const newDeadLinks = [];
|
|
19594
|
-
for (const diff of linkDiffs) {
|
|
19595
|
-
const newDead = diff.added.filter((target) => !vaultIndex.entities.has(target.toLowerCase()));
|
|
19596
|
-
if (newDead.length > 0) {
|
|
19597
|
-
newDeadLinks.push({ file: diff.file, targets: newDead });
|
|
19598
|
-
}
|
|
19599
|
-
}
|
|
19600
|
-
tracker.end({
|
|
19601
|
-
total_resolved: totalResolved,
|
|
19602
|
-
total_dead: totalDead,
|
|
19603
|
-
links: forwardLinkResults,
|
|
19604
|
-
link_diffs: linkDiffs,
|
|
19605
|
-
survived: survivedLinks,
|
|
19606
|
-
new_dead_links: newDeadLinks
|
|
19607
|
-
});
|
|
19608
|
-
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead${newDeadLinks.length > 0 ? `, ${newDeadLinks.reduce((s, d) => s + d.targets.length, 0)} new dead` : ""}`);
|
|
19609
19741
|
tracker.start("wikilink_check", { files: filteredEvents.length });
|
|
19610
19742
|
const trackedLinks = [];
|
|
19611
19743
|
if (stateDb) {
|
|
@@ -19730,6 +19862,24 @@ async function runPostIndexWork(index) {
|
|
|
19730
19862
|
if (newlySuppressed.length > 0) {
|
|
19731
19863
|
serverLog("watcher", `Suppression: ${newlySuppressed.length} entities newly suppressed: ${newlySuppressed.join(", ")}`);
|
|
19732
19864
|
}
|
|
19865
|
+
tracker.start("corrections", {});
|
|
19866
|
+
try {
|
|
19867
|
+
if (stateDb) {
|
|
19868
|
+
const corrProcessed = processPendingCorrections(stateDb);
|
|
19869
|
+
if (corrProcessed > 0) {
|
|
19870
|
+
updateSuppressionList(stateDb);
|
|
19871
|
+
}
|
|
19872
|
+
tracker.end({ processed: corrProcessed });
|
|
19873
|
+
if (corrProcessed > 0) {
|
|
19874
|
+
serverLog("watcher", `Corrections: ${corrProcessed} processed`);
|
|
19875
|
+
}
|
|
19876
|
+
} else {
|
|
19877
|
+
tracker.end({ skipped: true });
|
|
19878
|
+
}
|
|
19879
|
+
} catch (e) {
|
|
19880
|
+
tracker.end({ error: String(e) });
|
|
19881
|
+
serverLog("watcher", `Corrections: failed: ${e}`, "error");
|
|
19882
|
+
}
|
|
19733
19883
|
tracker.start("prospect_scan", { files: filteredEvents.length });
|
|
19734
19884
|
const prospectResults = [];
|
|
19735
19885
|
for (const event of filteredEvents) {
|
|
@@ -19793,45 +19943,50 @@ async function runPostIndexWork(index) {
|
|
|
19793
19943
|
serverLog("watcher", `Suggestion scoring: ${suggestionResults.length} files scored`);
|
|
19794
19944
|
}
|
|
19795
19945
|
tracker.start("tag_scan", { files: filteredEvents.length });
|
|
19796
|
-
|
|
19797
|
-
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
for (const
|
|
19801
|
-
|
|
19802
|
-
|
|
19946
|
+
try {
|
|
19947
|
+
const tagDiffs = [];
|
|
19948
|
+
if (stateDb) {
|
|
19949
|
+
const noteTagsForward = /* @__PURE__ */ new Map();
|
|
19950
|
+
for (const [tag, paths] of vaultIndex.tags) {
|
|
19951
|
+
for (const notePath of paths) {
|
|
19952
|
+
if (!noteTagsForward.has(notePath)) noteTagsForward.set(notePath, /* @__PURE__ */ new Set());
|
|
19953
|
+
noteTagsForward.get(notePath).add(tag);
|
|
19954
|
+
}
|
|
19803
19955
|
}
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19956
|
+
for (const event of filteredEvents) {
|
|
19957
|
+
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
19958
|
+
const currentSet = noteTagsForward.get(event.path) ?? /* @__PURE__ */ new Set();
|
|
19959
|
+
const previousSet = getStoredNoteTags(stateDb, event.path);
|
|
19960
|
+
if (previousSet.size === 0 && currentSet.size > 0) {
|
|
19961
|
+
updateStoredNoteTags(stateDb, event.path, currentSet);
|
|
19962
|
+
continue;
|
|
19963
|
+
}
|
|
19964
|
+
const added = [...currentSet].filter((t) => !previousSet.has(t));
|
|
19965
|
+
const removed = [...previousSet].filter((t) => !currentSet.has(t));
|
|
19966
|
+
if (added.length > 0 || removed.length > 0) {
|
|
19967
|
+
tagDiffs.push({ file: event.path, added, removed });
|
|
19968
|
+
}
|
|
19810
19969
|
updateStoredNoteTags(stateDb, event.path, currentSet);
|
|
19811
|
-
continue;
|
|
19812
|
-
}
|
|
19813
|
-
const added = [...currentSet].filter((t) => !previousSet.has(t));
|
|
19814
|
-
const removed = [...previousSet].filter((t) => !currentSet.has(t));
|
|
19815
|
-
if (added.length > 0 || removed.length > 0) {
|
|
19816
|
-
tagDiffs.push({ file: event.path, added, removed });
|
|
19817
19970
|
}
|
|
19818
|
-
|
|
19819
|
-
|
|
19820
|
-
|
|
19821
|
-
|
|
19822
|
-
|
|
19823
|
-
|
|
19824
|
-
|
|
19825
|
-
updateStoredNoteTags(stateDb, event.path, /* @__PURE__ */ new Set());
|
|
19971
|
+
for (const event of filteredEvents) {
|
|
19972
|
+
if (event.type === "delete") {
|
|
19973
|
+
const previousSet = getStoredNoteTags(stateDb, event.path);
|
|
19974
|
+
if (previousSet.size > 0) {
|
|
19975
|
+
tagDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
19976
|
+
updateStoredNoteTags(stateDb, event.path, /* @__PURE__ */ new Set());
|
|
19977
|
+
}
|
|
19826
19978
|
}
|
|
19827
19979
|
}
|
|
19828
19980
|
}
|
|
19829
|
-
|
|
19830
|
-
|
|
19831
|
-
|
|
19832
|
-
|
|
19833
|
-
|
|
19834
|
-
|
|
19981
|
+
const totalTagsAdded = tagDiffs.reduce((s, d) => s + d.added.length, 0);
|
|
19982
|
+
const totalTagsRemoved = tagDiffs.reduce((s, d) => s + d.removed.length, 0);
|
|
19983
|
+
tracker.end({ total_added: totalTagsAdded, total_removed: totalTagsRemoved, tag_diffs: tagDiffs });
|
|
19984
|
+
if (tagDiffs.length > 0) {
|
|
19985
|
+
serverLog("watcher", `Tag scan: ${totalTagsAdded} added, ${totalTagsRemoved} removed across ${tagDiffs.length} files`);
|
|
19986
|
+
}
|
|
19987
|
+
} catch (e) {
|
|
19988
|
+
tracker.end({ error: String(e) });
|
|
19989
|
+
serverLog("watcher", `Tag scan: failed: ${e}`, "error");
|
|
19835
19990
|
}
|
|
19836
19991
|
const duration = Date.now() - batchStart;
|
|
19837
19992
|
if (stateDb) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.52",
|
|
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.52",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|