@velvetmonkey/flywheel-memory 2.0.39 → 2.0.41
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 +329 -13
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3506,9 +3506,11 @@ var FEEDBACK_BOOST_TIERS = [
|
|
|
3506
3506
|
];
|
|
3507
3507
|
function recordFeedback(stateDb2, entity, context, notePath, correct) {
|
|
3508
3508
|
try {
|
|
3509
|
-
|
|
3509
|
+
console.error(`[Flywheel] recordFeedback: entity="${entity}" context="${context}" notePath="${notePath}" correct=${correct}`);
|
|
3510
|
+
const result = stateDb2.db.prepare(
|
|
3510
3511
|
"INSERT INTO wikilink_feedback (entity, context, note_path, correct) VALUES (?, ?, ?, ?)"
|
|
3511
3512
|
).run(entity, context, notePath, correct ? 1 : 0);
|
|
3513
|
+
console.error(`[Flywheel] recordFeedback: inserted id=${result.lastInsertRowid}`);
|
|
3512
3514
|
} catch (e) {
|
|
3513
3515
|
console.error(`[Flywheel] recordFeedback failed for entity="${entity}": ${e}`);
|
|
3514
3516
|
throw e;
|
|
@@ -3719,13 +3721,25 @@ function getStoredNoteLinks(stateDb2, notePath) {
|
|
|
3719
3721
|
return new Set(rows.map((r) => r.target));
|
|
3720
3722
|
}
|
|
3721
3723
|
function updateStoredNoteLinks(stateDb2, notePath, currentLinks) {
|
|
3722
|
-
const
|
|
3723
|
-
|
|
3724
|
+
const ins = stateDb2.db.prepare(
|
|
3725
|
+
"INSERT OR IGNORE INTO note_links (note_path, target) VALUES (?, ?)"
|
|
3726
|
+
);
|
|
3727
|
+
const del = stateDb2.db.prepare(
|
|
3728
|
+
"DELETE FROM note_links WHERE note_path = ? AND target = ?"
|
|
3729
|
+
);
|
|
3730
|
+
const existing = stateDb2.db.prepare(
|
|
3731
|
+
"SELECT target FROM note_links WHERE note_path = ?"
|
|
3732
|
+
);
|
|
3724
3733
|
const tx = stateDb2.db.transaction(() => {
|
|
3725
|
-
del.run(notePath);
|
|
3726
3734
|
for (const target of currentLinks) {
|
|
3727
3735
|
ins.run(notePath, target);
|
|
3728
3736
|
}
|
|
3737
|
+
const rows = existing.all(notePath);
|
|
3738
|
+
for (const row of rows) {
|
|
3739
|
+
if (!currentLinks.has(row.target)) {
|
|
3740
|
+
del.run(notePath, row.target);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3729
3743
|
});
|
|
3730
3744
|
tx();
|
|
3731
3745
|
}
|
|
@@ -5387,6 +5401,9 @@ function getEffectiveStrictness(notePath) {
|
|
|
5387
5401
|
function getCooccurrenceIndex() {
|
|
5388
5402
|
return cooccurrenceIndex;
|
|
5389
5403
|
}
|
|
5404
|
+
function setCooccurrenceIndex(index) {
|
|
5405
|
+
cooccurrenceIndex = index;
|
|
5406
|
+
}
|
|
5390
5407
|
var entityIndex = null;
|
|
5391
5408
|
var indexReady = false;
|
|
5392
5409
|
var indexError2 = null;
|
|
@@ -5502,6 +5519,11 @@ function checkAndRefreshIfStale() {
|
|
|
5502
5519
|
console.error(`[Flywheel] Reloaded ${dbIndex._metadata.total_entities} entities`);
|
|
5503
5520
|
}
|
|
5504
5521
|
}
|
|
5522
|
+
const freshRecency = loadRecencyFromStateDb();
|
|
5523
|
+
if (freshRecency && freshRecency.lastUpdated > (recencyIndex?.lastUpdated ?? 0)) {
|
|
5524
|
+
recencyIndex = freshRecency;
|
|
5525
|
+
console.error(`[Flywheel] Refreshed recency index (${freshRecency.lastMentioned.size} entities)`);
|
|
5526
|
+
}
|
|
5505
5527
|
} catch (e) {
|
|
5506
5528
|
console.error("[Flywheel] Failed to check for stale entities:", e);
|
|
5507
5529
|
}
|
|
@@ -7305,7 +7327,7 @@ var GetBacklinksOutputSchema = {
|
|
|
7305
7327
|
returned_count: z.coerce.number().describe("Number of backlinks returned (may be limited)"),
|
|
7306
7328
|
backlinks: z.array(BacklinkItemSchema).describe("List of backlinks")
|
|
7307
7329
|
};
|
|
7308
|
-
function registerGraphTools(server2, getIndex, getVaultPath) {
|
|
7330
|
+
function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
7309
7331
|
server2.registerTool(
|
|
7310
7332
|
"get_backlinks",
|
|
7311
7333
|
{
|
|
@@ -7427,6 +7449,89 @@ function registerGraphTools(server2, getIndex, getVaultPath) {
|
|
|
7427
7449
|
};
|
|
7428
7450
|
}
|
|
7429
7451
|
);
|
|
7452
|
+
server2.tool(
|
|
7453
|
+
"get_weighted_links",
|
|
7454
|
+
"Get outgoing links from a note ranked by edge weight. Weights reflect link survival, co-session access, and source activity. Time decay is applied at query time.",
|
|
7455
|
+
{
|
|
7456
|
+
path: z.string().describe('Path to the note (e.g., "daily/2026-02-24.md")'),
|
|
7457
|
+
min_weight: z.number().default(0).describe("Minimum weight threshold (default 0 = all links)"),
|
|
7458
|
+
limit: z.number().default(20).describe("Maximum number of results to return")
|
|
7459
|
+
},
|
|
7460
|
+
async ({ path: notePath, min_weight, limit: requestedLimit }) => {
|
|
7461
|
+
const stateDb2 = getStateDb?.();
|
|
7462
|
+
if (!stateDb2) {
|
|
7463
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
|
|
7464
|
+
}
|
|
7465
|
+
const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
|
|
7466
|
+
const now = Date.now();
|
|
7467
|
+
const rows = stateDb2.db.prepare(`
|
|
7468
|
+
SELECT target, weight, weight_updated_at
|
|
7469
|
+
FROM note_links
|
|
7470
|
+
WHERE note_path = ?
|
|
7471
|
+
ORDER BY weight DESC
|
|
7472
|
+
`).all(notePath);
|
|
7473
|
+
const results = rows.map((row) => {
|
|
7474
|
+
const daysSinceUpdated = row.weight_updated_at ? (now - row.weight_updated_at) / (1e3 * 60 * 60 * 24) : 0;
|
|
7475
|
+
const decayFactor = Math.max(0.1, 1 - daysSinceUpdated / 180);
|
|
7476
|
+
const effectiveWeight = Math.round(row.weight * decayFactor * 1e3) / 1e3;
|
|
7477
|
+
return {
|
|
7478
|
+
target: row.target,
|
|
7479
|
+
weight: row.weight,
|
|
7480
|
+
weight_effective: effectiveWeight,
|
|
7481
|
+
last_updated: row.weight_updated_at
|
|
7482
|
+
};
|
|
7483
|
+
}).filter((r) => r.weight_effective >= min_weight).slice(0, limit);
|
|
7484
|
+
return {
|
|
7485
|
+
content: [{
|
|
7486
|
+
type: "text",
|
|
7487
|
+
text: JSON.stringify({
|
|
7488
|
+
note: notePath,
|
|
7489
|
+
count: results.length,
|
|
7490
|
+
links: results
|
|
7491
|
+
}, null, 2)
|
|
7492
|
+
}]
|
|
7493
|
+
};
|
|
7494
|
+
}
|
|
7495
|
+
);
|
|
7496
|
+
server2.tool(
|
|
7497
|
+
"get_strong_connections",
|
|
7498
|
+
"Get bidirectional connections for a note ranked by combined edge weight. Returns both outgoing and incoming links.",
|
|
7499
|
+
{
|
|
7500
|
+
path: z.string().describe('Path to the note (e.g., "daily/2026-02-24.md")'),
|
|
7501
|
+
limit: z.number().default(20).describe("Maximum number of results to return")
|
|
7502
|
+
},
|
|
7503
|
+
async ({ path: notePath, limit: requestedLimit }) => {
|
|
7504
|
+
const stateDb2 = getStateDb?.();
|
|
7505
|
+
if (!stateDb2) {
|
|
7506
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
|
|
7507
|
+
}
|
|
7508
|
+
const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
|
|
7509
|
+
const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? "";
|
|
7510
|
+
const rows = stateDb2.db.prepare(`
|
|
7511
|
+
SELECT target AS node, weight, 'outgoing' AS direction
|
|
7512
|
+
FROM note_links WHERE note_path = ?
|
|
7513
|
+
UNION ALL
|
|
7514
|
+
SELECT note_path AS node, weight, 'incoming' AS direction
|
|
7515
|
+
FROM note_links WHERE target = ?
|
|
7516
|
+
ORDER BY weight DESC
|
|
7517
|
+
LIMIT ?
|
|
7518
|
+
`).all(notePath, stem2, limit);
|
|
7519
|
+
return {
|
|
7520
|
+
content: [{
|
|
7521
|
+
type: "text",
|
|
7522
|
+
text: JSON.stringify({
|
|
7523
|
+
note: notePath,
|
|
7524
|
+
count: rows.length,
|
|
7525
|
+
connections: rows.map((r) => ({
|
|
7526
|
+
node: r.node,
|
|
7527
|
+
weight: r.weight,
|
|
7528
|
+
direction: r.direction
|
|
7529
|
+
}))
|
|
7530
|
+
}, null, 2)
|
|
7531
|
+
}]
|
|
7532
|
+
};
|
|
7533
|
+
}
|
|
7534
|
+
);
|
|
7430
7535
|
}
|
|
7431
7536
|
|
|
7432
7537
|
// src/tools/read/wikilinks.ts
|
|
@@ -8817,9 +8922,11 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
8817
8922
|
// Entity options (used with scope "entities")
|
|
8818
8923
|
prefix: z4.boolean().default(false).describe("Enable prefix matching for entity search (autocomplete)"),
|
|
8819
8924
|
// Pagination
|
|
8820
|
-
limit: z4.number().default(20).describe("Maximum number of results to return")
|
|
8925
|
+
limit: z4.number().default(20).describe("Maximum number of results to return"),
|
|
8926
|
+
// Context boost (edge weights)
|
|
8927
|
+
context_note: z4.string().optional().describe("Path of the note providing context. When set, results connected to this note via weighted edges get an RRF boost.")
|
|
8821
8928
|
},
|
|
8822
|
-
async ({ query, scope, where, has_tag, has_any_tag, has_all_tags, include_children, folder, title_contains, modified_after, modified_before, sort_by, order, prefix, limit: requestedLimit }) => {
|
|
8929
|
+
async ({ query, scope, where, has_tag, has_any_tag, has_all_tags, include_children, folder, title_contains, modified_after, modified_before, sort_by, order, prefix, limit: requestedLimit, context_note }) => {
|
|
8823
8930
|
const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
|
|
8824
8931
|
const index = getIndex();
|
|
8825
8932
|
const vaultPath2 = getVaultPath();
|
|
@@ -8930,17 +9037,47 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
8930
9037
|
}
|
|
8931
9038
|
}
|
|
8932
9039
|
}
|
|
9040
|
+
let edgeRanked = [];
|
|
9041
|
+
if (context_note) {
|
|
9042
|
+
const ctxStateDb = getStateDb();
|
|
9043
|
+
if (ctxStateDb) {
|
|
9044
|
+
try {
|
|
9045
|
+
const edgeRows = ctxStateDb.db.prepare(`
|
|
9046
|
+
SELECT nl.target, nl.weight FROM note_links nl
|
|
9047
|
+
WHERE nl.note_path = ? AND nl.weight > 1.0
|
|
9048
|
+
ORDER BY nl.weight DESC LIMIT ?
|
|
9049
|
+
`).all(context_note, limit);
|
|
9050
|
+
if (edgeRows.length > 0) {
|
|
9051
|
+
const entityRows = ctxStateDb.db.prepare(
|
|
9052
|
+
"SELECT path, name_lower FROM entities"
|
|
9053
|
+
).all();
|
|
9054
|
+
const targetToPath = /* @__PURE__ */ new Map();
|
|
9055
|
+
for (const e of entityRows) {
|
|
9056
|
+
targetToPath.set(e.name_lower, e.path);
|
|
9057
|
+
}
|
|
9058
|
+
edgeRanked = edgeRows.map((r) => {
|
|
9059
|
+
const entityPath = targetToPath.get(r.target);
|
|
9060
|
+
return entityPath ? { path: entityPath, title: r.target } : null;
|
|
9061
|
+
}).filter((r) => r !== null);
|
|
9062
|
+
}
|
|
9063
|
+
} catch {
|
|
9064
|
+
}
|
|
9065
|
+
}
|
|
9066
|
+
}
|
|
8933
9067
|
if (hasEmbeddingsIndex()) {
|
|
8934
9068
|
try {
|
|
8935
9069
|
const semanticResults = await semanticSearch(query, limit);
|
|
8936
9070
|
const fts5Ranked = fts5Results.map((r) => ({ path: r.path, title: r.title, snippet: r.snippet }));
|
|
8937
9071
|
const semanticRanked = semanticResults.map((r) => ({ path: r.path, title: r.title }));
|
|
8938
|
-
const
|
|
8939
|
-
const
|
|
9072
|
+
const entityRankedList = entityResults.map((r) => ({ path: r.path, title: r.name }));
|
|
9073
|
+
const rrfLists = [fts5Ranked, semanticRanked, entityRankedList];
|
|
9074
|
+
if (edgeRanked.length > 0) rrfLists.push(edgeRanked);
|
|
9075
|
+
const rrfScores = reciprocalRankFusion(...rrfLists);
|
|
8940
9076
|
const allPaths = /* @__PURE__ */ new Set([
|
|
8941
9077
|
...fts5Results.map((r) => r.path),
|
|
8942
9078
|
...semanticResults.map((r) => r.path),
|
|
8943
|
-
...entityResults.map((r) => r.path)
|
|
9079
|
+
...entityResults.map((r) => r.path),
|
|
9080
|
+
...edgeRanked.map((r) => r.path)
|
|
8944
9081
|
]);
|
|
8945
9082
|
const fts5Map = new Map(fts5Results.map((r) => [r.path, r]));
|
|
8946
9083
|
const semanticMap = new Map(semanticResults.map((r) => [r.path, r]));
|
|
@@ -9065,6 +9202,123 @@ function suggestEntityAliases(stateDb2, folder) {
|
|
|
9065
9202
|
return suggestions;
|
|
9066
9203
|
}
|
|
9067
9204
|
|
|
9205
|
+
// src/core/write/edgeWeights.ts
|
|
9206
|
+
var moduleStateDb5 = null;
|
|
9207
|
+
function setEdgeWeightStateDb(stateDb2) {
|
|
9208
|
+
moduleStateDb5 = stateDb2;
|
|
9209
|
+
}
|
|
9210
|
+
function buildPathToTargetsMap(stateDb2) {
|
|
9211
|
+
const map = /* @__PURE__ */ new Map();
|
|
9212
|
+
const rows = stateDb2.db.prepare(
|
|
9213
|
+
"SELECT path, name_lower, aliases_json FROM entities"
|
|
9214
|
+
).all();
|
|
9215
|
+
for (const row of rows) {
|
|
9216
|
+
const targets = /* @__PURE__ */ new Set();
|
|
9217
|
+
targets.add(row.name_lower);
|
|
9218
|
+
if (row.aliases_json) {
|
|
9219
|
+
try {
|
|
9220
|
+
const aliases = JSON.parse(row.aliases_json);
|
|
9221
|
+
for (const alias of aliases) {
|
|
9222
|
+
targets.add(alias.toLowerCase());
|
|
9223
|
+
}
|
|
9224
|
+
} catch {
|
|
9225
|
+
}
|
|
9226
|
+
}
|
|
9227
|
+
map.set(row.path, targets);
|
|
9228
|
+
}
|
|
9229
|
+
return map;
|
|
9230
|
+
}
|
|
9231
|
+
function pathToFallbackTarget(filePath) {
|
|
9232
|
+
return filePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? filePath.toLowerCase();
|
|
9233
|
+
}
|
|
9234
|
+
function recomputeEdgeWeights(stateDb2) {
|
|
9235
|
+
const start = Date.now();
|
|
9236
|
+
const edges = stateDb2.db.prepare(
|
|
9237
|
+
"SELECT note_path, target FROM note_links"
|
|
9238
|
+
).all();
|
|
9239
|
+
if (edges.length === 0) {
|
|
9240
|
+
return { edges_updated: 0, duration_ms: Date.now() - start };
|
|
9241
|
+
}
|
|
9242
|
+
const survivalMap = /* @__PURE__ */ new Map();
|
|
9243
|
+
const historyRows = stateDb2.db.prepare(
|
|
9244
|
+
"SELECT note_path, target, edits_survived FROM note_link_history"
|
|
9245
|
+
).all();
|
|
9246
|
+
for (const row of historyRows) {
|
|
9247
|
+
survivalMap.set(`${row.note_path}\0${row.target}`, row.edits_survived);
|
|
9248
|
+
}
|
|
9249
|
+
const pathToTargets = buildPathToTargetsMap(stateDb2);
|
|
9250
|
+
const targetToPaths = /* @__PURE__ */ new Map();
|
|
9251
|
+
for (const [entityPath, targets] of pathToTargets) {
|
|
9252
|
+
for (const target of targets) {
|
|
9253
|
+
let paths = targetToPaths.get(target);
|
|
9254
|
+
if (!paths) {
|
|
9255
|
+
paths = /* @__PURE__ */ new Set();
|
|
9256
|
+
targetToPaths.set(target, paths);
|
|
9257
|
+
}
|
|
9258
|
+
paths.add(entityPath);
|
|
9259
|
+
}
|
|
9260
|
+
}
|
|
9261
|
+
const sessionRows = stateDb2.db.prepare(
|
|
9262
|
+
`SELECT session_id, note_paths FROM tool_invocations
|
|
9263
|
+
WHERE note_paths IS NOT NULL AND note_paths != '[]'`
|
|
9264
|
+
).all();
|
|
9265
|
+
const sessionPaths = /* @__PURE__ */ new Map();
|
|
9266
|
+
for (const row of sessionRows) {
|
|
9267
|
+
try {
|
|
9268
|
+
const paths = JSON.parse(row.note_paths);
|
|
9269
|
+
if (!Array.isArray(paths) || paths.length === 0) continue;
|
|
9270
|
+
let existing = sessionPaths.get(row.session_id);
|
|
9271
|
+
if (!existing) {
|
|
9272
|
+
existing = /* @__PURE__ */ new Set();
|
|
9273
|
+
sessionPaths.set(row.session_id, existing);
|
|
9274
|
+
}
|
|
9275
|
+
for (const p of paths) {
|
|
9276
|
+
existing.add(p);
|
|
9277
|
+
}
|
|
9278
|
+
} catch {
|
|
9279
|
+
}
|
|
9280
|
+
}
|
|
9281
|
+
const coSessionCount = /* @__PURE__ */ new Map();
|
|
9282
|
+
const sourceActivityCount = /* @__PURE__ */ new Map();
|
|
9283
|
+
for (const [, paths] of sessionPaths) {
|
|
9284
|
+
const sessionTargets = /* @__PURE__ */ new Set();
|
|
9285
|
+
for (const p of paths) {
|
|
9286
|
+
const targets = pathToTargets.get(p);
|
|
9287
|
+
if (targets) {
|
|
9288
|
+
for (const t of targets) sessionTargets.add(t);
|
|
9289
|
+
} else {
|
|
9290
|
+
sessionTargets.add(pathToFallbackTarget(p));
|
|
9291
|
+
}
|
|
9292
|
+
}
|
|
9293
|
+
for (const edge of edges) {
|
|
9294
|
+
if (paths.has(edge.note_path)) {
|
|
9295
|
+
const srcKey = edge.note_path;
|
|
9296
|
+
sourceActivityCount.set(srcKey, (sourceActivityCount.get(srcKey) ?? 0) + 1);
|
|
9297
|
+
if (sessionTargets.has(edge.target)) {
|
|
9298
|
+
const edgeKey = `${edge.note_path}\0${edge.target}`;
|
|
9299
|
+
coSessionCount.set(edgeKey, (coSessionCount.get(edgeKey) ?? 0) + 1);
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
}
|
|
9303
|
+
}
|
|
9304
|
+
const now = Date.now();
|
|
9305
|
+
const update = stateDb2.db.prepare(
|
|
9306
|
+
"UPDATE note_links SET weight = ?, weight_updated_at = ? WHERE note_path = ? AND target = ?"
|
|
9307
|
+
);
|
|
9308
|
+
const tx = stateDb2.db.transaction(() => {
|
|
9309
|
+
for (const edge of edges) {
|
|
9310
|
+
const edgeKey = `${edge.note_path}\0${edge.target}`;
|
|
9311
|
+
const editsSurvived = survivalMap.get(edgeKey) ?? 0;
|
|
9312
|
+
const coSessions = coSessionCount.get(edgeKey) ?? 0;
|
|
9313
|
+
const sourceAccess = sourceActivityCount.get(edge.note_path) ?? 0;
|
|
9314
|
+
const weight = 1 + editsSurvived * 0.5 + Math.min(coSessions * 0.5, 3) + Math.min(sourceAccess * 0.2, 2);
|
|
9315
|
+
update.run(Math.round(weight * 1e3) / 1e3, now, edge.note_path, edge.target);
|
|
9316
|
+
}
|
|
9317
|
+
});
|
|
9318
|
+
tx();
|
|
9319
|
+
return { edges_updated: edges.length, duration_ms: Date.now() - start };
|
|
9320
|
+
}
|
|
9321
|
+
|
|
9068
9322
|
// src/tools/read/system.ts
|
|
9069
9323
|
function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
|
|
9070
9324
|
const RefreshIndexOutputSchema = {
|
|
@@ -9072,6 +9326,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
9072
9326
|
notes_count: z5.number().describe("Number of notes indexed"),
|
|
9073
9327
|
entities_count: z5.number().describe("Number of entities (titles + aliases)"),
|
|
9074
9328
|
fts5_notes: z5.number().describe("Number of notes in FTS5 search index"),
|
|
9329
|
+
edges_recomputed: z5.number().optional().describe("Number of edges with recomputed weights"),
|
|
9075
9330
|
duration_ms: z5.number().describe("Time taken to rebuild index")
|
|
9076
9331
|
};
|
|
9077
9332
|
server2.registerTool(
|
|
@@ -9139,6 +9394,16 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
9139
9394
|
} catch (err) {
|
|
9140
9395
|
console.error("[Flywheel] FTS5 rebuild failed:", err);
|
|
9141
9396
|
}
|
|
9397
|
+
let edgesRecomputed = 0;
|
|
9398
|
+
if (stateDb2) {
|
|
9399
|
+
try {
|
|
9400
|
+
const edgeResult = recomputeEdgeWeights(stateDb2);
|
|
9401
|
+
edgesRecomputed = edgeResult.edges_updated;
|
|
9402
|
+
console.error(`[Flywheel] Edge weights: ${edgeResult.edges_updated} edges in ${edgeResult.duration_ms}ms`);
|
|
9403
|
+
} catch (err) {
|
|
9404
|
+
console.error("[Flywheel] Edge weight recompute failed:", err);
|
|
9405
|
+
}
|
|
9406
|
+
}
|
|
9142
9407
|
const duration = Date.now() - startTime;
|
|
9143
9408
|
if (stateDb2) {
|
|
9144
9409
|
recordIndexEvent(stateDb2, {
|
|
@@ -9152,6 +9417,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
|
|
|
9152
9417
|
notes_count: newIndex.notes.size,
|
|
9153
9418
|
entities_count: newIndex.entities.size,
|
|
9154
9419
|
fts5_notes: fts5Notes,
|
|
9420
|
+
edges_recomputed: edgesRecomputed,
|
|
9155
9421
|
duration_ms: duration
|
|
9156
9422
|
};
|
|
9157
9423
|
return {
|
|
@@ -12164,6 +12430,13 @@ function normalizeInput(content, format) {
|
|
|
12164
12430
|
normalized = trimmed;
|
|
12165
12431
|
changes.push("Trimmed excessive blank lines");
|
|
12166
12432
|
}
|
|
12433
|
+
const multiLineWikilink = /\[\[([^\]]*\n[^\]]*)\]\]/g;
|
|
12434
|
+
if (multiLineWikilink.test(normalized)) {
|
|
12435
|
+
normalized = normalized.replace(multiLineWikilink, (_match, inner) => {
|
|
12436
|
+
return "[[" + inner.replace(/\s*\n\s*/g, " ").trim() + "]]";
|
|
12437
|
+
});
|
|
12438
|
+
changes.push("Fixed multi-line wikilinks");
|
|
12439
|
+
}
|
|
12167
12440
|
return {
|
|
12168
12441
|
content: normalized,
|
|
12169
12442
|
normalized: changes.length > 0,
|
|
@@ -15413,9 +15686,11 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
15413
15686
|
let result;
|
|
15414
15687
|
switch (mode) {
|
|
15415
15688
|
case "report": {
|
|
15689
|
+
console.error(`[Flywheel] wikilink_feedback report: entity="${entity}" correct=${JSON.stringify(correct)} (type: ${typeof correct})`);
|
|
15416
15690
|
if (!entity || correct === void 0) {
|
|
15417
15691
|
return {
|
|
15418
|
-
content: [{ type: "text", text: JSON.stringify({ error: "entity and correct are required for report mode" }) }]
|
|
15692
|
+
content: [{ type: "text", text: JSON.stringify({ error: "entity and correct are required for report mode" }) }],
|
|
15693
|
+
isError: true
|
|
15419
15694
|
};
|
|
15420
15695
|
}
|
|
15421
15696
|
try {
|
|
@@ -15424,7 +15699,8 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
|
|
|
15424
15699
|
return {
|
|
15425
15700
|
content: [{ type: "text", text: JSON.stringify({
|
|
15426
15701
|
error: `Failed to record feedback: ${e instanceof Error ? e.message : String(e)}`
|
|
15427
|
-
}) }]
|
|
15702
|
+
}) }],
|
|
15703
|
+
isError: true
|
|
15428
15704
|
};
|
|
15429
15705
|
}
|
|
15430
15706
|
const suppressionUpdated = updateSuppressionList(stateDb2) > 0;
|
|
@@ -16933,7 +17209,7 @@ registerSystemTools(
|
|
|
16933
17209
|
},
|
|
16934
17210
|
() => stateDb
|
|
16935
17211
|
);
|
|
16936
|
-
registerGraphTools(server, () => vaultIndex, () => vaultPath);
|
|
17212
|
+
registerGraphTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
16937
17213
|
registerWikilinkTools(server, () => vaultIndex, () => vaultPath);
|
|
16938
17214
|
registerQueryTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
16939
17215
|
registerPrimitiveTools(server, () => vaultIndex, () => vaultPath, () => flywheelConfig);
|
|
@@ -16987,6 +17263,7 @@ async function main() {
|
|
|
16987
17263
|
loadEntityEmbeddingsToMemory();
|
|
16988
17264
|
setWriteStateDb(stateDb);
|
|
16989
17265
|
setRecencyStateDb(stateDb);
|
|
17266
|
+
setEdgeWeightStateDb(stateDb);
|
|
16990
17267
|
} catch (err) {
|
|
16991
17268
|
const msg = err instanceof Error ? err.message : String(err);
|
|
16992
17269
|
serverLog("statedb", `StateDb initialization failed: ${msg}`, "error");
|
|
@@ -17083,6 +17360,8 @@ async function main() {
|
|
|
17083
17360
|
}
|
|
17084
17361
|
}
|
|
17085
17362
|
var DEFAULT_ENTITY_EXCLUDE_FOLDERS = ["node_modules", "templates", "attachments", "tmp"];
|
|
17363
|
+
var lastCooccurrenceRebuildAt = 0;
|
|
17364
|
+
var lastEdgeWeightRebuildAt = 0;
|
|
17086
17365
|
async function updateEntitiesInStateDb() {
|
|
17087
17366
|
if (!stateDb) return;
|
|
17088
17367
|
try {
|
|
@@ -17423,6 +17702,42 @@ async function runPostIndexWork(index) {
|
|
|
17423
17702
|
tracker.end({ error: String(e) });
|
|
17424
17703
|
serverLog("watcher", `Recency: failed: ${e}`);
|
|
17425
17704
|
}
|
|
17705
|
+
tracker.start("cooccurrence", { entity_count: entitiesAfter.length });
|
|
17706
|
+
try {
|
|
17707
|
+
const cooccurrenceAgeMs = lastCooccurrenceRebuildAt > 0 ? Date.now() - lastCooccurrenceRebuildAt : Infinity;
|
|
17708
|
+
if (cooccurrenceAgeMs >= 60 * 60 * 1e3) {
|
|
17709
|
+
const entityNames = entitiesAfter.map((e) => e.name);
|
|
17710
|
+
const cooccurrenceIdx = await mineCooccurrences(vaultPath, entityNames);
|
|
17711
|
+
setCooccurrenceIndex(cooccurrenceIdx);
|
|
17712
|
+
lastCooccurrenceRebuildAt = Date.now();
|
|
17713
|
+
tracker.end({ rebuilt: true, associations: cooccurrenceIdx._metadata.total_associations });
|
|
17714
|
+
serverLog("watcher", `Co-occurrence: rebuilt ${cooccurrenceIdx._metadata.total_associations} associations`);
|
|
17715
|
+
} else {
|
|
17716
|
+
tracker.end({ rebuilt: false, age_ms: cooccurrenceAgeMs });
|
|
17717
|
+
serverLog("watcher", `Co-occurrence: cache valid (${Math.round(cooccurrenceAgeMs / 1e3)}s old)`);
|
|
17718
|
+
}
|
|
17719
|
+
} catch (e) {
|
|
17720
|
+
tracker.end({ error: String(e) });
|
|
17721
|
+
serverLog("watcher", `Co-occurrence: failed: ${e}`);
|
|
17722
|
+
}
|
|
17723
|
+
if (stateDb) {
|
|
17724
|
+
tracker.start("edge_weights", {});
|
|
17725
|
+
try {
|
|
17726
|
+
const edgeWeightAgeMs = lastEdgeWeightRebuildAt > 0 ? Date.now() - lastEdgeWeightRebuildAt : Infinity;
|
|
17727
|
+
if (edgeWeightAgeMs >= 60 * 60 * 1e3) {
|
|
17728
|
+
const result = recomputeEdgeWeights(stateDb);
|
|
17729
|
+
lastEdgeWeightRebuildAt = Date.now();
|
|
17730
|
+
tracker.end({ rebuilt: true, edges: result.edges_updated, duration_ms: result.duration_ms });
|
|
17731
|
+
serverLog("watcher", `Edge weights: ${result.edges_updated} edges in ${result.duration_ms}ms`);
|
|
17732
|
+
} else {
|
|
17733
|
+
tracker.end({ rebuilt: false, age_ms: edgeWeightAgeMs });
|
|
17734
|
+
serverLog("watcher", `Edge weights: cache valid (${Math.round(edgeWeightAgeMs / 1e3)}s old)`);
|
|
17735
|
+
}
|
|
17736
|
+
} catch (e) {
|
|
17737
|
+
tracker.end({ error: String(e) });
|
|
17738
|
+
serverLog("watcher", `Edge weights: failed: ${e}`);
|
|
17739
|
+
}
|
|
17740
|
+
}
|
|
17426
17741
|
if (hasEmbeddingsIndex()) {
|
|
17427
17742
|
tracker.start("note_embeddings", { files: filteredEvents.length });
|
|
17428
17743
|
let embUpdated = 0;
|
|
@@ -17561,6 +17876,7 @@ async function runPostIndexWork(index) {
|
|
|
17561
17876
|
linkDiffs.push({ file: entry.file, ...diff });
|
|
17562
17877
|
}
|
|
17563
17878
|
updateStoredNoteLinks(stateDb, entry.file, currentSet);
|
|
17879
|
+
if (diff.removed.length === 0) continue;
|
|
17564
17880
|
for (const link of currentSet) {
|
|
17565
17881
|
if (!previousSet.has(link)) continue;
|
|
17566
17882
|
upsertHistory.run(entry.file, link);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velvetmonkey/flywheel-memory",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.41",
|
|
4
4
|
"description": "MCP server that gives Claude full read/write access to your Obsidian vault. 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.40",
|
|
56
56
|
"better-sqlite3": "^11.0.0",
|
|
57
57
|
"chokidar": "^4.0.0",
|
|
58
58
|
"gray-matter": "^4.0.3",
|