@velvetmonkey/flywheel-memory 2.0.50 → 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 +1443 -379
- 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) {
|
|
@@ -16390,99 +16502,1150 @@ function registerCorrectionTools(server2, getStateDb) {
|
|
|
16390
16502
|
);
|
|
16391
16503
|
}
|
|
16392
16504
|
|
|
16393
|
-
// src/tools/write/
|
|
16505
|
+
// src/tools/write/memory.ts
|
|
16394
16506
|
import { z as z23 } from "zod";
|
|
16395
|
-
|
|
16396
|
-
|
|
16397
|
-
|
|
16398
|
-
|
|
16399
|
-
|
|
16400
|
-
|
|
16401
|
-
|
|
16402
|
-
|
|
16403
|
-
|
|
16404
|
-
|
|
16405
|
-
|
|
16507
|
+
|
|
16508
|
+
// src/core/write/memory.ts
|
|
16509
|
+
import { recordEntityMention as recordEntityMention2 } from "@velvetmonkey/vault-core";
|
|
16510
|
+
function detectEntities(stateDb2, text) {
|
|
16511
|
+
const allEntities = stateDb2.getAllEntities.all();
|
|
16512
|
+
const detected = /* @__PURE__ */ new Set();
|
|
16513
|
+
const textLower = text.toLowerCase();
|
|
16514
|
+
for (const entity of allEntities) {
|
|
16515
|
+
if (textLower.includes(entity.name_lower)) {
|
|
16516
|
+
detected.add(entity.name);
|
|
16517
|
+
}
|
|
16518
|
+
if (entity.aliases_json) {
|
|
16519
|
+
try {
|
|
16520
|
+
const aliases = JSON.parse(entity.aliases_json);
|
|
16521
|
+
for (const alias of aliases) {
|
|
16522
|
+
if (alias.length >= 3 && textLower.includes(alias.toLowerCase())) {
|
|
16523
|
+
detected.add(entity.name);
|
|
16524
|
+
break;
|
|
16525
|
+
}
|
|
16526
|
+
}
|
|
16527
|
+
} catch {
|
|
16528
|
+
}
|
|
16529
|
+
}
|
|
16530
|
+
}
|
|
16531
|
+
return [...detected];
|
|
16532
|
+
}
|
|
16533
|
+
function updateGraphSignals(stateDb2, memoryKey, entities) {
|
|
16534
|
+
if (entities.length === 0) return;
|
|
16535
|
+
const now = /* @__PURE__ */ new Date();
|
|
16536
|
+
for (const entity of entities) {
|
|
16537
|
+
recordEntityMention2(stateDb2, entity, now);
|
|
16538
|
+
}
|
|
16539
|
+
const sourcePath = `memory:${memoryKey}`;
|
|
16540
|
+
const targets = new Set(entities.map((e) => e.toLowerCase()));
|
|
16541
|
+
updateStoredNoteLinks(stateDb2, sourcePath, targets);
|
|
16542
|
+
}
|
|
16543
|
+
function removeGraphSignals(stateDb2, memoryKey) {
|
|
16544
|
+
const sourcePath = `memory:${memoryKey}`;
|
|
16545
|
+
updateStoredNoteLinks(stateDb2, sourcePath, /* @__PURE__ */ new Set());
|
|
16546
|
+
}
|
|
16547
|
+
function storeMemory(stateDb2, options) {
|
|
16548
|
+
const {
|
|
16549
|
+
key,
|
|
16550
|
+
value,
|
|
16551
|
+
type,
|
|
16552
|
+
entity,
|
|
16553
|
+
confidence = 1,
|
|
16554
|
+
ttl_days,
|
|
16555
|
+
agent_id,
|
|
16556
|
+
session_id,
|
|
16557
|
+
visibility = "shared"
|
|
16558
|
+
} = options;
|
|
16559
|
+
const now = Date.now();
|
|
16560
|
+
const detectedEntities = detectEntities(stateDb2, value);
|
|
16561
|
+
if (entity && !detectedEntities.includes(entity)) {
|
|
16562
|
+
detectedEntities.push(entity);
|
|
16563
|
+
}
|
|
16564
|
+
const entitiesJson = detectedEntities.length > 0 ? JSON.stringify(detectedEntities) : null;
|
|
16565
|
+
const existing = stateDb2.db.prepare(
|
|
16566
|
+
"SELECT id FROM memories WHERE key = ?"
|
|
16567
|
+
).get(key);
|
|
16568
|
+
if (existing) {
|
|
16569
|
+
stateDb2.db.prepare(`
|
|
16570
|
+
UPDATE memories SET
|
|
16571
|
+
value = ?, memory_type = ?, entity = ?, entities_json = ?,
|
|
16572
|
+
source_agent_id = ?, source_session_id = ?,
|
|
16573
|
+
confidence = ?, updated_at = ?, accessed_at = ?,
|
|
16574
|
+
ttl_days = ?, visibility = ?, superseded_by = NULL
|
|
16575
|
+
WHERE key = ?
|
|
16576
|
+
`).run(
|
|
16577
|
+
value,
|
|
16578
|
+
type,
|
|
16579
|
+
entity ?? null,
|
|
16580
|
+
entitiesJson,
|
|
16581
|
+
agent_id ?? null,
|
|
16582
|
+
session_id ?? null,
|
|
16583
|
+
confidence,
|
|
16584
|
+
now,
|
|
16585
|
+
now,
|
|
16586
|
+
ttl_days ?? null,
|
|
16587
|
+
visibility,
|
|
16588
|
+
key
|
|
16589
|
+
);
|
|
16590
|
+
} else {
|
|
16591
|
+
stateDb2.db.prepare(`
|
|
16592
|
+
INSERT INTO memories (key, value, memory_type, entity, entities_json,
|
|
16593
|
+
source_agent_id, source_session_id, confidence,
|
|
16594
|
+
created_at, updated_at, accessed_at, ttl_days, visibility)
|
|
16595
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
16596
|
+
`).run(
|
|
16597
|
+
key,
|
|
16598
|
+
value,
|
|
16599
|
+
type,
|
|
16600
|
+
entity ?? null,
|
|
16601
|
+
entitiesJson,
|
|
16602
|
+
agent_id ?? null,
|
|
16603
|
+
session_id ?? null,
|
|
16604
|
+
confidence,
|
|
16605
|
+
now,
|
|
16606
|
+
now,
|
|
16607
|
+
now,
|
|
16608
|
+
ttl_days ?? null,
|
|
16609
|
+
visibility
|
|
16610
|
+
);
|
|
16611
|
+
}
|
|
16612
|
+
updateGraphSignals(stateDb2, key, detectedEntities);
|
|
16613
|
+
return stateDb2.db.prepare(
|
|
16614
|
+
"SELECT * FROM memories WHERE key = ?"
|
|
16615
|
+
).get(key);
|
|
16616
|
+
}
|
|
16617
|
+
function getMemory(stateDb2, key) {
|
|
16618
|
+
const memory = stateDb2.db.prepare(
|
|
16619
|
+
"SELECT * FROM memories WHERE key = ? AND superseded_by IS NULL"
|
|
16620
|
+
).get(key);
|
|
16621
|
+
if (!memory) return null;
|
|
16622
|
+
stateDb2.db.prepare(
|
|
16623
|
+
"UPDATE memories SET accessed_at = ? WHERE id = ?"
|
|
16624
|
+
).run(Date.now(), memory.id);
|
|
16625
|
+
return memory;
|
|
16626
|
+
}
|
|
16627
|
+
function searchMemories(stateDb2, options) {
|
|
16628
|
+
const { query, type, entity, limit = 20, agent_id } = options;
|
|
16629
|
+
const conditions = ["m.superseded_by IS NULL"];
|
|
16630
|
+
const params = [];
|
|
16631
|
+
if (type) {
|
|
16632
|
+
conditions.push("m.memory_type = ?");
|
|
16633
|
+
params.push(type);
|
|
16634
|
+
}
|
|
16635
|
+
if (entity) {
|
|
16636
|
+
conditions.push("(m.entity = ? COLLATE NOCASE OR m.entities_json LIKE ?)");
|
|
16637
|
+
params.push(entity, `%"${entity}"%`);
|
|
16638
|
+
}
|
|
16639
|
+
if (agent_id) {
|
|
16640
|
+
conditions.push("(m.visibility = 'shared' OR m.source_agent_id = ?)");
|
|
16641
|
+
params.push(agent_id);
|
|
16642
|
+
}
|
|
16643
|
+
const where = conditions.length > 0 ? `AND ${conditions.join(" AND ")}` : "";
|
|
16644
|
+
try {
|
|
16645
|
+
const results = stateDb2.db.prepare(`
|
|
16646
|
+
SELECT m.* FROM memories_fts
|
|
16647
|
+
JOIN memories m ON m.id = memories_fts.rowid
|
|
16648
|
+
WHERE memories_fts MATCH ?
|
|
16649
|
+
${where}
|
|
16650
|
+
ORDER BY bm25(memories_fts)
|
|
16651
|
+
LIMIT ?
|
|
16652
|
+
`).all(query, ...params, limit);
|
|
16653
|
+
return results;
|
|
16654
|
+
} catch (err) {
|
|
16655
|
+
if (err instanceof Error && err.message.includes("fts5: syntax error")) {
|
|
16656
|
+
throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
|
|
16657
|
+
}
|
|
16658
|
+
throw err;
|
|
16659
|
+
}
|
|
16660
|
+
}
|
|
16661
|
+
function listMemories(stateDb2, options = {}) {
|
|
16662
|
+
const { type, entity, limit = 50, agent_id, include_expired = false } = options;
|
|
16663
|
+
const conditions = [];
|
|
16664
|
+
const params = [];
|
|
16665
|
+
if (!include_expired) {
|
|
16666
|
+
conditions.push("superseded_by IS NULL");
|
|
16667
|
+
}
|
|
16668
|
+
if (type) {
|
|
16669
|
+
conditions.push("memory_type = ?");
|
|
16670
|
+
params.push(type);
|
|
16671
|
+
}
|
|
16672
|
+
if (entity) {
|
|
16673
|
+
conditions.push("(entity = ? COLLATE NOCASE OR entities_json LIKE ?)");
|
|
16674
|
+
params.push(entity, `%"${entity}"%`);
|
|
16675
|
+
}
|
|
16676
|
+
if (agent_id) {
|
|
16677
|
+
conditions.push("(visibility = 'shared' OR source_agent_id = ?)");
|
|
16678
|
+
params.push(agent_id);
|
|
16679
|
+
}
|
|
16680
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
16681
|
+
params.push(limit);
|
|
16682
|
+
return stateDb2.db.prepare(
|
|
16683
|
+
`SELECT * FROM memories ${where} ORDER BY updated_at DESC LIMIT ?`
|
|
16684
|
+
).all(...params);
|
|
16685
|
+
}
|
|
16686
|
+
function forgetMemory(stateDb2, key) {
|
|
16687
|
+
const memory = stateDb2.db.prepare(
|
|
16688
|
+
"SELECT id FROM memories WHERE key = ?"
|
|
16689
|
+
).get(key);
|
|
16690
|
+
if (!memory) return false;
|
|
16691
|
+
removeGraphSignals(stateDb2, key);
|
|
16692
|
+
stateDb2.db.prepare("DELETE FROM memories WHERE key = ?").run(key);
|
|
16693
|
+
return true;
|
|
16694
|
+
}
|
|
16695
|
+
function storeSessionSummary(stateDb2, sessionId, summary, options = {}) {
|
|
16696
|
+
const now = Date.now();
|
|
16697
|
+
const { topics, notes_modified, agent_id, started_at, tool_count } = options;
|
|
16698
|
+
stateDb2.db.prepare(`
|
|
16699
|
+
INSERT OR REPLACE INTO session_summaries
|
|
16700
|
+
(session_id, summary, topics_json, notes_modified_json,
|
|
16701
|
+
agent_id, started_at, ended_at, tool_count)
|
|
16702
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
16703
|
+
`).run(
|
|
16704
|
+
sessionId,
|
|
16705
|
+
summary,
|
|
16706
|
+
topics ? JSON.stringify(topics) : null,
|
|
16707
|
+
notes_modified ? JSON.stringify(notes_modified) : null,
|
|
16708
|
+
agent_id ?? null,
|
|
16709
|
+
started_at ?? null,
|
|
16710
|
+
now,
|
|
16711
|
+
tool_count ?? null
|
|
16712
|
+
);
|
|
16713
|
+
return stateDb2.db.prepare(
|
|
16714
|
+
"SELECT * FROM session_summaries WHERE session_id = ?"
|
|
16715
|
+
).get(sessionId);
|
|
16716
|
+
}
|
|
16717
|
+
function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
|
|
16718
|
+
if (agent_id) {
|
|
16719
|
+
return stateDb2.db.prepare(
|
|
16720
|
+
"SELECT * FROM session_summaries WHERE agent_id = ? ORDER BY ended_at DESC LIMIT ?"
|
|
16721
|
+
).all(agent_id, limit);
|
|
16722
|
+
}
|
|
16723
|
+
return stateDb2.db.prepare(
|
|
16724
|
+
"SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
|
|
16725
|
+
).all(limit);
|
|
16726
|
+
}
|
|
16727
|
+
function findContradictions2(stateDb2, entity) {
|
|
16728
|
+
const conditions = ["superseded_by IS NULL"];
|
|
16729
|
+
const params = [];
|
|
16730
|
+
if (entity) {
|
|
16731
|
+
conditions.push("(entity = ? COLLATE NOCASE OR entities_json LIKE ?)");
|
|
16732
|
+
params.push(entity, `%"${entity}"%`);
|
|
16733
|
+
}
|
|
16734
|
+
const where = `WHERE ${conditions.join(" AND ")}`;
|
|
16735
|
+
const memories = stateDb2.db.prepare(
|
|
16736
|
+
`SELECT * FROM memories ${where} ORDER BY entity, key, updated_at DESC`
|
|
16737
|
+
).all(...params);
|
|
16738
|
+
const contradictions = [];
|
|
16739
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
16740
|
+
for (const m of memories) {
|
|
16741
|
+
const group = m.key;
|
|
16742
|
+
const list = byKey.get(group) || [];
|
|
16743
|
+
list.push(m);
|
|
16744
|
+
byKey.set(group, list);
|
|
16745
|
+
}
|
|
16746
|
+
for (const [, mems] of byKey) {
|
|
16747
|
+
if (mems.length > 1) {
|
|
16748
|
+
for (let i = 0; i < mems.length - 1; i++) {
|
|
16749
|
+
contradictions.push({ memory_a: mems[i], memory_b: mems[i + 1] });
|
|
16406
16750
|
}
|
|
16751
|
+
}
|
|
16752
|
+
}
|
|
16753
|
+
return contradictions;
|
|
16754
|
+
}
|
|
16755
|
+
|
|
16756
|
+
// src/tools/write/memory.ts
|
|
16757
|
+
function registerMemoryTools(server2, getStateDb) {
|
|
16758
|
+
server2.tool(
|
|
16759
|
+
"memory",
|
|
16760
|
+
"Store, retrieve, search, and manage agent working memory. Actions: store, get, search, list, forget, summarize_session.",
|
|
16761
|
+
{
|
|
16762
|
+
action: z23.enum(["store", "get", "search", "list", "forget", "summarize_session"]).describe("Action to perform"),
|
|
16763
|
+
// store params
|
|
16764
|
+
key: z23.string().optional().describe('Memory key (e.g., "user.pref.theme", "project.x.deadline")'),
|
|
16765
|
+
value: z23.string().optional().describe("The fact/preference/observation to store (up to 2000 chars)"),
|
|
16766
|
+
type: z23.enum(["fact", "preference", "observation", "summary"]).optional().describe("Memory type"),
|
|
16767
|
+
entity: z23.string().optional().describe("Primary entity association"),
|
|
16768
|
+
confidence: z23.number().min(0).max(1).optional().describe("Confidence level (0-1, default 1.0)"),
|
|
16769
|
+
ttl_days: z23.number().min(1).optional().describe("Time-to-live in days (null = permanent)"),
|
|
16770
|
+
// search params
|
|
16771
|
+
query: z23.string().optional().describe("FTS5 search query"),
|
|
16772
|
+
// list/search params
|
|
16773
|
+
limit: z23.number().min(1).max(200).optional().describe("Max results to return"),
|
|
16774
|
+
// summarize_session params
|
|
16775
|
+
session_id: z23.string().optional().describe("Session ID for summarize_session"),
|
|
16776
|
+
summary: z23.string().optional().describe("Session summary text"),
|
|
16777
|
+
topics: z23.array(z23.string()).optional().describe("Topics discussed in session"),
|
|
16778
|
+
notes_modified: z23.array(z23.string()).optional().describe("Note paths modified during session"),
|
|
16779
|
+
tool_count: z23.number().optional().describe("Number of tool calls in session")
|
|
16407
16780
|
},
|
|
16408
|
-
async (
|
|
16409
|
-
|
|
16781
|
+
async (args) => {
|
|
16782
|
+
const stateDb2 = getStateDb();
|
|
16783
|
+
if (!stateDb2) {
|
|
16784
|
+
return {
|
|
16785
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
16786
|
+
isError: true
|
|
16787
|
+
};
|
|
16788
|
+
}
|
|
16789
|
+
const agentId = process.env.FLYWHEEL_AGENT_ID || void 0;
|
|
16790
|
+
let sessionId;
|
|
16791
|
+
try {
|
|
16792
|
+
const { getSessionId: getSessionId2 } = await import("@velvetmonkey/vault-core");
|
|
16793
|
+
sessionId = getSessionId2();
|
|
16794
|
+
} catch {
|
|
16795
|
+
}
|
|
16796
|
+
switch (args.action) {
|
|
16797
|
+
case "store": {
|
|
16798
|
+
if (!args.key || !args.value || !args.type) {
|
|
16799
|
+
return {
|
|
16800
|
+
content: [{ type: "text", text: JSON.stringify({ error: "store requires key, value, and type" }) }],
|
|
16801
|
+
isError: true
|
|
16802
|
+
};
|
|
16803
|
+
}
|
|
16804
|
+
if (args.value.length > 2e3) {
|
|
16805
|
+
return {
|
|
16806
|
+
content: [{ type: "text", text: JSON.stringify({ error: "value must be 2000 chars or less" }) }],
|
|
16807
|
+
isError: true
|
|
16808
|
+
};
|
|
16809
|
+
}
|
|
16810
|
+
const memory = storeMemory(stateDb2, {
|
|
16811
|
+
key: args.key,
|
|
16812
|
+
value: args.value,
|
|
16813
|
+
type: args.type,
|
|
16814
|
+
entity: args.entity,
|
|
16815
|
+
confidence: args.confidence,
|
|
16816
|
+
ttl_days: args.ttl_days,
|
|
16817
|
+
agent_id: agentId,
|
|
16818
|
+
session_id: sessionId
|
|
16819
|
+
});
|
|
16820
|
+
return {
|
|
16821
|
+
content: [{
|
|
16822
|
+
type: "text",
|
|
16823
|
+
text: JSON.stringify({
|
|
16824
|
+
stored: true,
|
|
16825
|
+
memory: {
|
|
16826
|
+
key: memory.key,
|
|
16827
|
+
value: memory.value,
|
|
16828
|
+
type: memory.memory_type,
|
|
16829
|
+
entity: memory.entity,
|
|
16830
|
+
entities_detected: memory.entities_json ? JSON.parse(memory.entities_json) : [],
|
|
16831
|
+
confidence: memory.confidence
|
|
16832
|
+
}
|
|
16833
|
+
}, null, 2)
|
|
16834
|
+
}]
|
|
16835
|
+
};
|
|
16836
|
+
}
|
|
16410
16837
|
case "get": {
|
|
16411
|
-
|
|
16838
|
+
if (!args.key) {
|
|
16839
|
+
return {
|
|
16840
|
+
content: [{ type: "text", text: JSON.stringify({ error: "get requires key" }) }],
|
|
16841
|
+
isError: true
|
|
16842
|
+
};
|
|
16843
|
+
}
|
|
16844
|
+
const memory = getMemory(stateDb2, args.key);
|
|
16845
|
+
if (!memory) {
|
|
16846
|
+
return {
|
|
16847
|
+
content: [{ type: "text", text: JSON.stringify({ found: false, key: args.key }) }]
|
|
16848
|
+
};
|
|
16849
|
+
}
|
|
16412
16850
|
return {
|
|
16413
|
-
content: [{
|
|
16851
|
+
content: [{
|
|
16852
|
+
type: "text",
|
|
16853
|
+
text: JSON.stringify({
|
|
16854
|
+
found: true,
|
|
16855
|
+
memory: {
|
|
16856
|
+
key: memory.key,
|
|
16857
|
+
value: memory.value,
|
|
16858
|
+
type: memory.memory_type,
|
|
16859
|
+
entity: memory.entity,
|
|
16860
|
+
entities: memory.entities_json ? JSON.parse(memory.entities_json) : [],
|
|
16861
|
+
confidence: memory.confidence,
|
|
16862
|
+
created_at: memory.created_at,
|
|
16863
|
+
updated_at: memory.updated_at,
|
|
16864
|
+
accessed_at: memory.accessed_at
|
|
16865
|
+
}
|
|
16866
|
+
}, null, 2)
|
|
16867
|
+
}]
|
|
16414
16868
|
};
|
|
16415
16869
|
}
|
|
16416
|
-
case "
|
|
16417
|
-
if (!
|
|
16870
|
+
case "search": {
|
|
16871
|
+
if (!args.query) {
|
|
16418
16872
|
return {
|
|
16419
|
-
content: [{ type: "text", text: JSON.stringify({ error: "
|
|
16873
|
+
content: [{ type: "text", text: JSON.stringify({ error: "search requires query" }) }],
|
|
16874
|
+
isError: true
|
|
16420
16875
|
};
|
|
16421
16876
|
}
|
|
16422
|
-
const
|
|
16423
|
-
|
|
16877
|
+
const results = searchMemories(stateDb2, {
|
|
16878
|
+
query: args.query,
|
|
16879
|
+
type: args.type,
|
|
16880
|
+
entity: args.entity,
|
|
16881
|
+
limit: args.limit,
|
|
16882
|
+
agent_id: agentId
|
|
16883
|
+
});
|
|
16884
|
+
return {
|
|
16885
|
+
content: [{
|
|
16886
|
+
type: "text",
|
|
16887
|
+
text: JSON.stringify({
|
|
16888
|
+
results: results.map((m) => ({
|
|
16889
|
+
key: m.key,
|
|
16890
|
+
value: m.value,
|
|
16891
|
+
type: m.memory_type,
|
|
16892
|
+
entity: m.entity,
|
|
16893
|
+
confidence: m.confidence,
|
|
16894
|
+
updated_at: m.updated_at
|
|
16895
|
+
})),
|
|
16896
|
+
count: results.length
|
|
16897
|
+
}, null, 2)
|
|
16898
|
+
}]
|
|
16899
|
+
};
|
|
16900
|
+
}
|
|
16901
|
+
case "list": {
|
|
16902
|
+
const results = listMemories(stateDb2, {
|
|
16903
|
+
type: args.type,
|
|
16904
|
+
entity: args.entity,
|
|
16905
|
+
limit: args.limit,
|
|
16906
|
+
agent_id: agentId
|
|
16907
|
+
});
|
|
16908
|
+
return {
|
|
16909
|
+
content: [{
|
|
16910
|
+
type: "text",
|
|
16911
|
+
text: JSON.stringify({
|
|
16912
|
+
memories: results.map((m) => ({
|
|
16913
|
+
key: m.key,
|
|
16914
|
+
value: m.value,
|
|
16915
|
+
type: m.memory_type,
|
|
16916
|
+
entity: m.entity,
|
|
16917
|
+
confidence: m.confidence,
|
|
16918
|
+
updated_at: m.updated_at
|
|
16919
|
+
})),
|
|
16920
|
+
count: results.length
|
|
16921
|
+
}, null, 2)
|
|
16922
|
+
}]
|
|
16923
|
+
};
|
|
16924
|
+
}
|
|
16925
|
+
case "forget": {
|
|
16926
|
+
if (!args.key) {
|
|
16424
16927
|
return {
|
|
16425
|
-
content: [{ type: "text", text: JSON.stringify({ error: "
|
|
16928
|
+
content: [{ type: "text", text: JSON.stringify({ error: "forget requires key" }) }],
|
|
16929
|
+
isError: true
|
|
16426
16930
|
};
|
|
16427
16931
|
}
|
|
16428
|
-
const
|
|
16429
|
-
const updated = { ...current, [key]: value };
|
|
16430
|
-
saveFlywheelConfigToDb2(stateDb2, updated);
|
|
16431
|
-
const reloaded = loadConfig(stateDb2);
|
|
16432
|
-
setConfig(reloaded);
|
|
16932
|
+
const deleted = forgetMemory(stateDb2, args.key);
|
|
16433
16933
|
return {
|
|
16434
|
-
content: [{
|
|
16934
|
+
content: [{
|
|
16935
|
+
type: "text",
|
|
16936
|
+
text: JSON.stringify({ forgotten: deleted, key: args.key }, null, 2)
|
|
16937
|
+
}]
|
|
16938
|
+
};
|
|
16939
|
+
}
|
|
16940
|
+
case "summarize_session": {
|
|
16941
|
+
const sid = args.session_id || sessionId;
|
|
16942
|
+
if (!sid || !args.summary) {
|
|
16943
|
+
return {
|
|
16944
|
+
content: [{ type: "text", text: JSON.stringify({ error: "summarize_session requires session_id and summary" }) }],
|
|
16945
|
+
isError: true
|
|
16946
|
+
};
|
|
16947
|
+
}
|
|
16948
|
+
const result = storeSessionSummary(stateDb2, sid, args.summary, {
|
|
16949
|
+
topics: args.topics,
|
|
16950
|
+
notes_modified: args.notes_modified,
|
|
16951
|
+
agent_id: agentId,
|
|
16952
|
+
tool_count: args.tool_count
|
|
16953
|
+
});
|
|
16954
|
+
return {
|
|
16955
|
+
content: [{
|
|
16956
|
+
type: "text",
|
|
16957
|
+
text: JSON.stringify({
|
|
16958
|
+
stored: true,
|
|
16959
|
+
session_id: result.session_id,
|
|
16960
|
+
summary_length: result.summary.length
|
|
16961
|
+
}, null, 2)
|
|
16962
|
+
}]
|
|
16435
16963
|
};
|
|
16436
16964
|
}
|
|
16965
|
+
default:
|
|
16966
|
+
return {
|
|
16967
|
+
content: [{ type: "text", text: JSON.stringify({ error: `Unknown action: ${args.action}` }) }],
|
|
16968
|
+
isError: true
|
|
16969
|
+
};
|
|
16437
16970
|
}
|
|
16438
16971
|
}
|
|
16439
16972
|
);
|
|
16440
16973
|
}
|
|
16441
16974
|
|
|
16442
|
-
// src/tools/
|
|
16975
|
+
// src/tools/read/recall.ts
|
|
16443
16976
|
import { z as z24 } from "zod";
|
|
16444
|
-
import
|
|
16445
|
-
|
|
16446
|
-
|
|
16447
|
-
|
|
16448
|
-
const
|
|
16449
|
-
|
|
16450
|
-
const
|
|
16451
|
-
|
|
16977
|
+
import { searchEntities as searchEntitiesDb2 } from "@velvetmonkey/vault-core";
|
|
16978
|
+
function scoreTextRelevance(query, content) {
|
|
16979
|
+
const queryTokens = tokenize(query).map((t) => t.toLowerCase());
|
|
16980
|
+
const queryStems = queryTokens.map((t) => stem(t));
|
|
16981
|
+
const contentLower = content.toLowerCase();
|
|
16982
|
+
const contentTokens = new Set(tokenize(contentLower));
|
|
16983
|
+
const contentStems = new Set([...contentTokens].map((t) => stem(t)));
|
|
16984
|
+
let score = 0;
|
|
16985
|
+
for (let i = 0; i < queryTokens.length; i++) {
|
|
16986
|
+
const token = queryTokens[i];
|
|
16987
|
+
const stemmed = queryStems[i];
|
|
16988
|
+
if (contentTokens.has(token)) {
|
|
16989
|
+
score += 10;
|
|
16990
|
+
} else if (contentStems.has(stemmed)) {
|
|
16991
|
+
score += 5;
|
|
16992
|
+
}
|
|
16993
|
+
}
|
|
16994
|
+
if (contentLower.includes(query.toLowerCase())) {
|
|
16995
|
+
score += 15;
|
|
16996
|
+
}
|
|
16997
|
+
return score;
|
|
16452
16998
|
}
|
|
16453
|
-
|
|
16999
|
+
function getEdgeWeightBoost(entityName, edgeWeightMap) {
|
|
17000
|
+
const avgWeight = edgeWeightMap.get(entityName.toLowerCase());
|
|
17001
|
+
if (!avgWeight || avgWeight <= 1) return 0;
|
|
17002
|
+
return Math.min((avgWeight - 1) * 3, 6);
|
|
17003
|
+
}
|
|
17004
|
+
async function performRecall(stateDb2, query, options = {}) {
|
|
17005
|
+
const {
|
|
17006
|
+
max_results = 20,
|
|
17007
|
+
focus = "all",
|
|
17008
|
+
entity,
|
|
17009
|
+
max_tokens
|
|
17010
|
+
} = options;
|
|
16454
17011
|
const results = [];
|
|
16455
|
-
|
|
16456
|
-
|
|
16457
|
-
|
|
16458
|
-
|
|
16459
|
-
|
|
16460
|
-
|
|
16461
|
-
|
|
16462
|
-
const
|
|
16463
|
-
|
|
16464
|
-
|
|
16465
|
-
|
|
17012
|
+
const recencyIndex2 = loadRecencyFromStateDb();
|
|
17013
|
+
const edgeWeightMap = getEntityEdgeWeightMap(stateDb2);
|
|
17014
|
+
const feedbackBoosts = getAllFeedbackBoosts(stateDb2);
|
|
17015
|
+
if (focus === "all" || focus === "entities") {
|
|
17016
|
+
try {
|
|
17017
|
+
const entityResults = searchEntitiesDb2(stateDb2, query, max_results);
|
|
17018
|
+
for (const e of entityResults) {
|
|
17019
|
+
const textScore = scoreTextRelevance(query, `${e.name} ${e.description || ""}`);
|
|
17020
|
+
const recency = recencyIndex2 ? getRecencyBoost(e.name, recencyIndex2) : 0;
|
|
17021
|
+
const feedback = feedbackBoosts.get(e.name) ?? 0;
|
|
17022
|
+
const edgeWeight = getEdgeWeightBoost(e.name, edgeWeightMap);
|
|
17023
|
+
const total = textScore + recency + feedback + edgeWeight;
|
|
17024
|
+
if (total > 0) {
|
|
17025
|
+
results.push({
|
|
17026
|
+
type: "entity",
|
|
17027
|
+
id: e.name,
|
|
17028
|
+
content: e.description || `Entity: ${e.name} (${e.category})`,
|
|
17029
|
+
score: total,
|
|
17030
|
+
breakdown: {
|
|
17031
|
+
textRelevance: textScore,
|
|
17032
|
+
recencyBoost: recency,
|
|
17033
|
+
cooccurrenceBoost: 0,
|
|
17034
|
+
feedbackBoost: feedback,
|
|
17035
|
+
edgeWeightBoost: edgeWeight,
|
|
17036
|
+
semanticBoost: 0
|
|
17037
|
+
}
|
|
17038
|
+
});
|
|
17039
|
+
}
|
|
16466
17040
|
}
|
|
17041
|
+
} catch {
|
|
16467
17042
|
}
|
|
16468
|
-
} catch {
|
|
16469
17043
|
}
|
|
16470
|
-
|
|
16471
|
-
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
|
|
16476
|
-
|
|
16477
|
-
|
|
16478
|
-
|
|
16479
|
-
|
|
16480
|
-
|
|
16481
|
-
|
|
16482
|
-
|
|
16483
|
-
|
|
16484
|
-
|
|
16485
|
-
|
|
17044
|
+
if (focus === "all" || focus === "notes") {
|
|
17045
|
+
try {
|
|
17046
|
+
const noteResults = searchFTS5("", query, max_results);
|
|
17047
|
+
for (const n of noteResults) {
|
|
17048
|
+
const textScore = Math.max(10, scoreTextRelevance(query, `${n.title || ""} ${n.snippet || ""}`));
|
|
17049
|
+
results.push({
|
|
17050
|
+
type: "note",
|
|
17051
|
+
id: n.path,
|
|
17052
|
+
content: n.snippet || n.title || n.path,
|
|
17053
|
+
score: textScore,
|
|
17054
|
+
breakdown: {
|
|
17055
|
+
textRelevance: textScore,
|
|
17056
|
+
recencyBoost: 0,
|
|
17057
|
+
cooccurrenceBoost: 0,
|
|
17058
|
+
feedbackBoost: 0,
|
|
17059
|
+
edgeWeightBoost: 0,
|
|
17060
|
+
semanticBoost: 0
|
|
17061
|
+
}
|
|
17062
|
+
});
|
|
17063
|
+
}
|
|
17064
|
+
} catch {
|
|
17065
|
+
}
|
|
17066
|
+
}
|
|
17067
|
+
if (focus === "all" || focus === "memories") {
|
|
17068
|
+
try {
|
|
17069
|
+
const memResults = searchMemories(stateDb2, {
|
|
17070
|
+
query,
|
|
17071
|
+
entity,
|
|
17072
|
+
limit: max_results
|
|
17073
|
+
});
|
|
17074
|
+
for (const m of memResults) {
|
|
17075
|
+
const textScore = scoreTextRelevance(query, `${m.key} ${m.value}`);
|
|
17076
|
+
const memScore = textScore + m.confidence * 5;
|
|
17077
|
+
results.push({
|
|
17078
|
+
type: "memory",
|
|
17079
|
+
id: m.key,
|
|
17080
|
+
content: m.value,
|
|
17081
|
+
score: memScore,
|
|
17082
|
+
breakdown: {
|
|
17083
|
+
textRelevance: textScore,
|
|
17084
|
+
recencyBoost: 0,
|
|
17085
|
+
cooccurrenceBoost: 0,
|
|
17086
|
+
feedbackBoost: m.confidence * 5,
|
|
17087
|
+
edgeWeightBoost: 0,
|
|
17088
|
+
semanticBoost: 0
|
|
17089
|
+
}
|
|
17090
|
+
});
|
|
17091
|
+
}
|
|
17092
|
+
} catch {
|
|
17093
|
+
}
|
|
17094
|
+
}
|
|
17095
|
+
if ((focus === "all" || focus === "entities") && query.length >= 20 && hasEntityEmbeddingsIndex()) {
|
|
17096
|
+
try {
|
|
17097
|
+
const embedding = await embedTextCached(query);
|
|
17098
|
+
const semanticMatches = findSemanticallySimilarEntities(embedding, max_results);
|
|
17099
|
+
for (const match of semanticMatches) {
|
|
17100
|
+
if (match.similarity < 0.3) continue;
|
|
17101
|
+
const boost = match.similarity * 15;
|
|
17102
|
+
const existing = results.find((r) => r.type === "entity" && r.id === match.entityName);
|
|
17103
|
+
if (existing) {
|
|
17104
|
+
existing.score += boost;
|
|
17105
|
+
existing.breakdown.semanticBoost = boost;
|
|
17106
|
+
} else {
|
|
17107
|
+
results.push({
|
|
17108
|
+
type: "entity",
|
|
17109
|
+
id: match.entityName,
|
|
17110
|
+
content: `Semantically similar to: "${query}"`,
|
|
17111
|
+
score: boost,
|
|
17112
|
+
breakdown: {
|
|
17113
|
+
textRelevance: 0,
|
|
17114
|
+
recencyBoost: 0,
|
|
17115
|
+
cooccurrenceBoost: 0,
|
|
17116
|
+
feedbackBoost: 0,
|
|
17117
|
+
edgeWeightBoost: 0,
|
|
17118
|
+
semanticBoost: boost
|
|
17119
|
+
}
|
|
17120
|
+
});
|
|
17121
|
+
}
|
|
17122
|
+
}
|
|
17123
|
+
} catch {
|
|
17124
|
+
}
|
|
17125
|
+
}
|
|
17126
|
+
results.sort((a, b) => b.score - a.score);
|
|
17127
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17128
|
+
const deduped = results.filter((r) => {
|
|
17129
|
+
const key = `${r.type}:${r.id}`;
|
|
17130
|
+
if (seen.has(key)) return false;
|
|
17131
|
+
seen.add(key);
|
|
17132
|
+
return true;
|
|
17133
|
+
});
|
|
17134
|
+
const truncated = deduped.slice(0, max_results);
|
|
17135
|
+
if (max_tokens) {
|
|
17136
|
+
let tokenBudget = max_tokens;
|
|
17137
|
+
const budgeted = [];
|
|
17138
|
+
for (const r of truncated) {
|
|
17139
|
+
const estimatedTokens = Math.ceil(r.content.length / 4);
|
|
17140
|
+
if (tokenBudget - estimatedTokens < 0 && budgeted.length > 0) break;
|
|
17141
|
+
tokenBudget -= estimatedTokens;
|
|
17142
|
+
budgeted.push(r);
|
|
17143
|
+
}
|
|
17144
|
+
return budgeted;
|
|
17145
|
+
}
|
|
17146
|
+
return truncated;
|
|
17147
|
+
}
|
|
17148
|
+
function registerRecallTools(server2, getStateDb) {
|
|
17149
|
+
server2.tool(
|
|
17150
|
+
"recall",
|
|
17151
|
+
"Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
|
|
17152
|
+
{
|
|
17153
|
+
query: z24.string().describe('What to recall (e.g., "Project X", "meetings about auth")'),
|
|
17154
|
+
max_results: z24.number().min(1).max(100).optional().describe("Max results (default: 20)"),
|
|
17155
|
+
focus: z24.enum(["entities", "notes", "memories", "all"]).optional().describe("Limit search to specific type (default: all)"),
|
|
17156
|
+
entity: z24.string().optional().describe("Filter memories by entity association"),
|
|
17157
|
+
max_tokens: z24.number().optional().describe("Token budget for response (truncates lower-ranked results)")
|
|
17158
|
+
},
|
|
17159
|
+
async (args) => {
|
|
17160
|
+
const stateDb2 = getStateDb();
|
|
17161
|
+
if (!stateDb2) {
|
|
17162
|
+
return {
|
|
17163
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
17164
|
+
isError: true
|
|
17165
|
+
};
|
|
17166
|
+
}
|
|
17167
|
+
const results = await performRecall(stateDb2, args.query, {
|
|
17168
|
+
max_results: args.max_results,
|
|
17169
|
+
focus: args.focus,
|
|
17170
|
+
entity: args.entity,
|
|
17171
|
+
max_tokens: args.max_tokens
|
|
17172
|
+
});
|
|
17173
|
+
const entities = results.filter((r) => r.type === "entity");
|
|
17174
|
+
const notes = results.filter((r) => r.type === "note");
|
|
17175
|
+
const memories = results.filter((r) => r.type === "memory");
|
|
17176
|
+
return {
|
|
17177
|
+
content: [{
|
|
17178
|
+
type: "text",
|
|
17179
|
+
text: JSON.stringify({
|
|
17180
|
+
query: args.query,
|
|
17181
|
+
total: results.length,
|
|
17182
|
+
entities: entities.map((e) => ({
|
|
17183
|
+
name: e.id,
|
|
17184
|
+
description: e.content,
|
|
17185
|
+
score: Math.round(e.score * 10) / 10,
|
|
17186
|
+
breakdown: e.breakdown
|
|
17187
|
+
})),
|
|
17188
|
+
notes: notes.map((n) => ({
|
|
17189
|
+
path: n.id,
|
|
17190
|
+
snippet: n.content,
|
|
17191
|
+
score: Math.round(n.score * 10) / 10
|
|
17192
|
+
})),
|
|
17193
|
+
memories: memories.map((m) => ({
|
|
17194
|
+
key: m.id,
|
|
17195
|
+
value: m.content,
|
|
17196
|
+
score: Math.round(m.score * 10) / 10
|
|
17197
|
+
}))
|
|
17198
|
+
}, null, 2)
|
|
17199
|
+
}]
|
|
17200
|
+
};
|
|
17201
|
+
}
|
|
17202
|
+
);
|
|
17203
|
+
}
|
|
17204
|
+
|
|
17205
|
+
// src/tools/read/brief.ts
|
|
17206
|
+
import { z as z25 } from "zod";
|
|
17207
|
+
|
|
17208
|
+
// src/core/shared/toolTracking.ts
|
|
17209
|
+
function recordToolInvocation(stateDb2, event) {
|
|
17210
|
+
stateDb2.db.prepare(
|
|
17211
|
+
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
17212
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
17213
|
+
).run(
|
|
17214
|
+
Date.now(),
|
|
17215
|
+
event.tool_name,
|
|
17216
|
+
event.session_id ?? null,
|
|
17217
|
+
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
17218
|
+
event.duration_ms ?? null,
|
|
17219
|
+
event.success !== false ? 1 : 0
|
|
17220
|
+
);
|
|
17221
|
+
}
|
|
17222
|
+
function rowToInvocation(row) {
|
|
17223
|
+
return {
|
|
17224
|
+
id: row.id,
|
|
17225
|
+
timestamp: row.timestamp,
|
|
17226
|
+
tool_name: row.tool_name,
|
|
17227
|
+
session_id: row.session_id,
|
|
17228
|
+
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
17229
|
+
duration_ms: row.duration_ms,
|
|
17230
|
+
success: row.success === 1
|
|
17231
|
+
};
|
|
17232
|
+
}
|
|
17233
|
+
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
17234
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
17235
|
+
const rows = stateDb2.db.prepare(`
|
|
17236
|
+
SELECT
|
|
17237
|
+
tool_name,
|
|
17238
|
+
COUNT(*) as invocation_count,
|
|
17239
|
+
AVG(duration_ms) as avg_duration_ms,
|
|
17240
|
+
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as success_rate,
|
|
17241
|
+
MAX(timestamp) as last_used
|
|
17242
|
+
FROM tool_invocations
|
|
17243
|
+
WHERE timestamp >= ?
|
|
17244
|
+
GROUP BY tool_name
|
|
17245
|
+
ORDER BY invocation_count DESC
|
|
17246
|
+
`).all(cutoff);
|
|
17247
|
+
return rows.map((r) => ({
|
|
17248
|
+
tool_name: r.tool_name,
|
|
17249
|
+
invocation_count: r.invocation_count,
|
|
17250
|
+
avg_duration_ms: Math.round(r.avg_duration_ms ?? 0),
|
|
17251
|
+
success_rate: Math.round(r.success_rate * 1e3) / 1e3,
|
|
17252
|
+
last_used: r.last_used
|
|
17253
|
+
}));
|
|
17254
|
+
}
|
|
17255
|
+
function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
17256
|
+
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
17257
|
+
const rows = stateDb2.db.prepare(`
|
|
17258
|
+
SELECT note_paths, tool_name, timestamp
|
|
17259
|
+
FROM tool_invocations
|
|
17260
|
+
WHERE timestamp >= ? AND note_paths IS NOT NULL
|
|
17261
|
+
ORDER BY timestamp DESC
|
|
17262
|
+
`).all(cutoff);
|
|
17263
|
+
const noteMap = /* @__PURE__ */ new Map();
|
|
17264
|
+
for (const row of rows) {
|
|
17265
|
+
let paths;
|
|
17266
|
+
try {
|
|
17267
|
+
paths = JSON.parse(row.note_paths);
|
|
17268
|
+
} catch {
|
|
17269
|
+
continue;
|
|
17270
|
+
}
|
|
17271
|
+
for (const p of paths) {
|
|
17272
|
+
const existing = noteMap.get(p);
|
|
17273
|
+
if (existing) {
|
|
17274
|
+
existing.access_count++;
|
|
17275
|
+
existing.last_accessed = Math.max(existing.last_accessed, row.timestamp);
|
|
17276
|
+
existing.tools.add(row.tool_name);
|
|
17277
|
+
} else {
|
|
17278
|
+
noteMap.set(p, {
|
|
17279
|
+
access_count: 1,
|
|
17280
|
+
last_accessed: row.timestamp,
|
|
17281
|
+
tools: /* @__PURE__ */ new Set([row.tool_name])
|
|
17282
|
+
});
|
|
17283
|
+
}
|
|
17284
|
+
}
|
|
17285
|
+
}
|
|
17286
|
+
return Array.from(noteMap.entries()).map(([path33, stats]) => ({
|
|
17287
|
+
path: path33,
|
|
17288
|
+
access_count: stats.access_count,
|
|
17289
|
+
last_accessed: stats.last_accessed,
|
|
17290
|
+
tools_used: Array.from(stats.tools)
|
|
17291
|
+
})).sort((a, b) => b.access_count - a.access_count);
|
|
17292
|
+
}
|
|
17293
|
+
function getSessionHistory(stateDb2, sessionId) {
|
|
17294
|
+
if (sessionId) {
|
|
17295
|
+
const rows2 = stateDb2.db.prepare(`
|
|
17296
|
+
SELECT * FROM tool_invocations
|
|
17297
|
+
WHERE session_id = ?
|
|
17298
|
+
ORDER BY timestamp
|
|
17299
|
+
`).all(sessionId);
|
|
17300
|
+
if (rows2.length === 0) return [];
|
|
17301
|
+
const tools = /* @__PURE__ */ new Set();
|
|
17302
|
+
const notes = /* @__PURE__ */ new Set();
|
|
17303
|
+
for (const row of rows2) {
|
|
17304
|
+
tools.add(row.tool_name);
|
|
17305
|
+
if (row.note_paths) {
|
|
17306
|
+
try {
|
|
17307
|
+
for (const p of JSON.parse(row.note_paths)) {
|
|
17308
|
+
notes.add(p);
|
|
17309
|
+
}
|
|
17310
|
+
} catch {
|
|
17311
|
+
}
|
|
17312
|
+
}
|
|
17313
|
+
}
|
|
17314
|
+
return [{
|
|
17315
|
+
session_id: sessionId,
|
|
17316
|
+
started_at: rows2[0].timestamp,
|
|
17317
|
+
last_activity: rows2[rows2.length - 1].timestamp,
|
|
17318
|
+
tool_count: rows2.length,
|
|
17319
|
+
unique_tools: Array.from(tools),
|
|
17320
|
+
notes_accessed: Array.from(notes)
|
|
17321
|
+
}];
|
|
17322
|
+
}
|
|
17323
|
+
const rows = stateDb2.db.prepare(`
|
|
17324
|
+
SELECT
|
|
17325
|
+
session_id,
|
|
17326
|
+
MIN(timestamp) as started_at,
|
|
17327
|
+
MAX(timestamp) as last_activity,
|
|
17328
|
+
COUNT(*) as tool_count
|
|
17329
|
+
FROM tool_invocations
|
|
17330
|
+
WHERE session_id IS NOT NULL
|
|
17331
|
+
GROUP BY session_id
|
|
17332
|
+
ORDER BY last_activity DESC
|
|
17333
|
+
LIMIT 20
|
|
17334
|
+
`).all();
|
|
17335
|
+
return rows.map((r) => ({
|
|
17336
|
+
session_id: r.session_id,
|
|
17337
|
+
started_at: r.started_at,
|
|
17338
|
+
last_activity: r.last_activity,
|
|
17339
|
+
tool_count: r.tool_count,
|
|
17340
|
+
unique_tools: [],
|
|
17341
|
+
notes_accessed: []
|
|
17342
|
+
}));
|
|
17343
|
+
}
|
|
17344
|
+
function getRecentInvocations(stateDb2, limit = 20) {
|
|
17345
|
+
const rows = stateDb2.db.prepare(
|
|
17346
|
+
"SELECT * FROM tool_invocations ORDER BY timestamp DESC LIMIT ?"
|
|
17347
|
+
).all(limit);
|
|
17348
|
+
return rows.map(rowToInvocation);
|
|
17349
|
+
}
|
|
17350
|
+
function purgeOldInvocations(stateDb2, retentionDays = 90) {
|
|
17351
|
+
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
17352
|
+
const result = stateDb2.db.prepare(
|
|
17353
|
+
"DELETE FROM tool_invocations WHERE timestamp < ?"
|
|
17354
|
+
).run(cutoff);
|
|
17355
|
+
return result.changes;
|
|
17356
|
+
}
|
|
17357
|
+
|
|
17358
|
+
// src/tools/read/brief.ts
|
|
17359
|
+
function estimateTokens2(value) {
|
|
17360
|
+
const str = JSON.stringify(value);
|
|
17361
|
+
return Math.ceil(str.length / 4);
|
|
17362
|
+
}
|
|
17363
|
+
function buildSessionSection(stateDb2, limit) {
|
|
17364
|
+
const summaries = getRecentSessionSummaries(stateDb2, limit);
|
|
17365
|
+
if (summaries.length > 0) {
|
|
17366
|
+
const content2 = summaries.map((s) => ({
|
|
17367
|
+
session_id: s.session_id,
|
|
17368
|
+
summary: s.summary,
|
|
17369
|
+
topics: s.topics_json ? JSON.parse(s.topics_json) : [],
|
|
17370
|
+
ended_at: s.ended_at,
|
|
17371
|
+
tool_count: s.tool_count
|
|
17372
|
+
}));
|
|
17373
|
+
return {
|
|
17374
|
+
name: "recent_sessions",
|
|
17375
|
+
priority: 1,
|
|
17376
|
+
content: content2,
|
|
17377
|
+
estimated_tokens: estimateTokens2(content2)
|
|
17378
|
+
};
|
|
17379
|
+
}
|
|
17380
|
+
const sessions = getSessionHistory(stateDb2);
|
|
17381
|
+
const recentSessions = sessions.slice(0, limit);
|
|
17382
|
+
if (recentSessions.length === 0) {
|
|
17383
|
+
return { name: "recent_sessions", priority: 1, content: [], estimated_tokens: 0 };
|
|
17384
|
+
}
|
|
17385
|
+
const content = recentSessions.map((s) => ({
|
|
17386
|
+
session_id: s.session_id,
|
|
17387
|
+
started_at: s.started_at,
|
|
17388
|
+
last_activity: s.last_activity,
|
|
17389
|
+
tool_count: s.tool_count,
|
|
17390
|
+
tools_used: s.unique_tools
|
|
17391
|
+
}));
|
|
17392
|
+
return {
|
|
17393
|
+
name: "recent_sessions",
|
|
17394
|
+
priority: 1,
|
|
17395
|
+
content,
|
|
17396
|
+
estimated_tokens: estimateTokens2(content)
|
|
17397
|
+
};
|
|
17398
|
+
}
|
|
17399
|
+
function buildActiveEntitiesSection(stateDb2, limit) {
|
|
17400
|
+
const rows = stateDb2.db.prepare(`
|
|
17401
|
+
SELECT r.entity_name_lower, r.last_mentioned_at, r.mention_count,
|
|
17402
|
+
e.name, e.category, e.description
|
|
17403
|
+
FROM recency r
|
|
17404
|
+
LEFT JOIN entities e ON e.name_lower = r.entity_name_lower
|
|
17405
|
+
ORDER BY r.last_mentioned_at DESC
|
|
17406
|
+
LIMIT ?
|
|
17407
|
+
`).all(limit);
|
|
17408
|
+
const content = rows.map((r) => ({
|
|
17409
|
+
name: r.name || r.entity_name_lower,
|
|
17410
|
+
category: r.category,
|
|
17411
|
+
description: r.description,
|
|
17412
|
+
last_mentioned: r.last_mentioned_at,
|
|
17413
|
+
mentions: r.mention_count
|
|
17414
|
+
}));
|
|
17415
|
+
return {
|
|
17416
|
+
name: "active_entities",
|
|
17417
|
+
priority: 2,
|
|
17418
|
+
content,
|
|
17419
|
+
estimated_tokens: estimateTokens2(content)
|
|
17420
|
+
};
|
|
17421
|
+
}
|
|
17422
|
+
function buildActiveMemoriesSection(stateDb2, limit) {
|
|
17423
|
+
const memories = listMemories(stateDb2, { limit });
|
|
17424
|
+
const content = memories.map((m) => ({
|
|
17425
|
+
key: m.key,
|
|
17426
|
+
value: m.value,
|
|
17427
|
+
type: m.memory_type,
|
|
17428
|
+
entity: m.entity,
|
|
17429
|
+
confidence: m.confidence,
|
|
17430
|
+
updated_at: m.updated_at
|
|
17431
|
+
}));
|
|
17432
|
+
return {
|
|
17433
|
+
name: "active_memories",
|
|
17434
|
+
priority: 3,
|
|
17435
|
+
content,
|
|
17436
|
+
estimated_tokens: estimateTokens2(content)
|
|
17437
|
+
};
|
|
17438
|
+
}
|
|
17439
|
+
function buildCorrectionsSection(stateDb2, limit) {
|
|
17440
|
+
const corrections = listCorrections(stateDb2, "pending", void 0, limit);
|
|
17441
|
+
const content = corrections.map((c) => ({
|
|
17442
|
+
id: c.id,
|
|
17443
|
+
type: c.correction_type,
|
|
17444
|
+
description: c.description,
|
|
17445
|
+
entity: c.entity,
|
|
17446
|
+
created_at: c.created_at
|
|
17447
|
+
}));
|
|
17448
|
+
return {
|
|
17449
|
+
name: "pending_corrections",
|
|
17450
|
+
priority: 4,
|
|
17451
|
+
content,
|
|
17452
|
+
estimated_tokens: estimateTokens2(content)
|
|
17453
|
+
};
|
|
17454
|
+
}
|
|
17455
|
+
function buildVaultPulseSection(stateDb2) {
|
|
17456
|
+
const now = Date.now();
|
|
17457
|
+
const day = 864e5;
|
|
17458
|
+
const recentToolCount = stateDb2.db.prepare(
|
|
17459
|
+
"SELECT COUNT(*) as cnt FROM tool_invocations WHERE timestamp > ?"
|
|
17460
|
+
).get(now - day).cnt;
|
|
17461
|
+
const entityCount = stateDb2.db.prepare(
|
|
17462
|
+
"SELECT COUNT(*) as cnt FROM entities"
|
|
17463
|
+
).get().cnt;
|
|
17464
|
+
const memoryCount = stateDb2.db.prepare(
|
|
17465
|
+
"SELECT COUNT(*) as cnt FROM memories WHERE superseded_by IS NULL"
|
|
17466
|
+
).get().cnt;
|
|
17467
|
+
let noteCount = 0;
|
|
17468
|
+
try {
|
|
17469
|
+
noteCount = stateDb2.db.prepare(
|
|
17470
|
+
"SELECT COUNT(*) as cnt FROM notes_fts"
|
|
17471
|
+
).get().cnt;
|
|
17472
|
+
} catch {
|
|
17473
|
+
}
|
|
17474
|
+
const contradictions = findContradictions2(stateDb2);
|
|
17475
|
+
const content = {
|
|
17476
|
+
notes: noteCount,
|
|
17477
|
+
entities: entityCount,
|
|
17478
|
+
memories: memoryCount,
|
|
17479
|
+
tool_calls_24h: recentToolCount,
|
|
17480
|
+
contradictions: contradictions.length
|
|
17481
|
+
};
|
|
17482
|
+
return {
|
|
17483
|
+
name: "vault_pulse",
|
|
17484
|
+
priority: 5,
|
|
17485
|
+
content,
|
|
17486
|
+
estimated_tokens: estimateTokens2(content)
|
|
17487
|
+
};
|
|
17488
|
+
}
|
|
17489
|
+
function registerBriefTools(server2, getStateDb) {
|
|
17490
|
+
server2.tool(
|
|
17491
|
+
"brief",
|
|
17492
|
+
"Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
|
|
17493
|
+
{
|
|
17494
|
+
max_tokens: z25.number().optional().describe("Token budget (lower-priority sections truncated first)"),
|
|
17495
|
+
focus: z25.string().optional().describe("Focus entity or topic (filters content)"),
|
|
17496
|
+
sections: z25.array(z25.enum(["recent_sessions", "active_entities", "active_memories", "pending_corrections", "vault_pulse"])).optional().describe("Which sections to include (default: all)")
|
|
17497
|
+
},
|
|
17498
|
+
async (args) => {
|
|
17499
|
+
const stateDb2 = getStateDb();
|
|
17500
|
+
if (!stateDb2) {
|
|
17501
|
+
return {
|
|
17502
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
|
|
17503
|
+
isError: true
|
|
17504
|
+
};
|
|
17505
|
+
}
|
|
17506
|
+
const requestedSections = args.sections ? new Set(args.sections) : /* @__PURE__ */ new Set(["recent_sessions", "active_entities", "active_memories", "pending_corrections", "vault_pulse"]);
|
|
17507
|
+
const sections = [];
|
|
17508
|
+
if (requestedSections.has("recent_sessions")) {
|
|
17509
|
+
sections.push(buildSessionSection(stateDb2, 5));
|
|
17510
|
+
}
|
|
17511
|
+
if (requestedSections.has("active_entities")) {
|
|
17512
|
+
sections.push(buildActiveEntitiesSection(stateDb2, 10));
|
|
17513
|
+
}
|
|
17514
|
+
if (requestedSections.has("active_memories")) {
|
|
17515
|
+
sections.push(buildActiveMemoriesSection(stateDb2, 20));
|
|
17516
|
+
}
|
|
17517
|
+
if (requestedSections.has("pending_corrections")) {
|
|
17518
|
+
sections.push(buildCorrectionsSection(stateDb2, 10));
|
|
17519
|
+
}
|
|
17520
|
+
if (requestedSections.has("vault_pulse")) {
|
|
17521
|
+
sections.push(buildVaultPulseSection(stateDb2));
|
|
17522
|
+
}
|
|
17523
|
+
if (args.max_tokens) {
|
|
17524
|
+
let totalTokens2 = 0;
|
|
17525
|
+
sections.sort((a, b) => a.priority - b.priority);
|
|
17526
|
+
for (const section of sections) {
|
|
17527
|
+
totalTokens2 += section.estimated_tokens;
|
|
17528
|
+
if (totalTokens2 > args.max_tokens) {
|
|
17529
|
+
if (Array.isArray(section.content)) {
|
|
17530
|
+
const remaining = Math.max(0, args.max_tokens - (totalTokens2 - section.estimated_tokens));
|
|
17531
|
+
const itemTokens = section.estimated_tokens / Math.max(1, section.content.length);
|
|
17532
|
+
const keepCount = Math.max(1, Math.floor(remaining / itemTokens));
|
|
17533
|
+
section.content = section.content.slice(0, keepCount);
|
|
17534
|
+
section.estimated_tokens = estimateTokens2(section.content);
|
|
17535
|
+
}
|
|
17536
|
+
}
|
|
17537
|
+
}
|
|
17538
|
+
}
|
|
17539
|
+
const response = {};
|
|
17540
|
+
let totalTokens = 0;
|
|
17541
|
+
for (const section of sections) {
|
|
17542
|
+
response[section.name] = section.content;
|
|
17543
|
+
totalTokens += section.estimated_tokens;
|
|
17544
|
+
}
|
|
17545
|
+
response._meta = { total_estimated_tokens: totalTokens };
|
|
17546
|
+
return {
|
|
17547
|
+
content: [{
|
|
17548
|
+
type: "text",
|
|
17549
|
+
text: JSON.stringify(response, null, 2)
|
|
17550
|
+
}]
|
|
17551
|
+
};
|
|
17552
|
+
}
|
|
17553
|
+
);
|
|
17554
|
+
}
|
|
17555
|
+
|
|
17556
|
+
// src/tools/write/config.ts
|
|
17557
|
+
import { z as z26 } from "zod";
|
|
17558
|
+
import { saveFlywheelConfigToDb as saveFlywheelConfigToDb2 } from "@velvetmonkey/vault-core";
|
|
17559
|
+
function registerConfigTools(server2, getConfig, setConfig, getStateDb) {
|
|
17560
|
+
server2.registerTool(
|
|
17561
|
+
"flywheel_config",
|
|
17562
|
+
{
|
|
17563
|
+
title: "Flywheel Config",
|
|
17564
|
+
description: 'Read or update Flywheel configuration.\n- "get": Returns the current FlywheelConfig\n- "set": Updates a single config key and returns the updated config\n\nExample: flywheel_config({ mode: "get" })\nExample: flywheel_config({ mode: "set", key: "exclude_analysis_tags", value: ["habit", "daily"] })',
|
|
17565
|
+
inputSchema: {
|
|
17566
|
+
mode: z26.enum(["get", "set"]).describe("Operation mode"),
|
|
17567
|
+
key: z26.string().optional().describe("Config key to update (required for set mode)"),
|
|
17568
|
+
value: z26.unknown().optional().describe("New value for the key (required for set mode)")
|
|
17569
|
+
}
|
|
17570
|
+
},
|
|
17571
|
+
async ({ mode, key, value }) => {
|
|
17572
|
+
switch (mode) {
|
|
17573
|
+
case "get": {
|
|
17574
|
+
const config = getConfig();
|
|
17575
|
+
return {
|
|
17576
|
+
content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
|
|
17577
|
+
};
|
|
17578
|
+
}
|
|
17579
|
+
case "set": {
|
|
17580
|
+
if (!key) {
|
|
17581
|
+
return {
|
|
17582
|
+
content: [{ type: "text", text: JSON.stringify({ error: "key is required for set mode" }) }]
|
|
17583
|
+
};
|
|
17584
|
+
}
|
|
17585
|
+
const stateDb2 = getStateDb();
|
|
17586
|
+
if (!stateDb2) {
|
|
17587
|
+
return {
|
|
17588
|
+
content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
|
|
17589
|
+
};
|
|
17590
|
+
}
|
|
17591
|
+
const current = getConfig();
|
|
17592
|
+
const updated = { ...current, [key]: value };
|
|
17593
|
+
saveFlywheelConfigToDb2(stateDb2, updated);
|
|
17594
|
+
const reloaded = loadConfig(stateDb2);
|
|
17595
|
+
setConfig(reloaded);
|
|
17596
|
+
return {
|
|
17597
|
+
content: [{ type: "text", text: JSON.stringify(reloaded, null, 2) }]
|
|
17598
|
+
};
|
|
17599
|
+
}
|
|
17600
|
+
}
|
|
17601
|
+
}
|
|
17602
|
+
);
|
|
17603
|
+
}
|
|
17604
|
+
|
|
17605
|
+
// src/tools/write/enrich.ts
|
|
17606
|
+
import { z as z27 } from "zod";
|
|
17607
|
+
import * as fs29 from "fs/promises";
|
|
17608
|
+
import * as path30 from "path";
|
|
17609
|
+
function hasSkipWikilinks(content) {
|
|
17610
|
+
if (!content.startsWith("---")) return false;
|
|
17611
|
+
const endIndex = content.indexOf("\n---", 3);
|
|
17612
|
+
if (endIndex === -1) return false;
|
|
17613
|
+
const frontmatter = content.substring(4, endIndex);
|
|
17614
|
+
return /^skipWikilinks:\s*true\s*$/m.test(frontmatter);
|
|
17615
|
+
}
|
|
17616
|
+
async function collectMarkdownFiles(dirPath, basePath, excludeFolders) {
|
|
17617
|
+
const results = [];
|
|
17618
|
+
try {
|
|
17619
|
+
const entries = await fs29.readdir(dirPath, { withFileTypes: true });
|
|
17620
|
+
for (const entry of entries) {
|
|
17621
|
+
if (entry.name.startsWith(".")) continue;
|
|
17622
|
+
const fullPath = path30.join(dirPath, entry.name);
|
|
17623
|
+
if (entry.isDirectory()) {
|
|
17624
|
+
if (excludeFolders.some((f) => entry.name.toLowerCase() === f.toLowerCase())) continue;
|
|
17625
|
+
const sub = await collectMarkdownFiles(fullPath, basePath, excludeFolders);
|
|
17626
|
+
results.push(...sub);
|
|
17627
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
17628
|
+
results.push(path30.relative(basePath, fullPath));
|
|
17629
|
+
}
|
|
17630
|
+
}
|
|
17631
|
+
} catch {
|
|
17632
|
+
}
|
|
17633
|
+
return results;
|
|
17634
|
+
}
|
|
17635
|
+
var EXCLUDE_FOLDERS = [
|
|
17636
|
+
"daily-notes",
|
|
17637
|
+
"daily",
|
|
17638
|
+
"weekly",
|
|
17639
|
+
"weekly-notes",
|
|
17640
|
+
"monthly",
|
|
17641
|
+
"monthly-notes",
|
|
17642
|
+
"quarterly",
|
|
17643
|
+
"yearly-notes",
|
|
17644
|
+
"periodic",
|
|
17645
|
+
"journal",
|
|
17646
|
+
"inbox",
|
|
17647
|
+
"templates",
|
|
17648
|
+
"attachments",
|
|
16486
17649
|
"tmp",
|
|
16487
17650
|
"clippings",
|
|
16488
17651
|
"readwise",
|
|
@@ -16495,9 +17658,9 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
16495
17658
|
"vault_init",
|
|
16496
17659
|
"Initialize vault for Flywheel \u2014 scans legacy notes with zero wikilinks and applies entity links. Safe to re-run (idempotent). Use dry_run (default) to preview.",
|
|
16497
17660
|
{
|
|
16498
|
-
dry_run:
|
|
16499
|
-
batch_size:
|
|
16500
|
-
offset:
|
|
17661
|
+
dry_run: z27.boolean().default(true).describe("If true (default), preview what would be linked without modifying files"),
|
|
17662
|
+
batch_size: z27.number().default(50).describe("Maximum notes to process per invocation (default: 50)"),
|
|
17663
|
+
offset: z27.number().default(0).describe("Skip this many eligible notes (for pagination across invocations)")
|
|
16501
17664
|
},
|
|
16502
17665
|
async ({ dry_run, batch_size, offset }) => {
|
|
16503
17666
|
const startTime = Date.now();
|
|
@@ -16592,7 +17755,7 @@ function registerInitTools(server2, vaultPath2, getStateDb) {
|
|
|
16592
17755
|
}
|
|
16593
17756
|
|
|
16594
17757
|
// src/tools/read/metrics.ts
|
|
16595
|
-
import { z as
|
|
17758
|
+
import { z as z28 } from "zod";
|
|
16596
17759
|
|
|
16597
17760
|
// src/core/shared/metrics.ts
|
|
16598
17761
|
var ALL_METRICS = [
|
|
@@ -16758,10 +17921,10 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16758
17921
|
title: "Vault Growth",
|
|
16759
17922
|
description: 'Track vault growth over time. Modes: "current" (live snapshot), "history" (time series), "trends" (deltas vs N days ago), "index_activity" (rebuild history). Tracks 11 metrics: note_count, link_count, orphan_count, tag_count, entity_count, avg_links_per_note, link_density, connected_ratio, wikilink_accuracy, wikilink_feedback_volume, wikilink_suppressed_count.',
|
|
16760
17923
|
inputSchema: {
|
|
16761
|
-
mode:
|
|
16762
|
-
metric:
|
|
16763
|
-
days_back:
|
|
16764
|
-
limit:
|
|
17924
|
+
mode: z28.enum(["current", "history", "trends", "index_activity"]).describe("Query mode: current snapshot, historical time series, trend analysis, or index rebuild activity"),
|
|
17925
|
+
metric: z28.string().optional().describe('Filter to specific metric (e.g., "note_count"). Omit for all metrics.'),
|
|
17926
|
+
days_back: z28.number().optional().describe("Number of days to look back for history/trends (default: 30)"),
|
|
17927
|
+
limit: z28.number().optional().describe("Number of recent events to return for index_activity mode (default: 20)")
|
|
16765
17928
|
}
|
|
16766
17929
|
},
|
|
16767
17930
|
async ({ mode, metric, days_back, limit: eventLimit }) => {
|
|
@@ -16834,159 +17997,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
|
|
|
16834
17997
|
}
|
|
16835
17998
|
|
|
16836
17999
|
// src/tools/read/activity.ts
|
|
16837
|
-
import { z as
|
|
16838
|
-
|
|
16839
|
-
// src/core/shared/toolTracking.ts
|
|
16840
|
-
function recordToolInvocation(stateDb2, event) {
|
|
16841
|
-
stateDb2.db.prepare(
|
|
16842
|
-
`INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
|
|
16843
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
16844
|
-
).run(
|
|
16845
|
-
Date.now(),
|
|
16846
|
-
event.tool_name,
|
|
16847
|
-
event.session_id ?? null,
|
|
16848
|
-
event.note_paths ? JSON.stringify(event.note_paths) : null,
|
|
16849
|
-
event.duration_ms ?? null,
|
|
16850
|
-
event.success !== false ? 1 : 0
|
|
16851
|
-
);
|
|
16852
|
-
}
|
|
16853
|
-
function rowToInvocation(row) {
|
|
16854
|
-
return {
|
|
16855
|
-
id: row.id,
|
|
16856
|
-
timestamp: row.timestamp,
|
|
16857
|
-
tool_name: row.tool_name,
|
|
16858
|
-
session_id: row.session_id,
|
|
16859
|
-
note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
|
|
16860
|
-
duration_ms: row.duration_ms,
|
|
16861
|
-
success: row.success === 1
|
|
16862
|
-
};
|
|
16863
|
-
}
|
|
16864
|
-
function getToolUsageSummary(stateDb2, daysBack = 30) {
|
|
16865
|
-
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
16866
|
-
const rows = stateDb2.db.prepare(`
|
|
16867
|
-
SELECT
|
|
16868
|
-
tool_name,
|
|
16869
|
-
COUNT(*) as invocation_count,
|
|
16870
|
-
AVG(duration_ms) as avg_duration_ms,
|
|
16871
|
-
SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) * 1.0 / COUNT(*) as success_rate,
|
|
16872
|
-
MAX(timestamp) as last_used
|
|
16873
|
-
FROM tool_invocations
|
|
16874
|
-
WHERE timestamp >= ?
|
|
16875
|
-
GROUP BY tool_name
|
|
16876
|
-
ORDER BY invocation_count DESC
|
|
16877
|
-
`).all(cutoff);
|
|
16878
|
-
return rows.map((r) => ({
|
|
16879
|
-
tool_name: r.tool_name,
|
|
16880
|
-
invocation_count: r.invocation_count,
|
|
16881
|
-
avg_duration_ms: Math.round(r.avg_duration_ms ?? 0),
|
|
16882
|
-
success_rate: Math.round(r.success_rate * 1e3) / 1e3,
|
|
16883
|
-
last_used: r.last_used
|
|
16884
|
-
}));
|
|
16885
|
-
}
|
|
16886
|
-
function getNoteAccessFrequency(stateDb2, daysBack = 30) {
|
|
16887
|
-
const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
|
|
16888
|
-
const rows = stateDb2.db.prepare(`
|
|
16889
|
-
SELECT note_paths, tool_name, timestamp
|
|
16890
|
-
FROM tool_invocations
|
|
16891
|
-
WHERE timestamp >= ? AND note_paths IS NOT NULL
|
|
16892
|
-
ORDER BY timestamp DESC
|
|
16893
|
-
`).all(cutoff);
|
|
16894
|
-
const noteMap = /* @__PURE__ */ new Map();
|
|
16895
|
-
for (const row of rows) {
|
|
16896
|
-
let paths;
|
|
16897
|
-
try {
|
|
16898
|
-
paths = JSON.parse(row.note_paths);
|
|
16899
|
-
} catch {
|
|
16900
|
-
continue;
|
|
16901
|
-
}
|
|
16902
|
-
for (const p of paths) {
|
|
16903
|
-
const existing = noteMap.get(p);
|
|
16904
|
-
if (existing) {
|
|
16905
|
-
existing.access_count++;
|
|
16906
|
-
existing.last_accessed = Math.max(existing.last_accessed, row.timestamp);
|
|
16907
|
-
existing.tools.add(row.tool_name);
|
|
16908
|
-
} else {
|
|
16909
|
-
noteMap.set(p, {
|
|
16910
|
-
access_count: 1,
|
|
16911
|
-
last_accessed: row.timestamp,
|
|
16912
|
-
tools: /* @__PURE__ */ new Set([row.tool_name])
|
|
16913
|
-
});
|
|
16914
|
-
}
|
|
16915
|
-
}
|
|
16916
|
-
}
|
|
16917
|
-
return Array.from(noteMap.entries()).map(([path33, stats]) => ({
|
|
16918
|
-
path: path33,
|
|
16919
|
-
access_count: stats.access_count,
|
|
16920
|
-
last_accessed: stats.last_accessed,
|
|
16921
|
-
tools_used: Array.from(stats.tools)
|
|
16922
|
-
})).sort((a, b) => b.access_count - a.access_count);
|
|
16923
|
-
}
|
|
16924
|
-
function getSessionHistory(stateDb2, sessionId) {
|
|
16925
|
-
if (sessionId) {
|
|
16926
|
-
const rows2 = stateDb2.db.prepare(`
|
|
16927
|
-
SELECT * FROM tool_invocations
|
|
16928
|
-
WHERE session_id = ?
|
|
16929
|
-
ORDER BY timestamp
|
|
16930
|
-
`).all(sessionId);
|
|
16931
|
-
if (rows2.length === 0) return [];
|
|
16932
|
-
const tools = /* @__PURE__ */ new Set();
|
|
16933
|
-
const notes = /* @__PURE__ */ new Set();
|
|
16934
|
-
for (const row of rows2) {
|
|
16935
|
-
tools.add(row.tool_name);
|
|
16936
|
-
if (row.note_paths) {
|
|
16937
|
-
try {
|
|
16938
|
-
for (const p of JSON.parse(row.note_paths)) {
|
|
16939
|
-
notes.add(p);
|
|
16940
|
-
}
|
|
16941
|
-
} catch {
|
|
16942
|
-
}
|
|
16943
|
-
}
|
|
16944
|
-
}
|
|
16945
|
-
return [{
|
|
16946
|
-
session_id: sessionId,
|
|
16947
|
-
started_at: rows2[0].timestamp,
|
|
16948
|
-
last_activity: rows2[rows2.length - 1].timestamp,
|
|
16949
|
-
tool_count: rows2.length,
|
|
16950
|
-
unique_tools: Array.from(tools),
|
|
16951
|
-
notes_accessed: Array.from(notes)
|
|
16952
|
-
}];
|
|
16953
|
-
}
|
|
16954
|
-
const rows = stateDb2.db.prepare(`
|
|
16955
|
-
SELECT
|
|
16956
|
-
session_id,
|
|
16957
|
-
MIN(timestamp) as started_at,
|
|
16958
|
-
MAX(timestamp) as last_activity,
|
|
16959
|
-
COUNT(*) as tool_count
|
|
16960
|
-
FROM tool_invocations
|
|
16961
|
-
WHERE session_id IS NOT NULL
|
|
16962
|
-
GROUP BY session_id
|
|
16963
|
-
ORDER BY last_activity DESC
|
|
16964
|
-
LIMIT 20
|
|
16965
|
-
`).all();
|
|
16966
|
-
return rows.map((r) => ({
|
|
16967
|
-
session_id: r.session_id,
|
|
16968
|
-
started_at: r.started_at,
|
|
16969
|
-
last_activity: r.last_activity,
|
|
16970
|
-
tool_count: r.tool_count,
|
|
16971
|
-
unique_tools: [],
|
|
16972
|
-
notes_accessed: []
|
|
16973
|
-
}));
|
|
16974
|
-
}
|
|
16975
|
-
function getRecentInvocations(stateDb2, limit = 20) {
|
|
16976
|
-
const rows = stateDb2.db.prepare(
|
|
16977
|
-
"SELECT * FROM tool_invocations ORDER BY timestamp DESC LIMIT ?"
|
|
16978
|
-
).all(limit);
|
|
16979
|
-
return rows.map(rowToInvocation);
|
|
16980
|
-
}
|
|
16981
|
-
function purgeOldInvocations(stateDb2, retentionDays = 90) {
|
|
16982
|
-
const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
|
|
16983
|
-
const result = stateDb2.db.prepare(
|
|
16984
|
-
"DELETE FROM tool_invocations WHERE timestamp < ?"
|
|
16985
|
-
).run(cutoff);
|
|
16986
|
-
return result.changes;
|
|
16987
|
-
}
|
|
16988
|
-
|
|
16989
|
-
// src/tools/read/activity.ts
|
|
18000
|
+
import { z as z29 } from "zod";
|
|
16990
18001
|
function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
16991
18002
|
server2.registerTool(
|
|
16992
18003
|
"vault_activity",
|
|
@@ -16994,10 +18005,10 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
16994
18005
|
title: "Vault Activity",
|
|
16995
18006
|
description: 'Track tool usage patterns and session activity. Modes:\n- "session": Current session summary (tools called, notes accessed)\n- "sessions": List of recent sessions\n- "note_access": Notes ranked by query frequency\n- "tool_usage": Tool usage patterns (most-used tools, avg duration)',
|
|
16996
18007
|
inputSchema: {
|
|
16997
|
-
mode:
|
|
16998
|
-
session_id:
|
|
16999
|
-
days_back:
|
|
17000
|
-
limit:
|
|
18008
|
+
mode: z29.enum(["session", "sessions", "note_access", "tool_usage"]).describe("Activity query mode"),
|
|
18009
|
+
session_id: z29.string().optional().describe("Specific session ID (for session mode, defaults to current)"),
|
|
18010
|
+
days_back: z29.number().optional().describe("Number of days to look back (default: 30)"),
|
|
18011
|
+
limit: z29.number().optional().describe("Maximum results to return (default: 20)")
|
|
17001
18012
|
}
|
|
17002
18013
|
},
|
|
17003
18014
|
async ({ mode, session_id, days_back, limit: resultLimit }) => {
|
|
@@ -17064,7 +18075,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
|
|
|
17064
18075
|
}
|
|
17065
18076
|
|
|
17066
18077
|
// src/tools/read/similarity.ts
|
|
17067
|
-
import { z as
|
|
18078
|
+
import { z as z30 } from "zod";
|
|
17068
18079
|
|
|
17069
18080
|
// src/core/read/similarity.ts
|
|
17070
18081
|
import * as fs30 from "fs";
|
|
@@ -17328,9 +18339,9 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
17328
18339
|
title: "Find Similar Notes",
|
|
17329
18340
|
description: "Find notes similar to a given note using FTS5 keyword matching. When embeddings have been built (via init_semantic), automatically uses hybrid ranking (BM25 + embedding similarity via Reciprocal Rank Fusion). Use exclude_linked to filter out notes already connected via wikilinks.",
|
|
17330
18341
|
inputSchema: {
|
|
17331
|
-
path:
|
|
17332
|
-
limit:
|
|
17333
|
-
exclude_linked:
|
|
18342
|
+
path: z30.string().describe('Path to the source note (relative to vault root, e.g. "projects/alpha.md")'),
|
|
18343
|
+
limit: z30.number().optional().describe("Maximum number of similar notes to return (default: 10)"),
|
|
18344
|
+
exclude_linked: z30.boolean().optional().describe("Exclude notes already linked to/from the source note (default: true)")
|
|
17334
18345
|
}
|
|
17335
18346
|
},
|
|
17336
18347
|
async ({ path: path33, limit, exclude_linked }) => {
|
|
@@ -17374,7 +18385,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
|
|
|
17374
18385
|
}
|
|
17375
18386
|
|
|
17376
18387
|
// src/tools/read/semantic.ts
|
|
17377
|
-
import { z as
|
|
18388
|
+
import { z as z31 } from "zod";
|
|
17378
18389
|
import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
|
|
17379
18390
|
function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
17380
18391
|
server2.registerTool(
|
|
@@ -17383,7 +18394,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
17383
18394
|
title: "Initialize Semantic Search",
|
|
17384
18395
|
description: "Download the embedding model and build semantic search index for this vault. After running, search and find_similar automatically use hybrid ranking (BM25 + semantic). Run once per vault \u2014 subsequent calls skip already-embedded notes unless force=true.",
|
|
17385
18396
|
inputSchema: {
|
|
17386
|
-
force:
|
|
18397
|
+
force: z31.boolean().optional().describe(
|
|
17387
18398
|
"Rebuild all embeddings even if they already exist (default: false)"
|
|
17388
18399
|
)
|
|
17389
18400
|
}
|
|
@@ -17463,7 +18474,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
|
|
|
17463
18474
|
|
|
17464
18475
|
// src/tools/read/merges.ts
|
|
17465
18476
|
init_levenshtein();
|
|
17466
|
-
import { z as
|
|
18477
|
+
import { z as z32 } from "zod";
|
|
17467
18478
|
import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
|
|
17468
18479
|
function normalizeName(name) {
|
|
17469
18480
|
return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
|
|
@@ -17473,7 +18484,7 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17473
18484
|
"suggest_entity_merges",
|
|
17474
18485
|
"Find potential duplicate entities that could be merged based on name similarity",
|
|
17475
18486
|
{
|
|
17476
|
-
limit:
|
|
18487
|
+
limit: z32.number().optional().default(50).describe("Maximum number of suggestions to return")
|
|
17477
18488
|
},
|
|
17478
18489
|
async ({ limit }) => {
|
|
17479
18490
|
const stateDb2 = getStateDb();
|
|
@@ -17575,11 +18586,11 @@ function registerMergeTools2(server2, getStateDb) {
|
|
|
17575
18586
|
"dismiss_merge_suggestion",
|
|
17576
18587
|
"Permanently dismiss a merge suggestion so it never reappears",
|
|
17577
18588
|
{
|
|
17578
|
-
source_path:
|
|
17579
|
-
target_path:
|
|
17580
|
-
source_name:
|
|
17581
|
-
target_name:
|
|
17582
|
-
reason:
|
|
18589
|
+
source_path: z32.string().describe("Path of the source entity"),
|
|
18590
|
+
target_path: z32.string().describe("Path of the target entity"),
|
|
18591
|
+
source_name: z32.string().describe("Name of the source entity"),
|
|
18592
|
+
target_name: z32.string().describe("Name of the target entity"),
|
|
18593
|
+
reason: z32.string().describe("Original suggestion reason")
|
|
17583
18594
|
},
|
|
17584
18595
|
async ({ source_path, target_path, source_name, target_name, reason }) => {
|
|
17585
18596
|
const stateDb2 = getStateDb();
|
|
@@ -17740,8 +18751,10 @@ var PRESETS = {
|
|
|
17740
18751
|
"frontmatter",
|
|
17741
18752
|
"notes",
|
|
17742
18753
|
"git",
|
|
17743
|
-
"policy"
|
|
18754
|
+
"policy",
|
|
18755
|
+
"memory"
|
|
17744
18756
|
],
|
|
18757
|
+
agent: ["search", "structure", "append", "frontmatter", "notes", "memory"],
|
|
17745
18758
|
// Composable bundles
|
|
17746
18759
|
graph: ["backlinks", "orphans", "hubs", "paths"],
|
|
17747
18760
|
analysis: ["schema", "wikilinks"],
|
|
@@ -17764,7 +18777,8 @@ var ALL_CATEGORIES = [
|
|
|
17764
18777
|
"frontmatter",
|
|
17765
18778
|
"notes",
|
|
17766
18779
|
"git",
|
|
17767
|
-
"policy"
|
|
18780
|
+
"policy",
|
|
18781
|
+
"memory"
|
|
17768
18782
|
];
|
|
17769
18783
|
var DEFAULT_PRESET = "full";
|
|
17770
18784
|
function parseEnabledCategories() {
|
|
@@ -17868,7 +18882,11 @@ var TOOL_CATEGORY = {
|
|
|
17868
18882
|
suggest_entity_merges: "health",
|
|
17869
18883
|
dismiss_merge_suggestion: "health",
|
|
17870
18884
|
// notes (entity merge)
|
|
17871
|
-
merge_entities: "notes"
|
|
18885
|
+
merge_entities: "notes",
|
|
18886
|
+
// memory (agent working memory)
|
|
18887
|
+
memory: "memory",
|
|
18888
|
+
recall: "memory",
|
|
18889
|
+
brief: "memory"
|
|
17872
18890
|
};
|
|
17873
18891
|
var server = new McpServer({
|
|
17874
18892
|
name: "flywheel-memory",
|
|
@@ -18000,6 +19018,9 @@ registerActivityTools(server, () => stateDb, () => {
|
|
|
18000
19018
|
registerSimilarityTools(server, () => vaultIndex, () => vaultPath, () => stateDb);
|
|
18001
19019
|
registerSemanticTools(server, () => vaultPath, () => stateDb);
|
|
18002
19020
|
registerMergeTools2(server, () => stateDb);
|
|
19021
|
+
registerMemoryTools(server, () => stateDb);
|
|
19022
|
+
registerRecallTools(server, () => stateDb);
|
|
19023
|
+
registerBriefTools(server, () => stateDb);
|
|
18003
19024
|
registerVaultResources(server, () => vaultIndex ?? null);
|
|
18004
19025
|
serverLog("server", `Registered ${_registeredCount} tools, skipped ${_skippedCount}`);
|
|
18005
19026
|
async function main() {
|
|
@@ -18017,6 +19038,12 @@ async function main() {
|
|
|
18017
19038
|
setWriteStateDb(stateDb);
|
|
18018
19039
|
setRecencyStateDb(stateDb);
|
|
18019
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
|
+
}
|
|
18020
19047
|
const vaultInitRow = stateDb.getMetadataValue.get("vault_init_last_run_at");
|
|
18021
19048
|
if (!vaultInitRow) {
|
|
18022
19049
|
serverLog("server", "Vault not initialized \u2014 call vault_init to enrich legacy notes");
|
|
@@ -18245,6 +19272,7 @@ async function runPostIndexWork(index) {
|
|
|
18245
19272
|
}
|
|
18246
19273
|
}
|
|
18247
19274
|
loadEntityEmbeddingsToMemory();
|
|
19275
|
+
setEmbeddingsBuildState("complete");
|
|
18248
19276
|
serverLog("semantic", "Embeddings ready");
|
|
18249
19277
|
}).catch((err) => {
|
|
18250
19278
|
serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
|
|
@@ -18430,17 +19458,22 @@ async function runPostIndexWork(index) {
|
|
|
18430
19458
|
tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges, description_changes: descriptionChanges });
|
|
18431
19459
|
serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
|
|
18432
19460
|
tracker.start("hub_scores", { entity_count: entitiesAfter.length });
|
|
18433
|
-
|
|
18434
|
-
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
|
|
18438
|
-
const
|
|
18439
|
-
|
|
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
|
+
}
|
|
18440
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");
|
|
18441
19476
|
}
|
|
18442
|
-
tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
|
|
18443
|
-
serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
|
|
18444
19477
|
tracker.start("recency", { entity_count: entitiesAfter.length });
|
|
18445
19478
|
try {
|
|
18446
19479
|
const cachedRecency = loadRecencyFromStateDb();
|
|
@@ -18467,6 +19500,9 @@ async function runPostIndexWork(index) {
|
|
|
18467
19500
|
const cooccurrenceIdx = await mineCooccurrences(vaultPath, entityNames);
|
|
18468
19501
|
setCooccurrenceIndex(cooccurrenceIdx);
|
|
18469
19502
|
lastCooccurrenceRebuildAt = Date.now();
|
|
19503
|
+
if (stateDb) {
|
|
19504
|
+
saveCooccurrenceToStateDb(stateDb, cooccurrenceIdx);
|
|
19505
|
+
}
|
|
18470
19506
|
tracker.end({ rebuilt: true, associations: cooccurrenceIdx._metadata.total_associations });
|
|
18471
19507
|
serverLog("watcher", `Co-occurrence: rebuilt ${cooccurrenceIdx._metadata.total_associations} associations`);
|
|
18472
19508
|
} else {
|
|
@@ -18582,87 +19618,99 @@ async function runPostIndexWork(index) {
|
|
|
18582
19618
|
}
|
|
18583
19619
|
tracker.end({ updated: taskUpdated, removed: taskRemoved });
|
|
18584
19620
|
serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
|
|
18585
|
-
tracker.start("forward_links", { files: filteredEvents.length });
|
|
18586
|
-
const eventTypeMap = new Map(filteredEvents.map((e) => [e.path, e.type]));
|
|
18587
19621
|
const forwardLinkResults = [];
|
|
18588
19622
|
let totalResolved = 0;
|
|
18589
19623
|
let totalDead = 0;
|
|
18590
|
-
for (const event of filteredEvents) {
|
|
18591
|
-
if (event.type === "delete" || !event.path.endsWith(".md")) continue;
|
|
18592
|
-
try {
|
|
18593
|
-
const links = getForwardLinksForNote(vaultIndex, event.path);
|
|
18594
|
-
const resolved = [];
|
|
18595
|
-
const dead = [];
|
|
18596
|
-
const seen = /* @__PURE__ */ new Set();
|
|
18597
|
-
for (const link of links) {
|
|
18598
|
-
const name = link.target;
|
|
18599
|
-
if (seen.has(name.toLowerCase())) continue;
|
|
18600
|
-
seen.add(name.toLowerCase());
|
|
18601
|
-
if (link.exists) resolved.push(name);
|
|
18602
|
-
else dead.push(name);
|
|
18603
|
-
}
|
|
18604
|
-
if (resolved.length > 0 || dead.length > 0) {
|
|
18605
|
-
forwardLinkResults.push({ file: event.path, resolved, dead });
|
|
18606
|
-
}
|
|
18607
|
-
totalResolved += resolved.length;
|
|
18608
|
-
totalDead += dead.length;
|
|
18609
|
-
} catch {
|
|
18610
|
-
}
|
|
18611
|
-
}
|
|
18612
19624
|
const linkDiffs = [];
|
|
18613
19625
|
const survivedLinks = [];
|
|
18614
|
-
|
|
18615
|
-
|
|
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(`
|
|
18616
19653
|
INSERT INTO note_link_history (note_path, target) VALUES (?, ?)
|
|
18617
19654
|
ON CONFLICT(note_path, target) DO UPDATE SET edits_survived = edits_survived + 1
|
|
18618
19655
|
`);
|
|
18619
|
-
|
|
19656
|
+
const checkThreshold = stateDb.db.prepare(`
|
|
18620
19657
|
SELECT target FROM note_link_history
|
|
18621
19658
|
WHERE note_path = ? AND target = ? AND edits_survived >= 3 AND last_positive_at IS NULL
|
|
18622
19659
|
`);
|
|
18623
|
-
|
|
19660
|
+
const markPositive = stateDb.db.prepare(`
|
|
18624
19661
|
UPDATE note_link_history SET last_positive_at = datetime('now') WHERE note_path = ? AND target = ?
|
|
18625
19662
|
`);
|
|
18626
|
-
|
|
18627
|
-
|
|
18628
|
-
|
|
18629
|
-
|
|
18630
|
-
|
|
18631
|
-
|
|
18632
|
-
|
|
18633
|
-
|
|
18634
|
-
|
|
18635
|
-
|
|
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
|
+
}
|
|
18636
19680
|
updateStoredNoteLinks(stateDb, entry.file, currentSet);
|
|
18637
|
-
continue;
|
|
18638
|
-
|
|
18639
|
-
|
|
18640
|
-
|
|
18641
|
-
|
|
18642
|
-
|
|
18643
|
-
|
|
18644
|
-
|
|
18645
|
-
|
|
18646
|
-
|
|
18647
|
-
|
|
18648
|
-
|
|
18649
|
-
|
|
18650
|
-
|
|
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
|
+
}
|
|
18651
19699
|
}
|
|
18652
|
-
|
|
18653
|
-
|
|
18654
|
-
|
|
18655
|
-
|
|
18656
|
-
)
|
|
18657
|
-
|
|
18658
|
-
|
|
18659
|
-
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());
|
|
18660
19707
|
}
|
|
18661
19708
|
}
|
|
18662
19709
|
}
|
|
18663
|
-
|
|
18664
|
-
|
|
18665
|
-
|
|
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;
|
|
18666
19714
|
const previousSet = getStoredNoteLinks(stateDb, event.path);
|
|
18667
19715
|
if (previousSet.size > 0) {
|
|
18668
19716
|
linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
@@ -18670,33 +19718,26 @@ async function runPostIndexWork(index) {
|
|
|
18670
19718
|
}
|
|
18671
19719
|
}
|
|
18672
19720
|
}
|
|
18673
|
-
const
|
|
18674
|
-
for (const
|
|
18675
|
-
|
|
18676
|
-
if (
|
|
18677
|
-
|
|
18678
|
-
if (previousSet.size > 0) {
|
|
18679
|
-
linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
|
|
18680
|
-
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 });
|
|
18681
19726
|
}
|
|
18682
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");
|
|
18683
19740
|
}
|
|
18684
|
-
const newDeadLinks = [];
|
|
18685
|
-
for (const diff of linkDiffs) {
|
|
18686
|
-
const newDead = diff.added.filter((target) => !vaultIndex.entities.has(target.toLowerCase()));
|
|
18687
|
-
if (newDead.length > 0) {
|
|
18688
|
-
newDeadLinks.push({ file: diff.file, targets: newDead });
|
|
18689
|
-
}
|
|
18690
|
-
}
|
|
18691
|
-
tracker.end({
|
|
18692
|
-
total_resolved: totalResolved,
|
|
18693
|
-
total_dead: totalDead,
|
|
18694
|
-
links: forwardLinkResults,
|
|
18695
|
-
link_diffs: linkDiffs,
|
|
18696
|
-
survived: survivedLinks,
|
|
18697
|
-
new_dead_links: newDeadLinks
|
|
18698
|
-
});
|
|
18699
|
-
serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead${newDeadLinks.length > 0 ? `, ${newDeadLinks.reduce((s, d) => s + d.targets.length, 0)} new dead` : ""}`);
|
|
18700
19741
|
tracker.start("wikilink_check", { files: filteredEvents.length });
|
|
18701
19742
|
const trackedLinks = [];
|
|
18702
19743
|
if (stateDb) {
|
|
@@ -18821,6 +19862,24 @@ async function runPostIndexWork(index) {
|
|
|
18821
19862
|
if (newlySuppressed.length > 0) {
|
|
18822
19863
|
serverLog("watcher", `Suppression: ${newlySuppressed.length} entities newly suppressed: ${newlySuppressed.join(", ")}`);
|
|
18823
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
|
+
}
|
|
18824
19883
|
tracker.start("prospect_scan", { files: filteredEvents.length });
|
|
18825
19884
|
const prospectResults = [];
|
|
18826
19885
|
for (const event of filteredEvents) {
|
|
@@ -18884,45 +19943,50 @@ async function runPostIndexWork(index) {
|
|
|
18884
19943
|
serverLog("watcher", `Suggestion scoring: ${suggestionResults.length} files scored`);
|
|
18885
19944
|
}
|
|
18886
19945
|
tracker.start("tag_scan", { files: filteredEvents.length });
|
|
18887
|
-
|
|
18888
|
-
|
|
18889
|
-
|
|
18890
|
-
|
|
18891
|
-
for (const
|
|
18892
|
-
|
|
18893
|
-
|
|
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
|
+
}
|
|
18894
19955
|
}
|
|
18895
|
-
|
|
18896
|
-
|
|
18897
|
-
|
|
18898
|
-
|
|
18899
|
-
|
|
18900
|
-
|
|
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
|
+
}
|
|
18901
19969
|
updateStoredNoteTags(stateDb, event.path, currentSet);
|
|
18902
|
-
continue;
|
|
18903
|
-
}
|
|
18904
|
-
const added = [...currentSet].filter((t) => !previousSet.has(t));
|
|
18905
|
-
const removed = [...previousSet].filter((t) => !currentSet.has(t));
|
|
18906
|
-
if (added.length > 0 || removed.length > 0) {
|
|
18907
|
-
tagDiffs.push({ file: event.path, added, removed });
|
|
18908
19970
|
}
|
|
18909
|
-
|
|
18910
|
-
|
|
18911
|
-
|
|
18912
|
-
|
|
18913
|
-
|
|
18914
|
-
|
|
18915
|
-
|
|
18916
|
-
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
|
+
}
|
|
18917
19978
|
}
|
|
18918
19979
|
}
|
|
18919
19980
|
}
|
|
18920
|
-
|
|
18921
|
-
|
|
18922
|
-
|
|
18923
|
-
|
|
18924
|
-
|
|
18925
|
-
|
|
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");
|
|
18926
19990
|
}
|
|
18927
19991
|
const duration = Date.now() - batchStart;
|
|
18928
19992
|
if (stateDb) {
|