hmem-mcp 2.5.3 → 2.6.0
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/hmem-store.d.ts +4 -2
- package/dist/hmem-store.js +117 -9
- package/dist/hmem-store.js.map +1 -1
- package/dist/mcp-server.js +108 -18
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
package/dist/hmem-store.d.ts
CHANGED
|
@@ -48,6 +48,8 @@ export interface MemoryEntry {
|
|
|
48
48
|
favorite?: boolean;
|
|
49
49
|
/** True if the agent marked this entry as irrelevant. Hidden from bulk reads, no correction needed. */
|
|
50
50
|
irrelevant?: boolean;
|
|
51
|
+
/** True if this entry is actively relevant (root-only). When any entry in a prefix has active=1, only active entries of that prefix are expanded in bulk reads. */
|
|
52
|
+
active?: boolean;
|
|
51
53
|
/** True if this entry was already delivered in a previous bulk read (session cache). */
|
|
52
54
|
suppressed?: boolean;
|
|
53
55
|
/**
|
|
@@ -245,7 +247,7 @@ export declare class HmemStore {
|
|
|
245
247
|
/**
|
|
246
248
|
* Update specific fields of an existing root entry (curator use only).
|
|
247
249
|
*/
|
|
248
|
-
update(id: string, fields: Partial<Pick<MemoryEntry, "level_1" | "level_2" | "level_3" | "level_4" | "level_5" | "links" | "min_role" | "obsolete" | "favorite" | "irrelevant">>): boolean;
|
|
250
|
+
update(id: string, fields: Partial<Pick<MemoryEntry, "level_1" | "level_2" | "level_3" | "level_4" | "level_5" | "links" | "min_role" | "obsolete" | "favorite" | "irrelevant" | "active">>): boolean;
|
|
249
251
|
/**
|
|
250
252
|
* Delete an entry by ID (curator use only).
|
|
251
253
|
* Also deletes all associated memory_nodes.
|
|
@@ -257,7 +259,7 @@ export declare class HmemStore {
|
|
|
257
259
|
* For sub-nodes: updates node content only.
|
|
258
260
|
* Does NOT modify children — use appendChildren to extend the tree.
|
|
259
261
|
*/
|
|
260
|
-
updateNode(id: string, newContent: string, links?: string[], obsolete?: boolean, favorite?: boolean, curatorBypass?: boolean, irrelevant?: boolean, tags?: string[], pinned?: boolean): boolean;
|
|
262
|
+
updateNode(id: string, newContent: string, links?: string[], obsolete?: boolean, favorite?: boolean, curatorBypass?: boolean, irrelevant?: boolean, tags?: string[], pinned?: boolean, active?: boolean): boolean;
|
|
261
263
|
/**
|
|
262
264
|
* Append new child nodes under an existing entry (root or node).
|
|
263
265
|
* Content is tab-indented relative to the parent:
|
package/dist/hmem-store.js
CHANGED
|
@@ -157,6 +157,8 @@ const MIGRATIONS = [
|
|
|
157
157
|
// Sync support: track last content modification (separate from last_accessed)
|
|
158
158
|
"ALTER TABLE memories ADD COLUMN updated_at TEXT",
|
|
159
159
|
"ALTER TABLE memory_nodes ADD COLUMN updated_at TEXT",
|
|
160
|
+
// Active flag: marks entries as currently relevant — non-active entries in same prefix shown title-only
|
|
161
|
+
"ALTER TABLE memories ADD COLUMN active INTEGER DEFAULT 0",
|
|
160
162
|
];
|
|
161
163
|
// ---- HmemStore class ----
|
|
162
164
|
export class HmemStore {
|
|
@@ -258,8 +260,11 @@ export class HmemStore {
|
|
|
258
260
|
INSERT INTO memory_nodes (id, parent_id, root_id, depth, seq, title, content, created_at, updated_at)
|
|
259
261
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
260
262
|
`);
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
+
// Tags are mandatory — at least 1 required for discoverability
|
|
264
|
+
if (!tags || tags.length === 0) {
|
|
265
|
+
throw new Error("Tags are required. Provide at least 1 tag (3+ recommended) for discoverability. Example: tags=['#hmem', '#sqlite', '#bug']");
|
|
266
|
+
}
|
|
267
|
+
const validatedTags = this.validateTags(tags);
|
|
263
268
|
// Run in a transaction
|
|
264
269
|
this.db.transaction(() => {
|
|
265
270
|
insertRoot.run(rootId, prefix, seq, timestamp, timestamp, title, level1, links ? JSON.stringify(links) : null, minRole, favorite ? 1 : 0, pinned ? 1 : 0);
|
|
@@ -558,6 +563,75 @@ export class HmemStore {
|
|
|
558
563
|
// Step 0: Filter out irrelevant entries (never shown in bulk reads)
|
|
559
564
|
const irrelevantCount = rows.filter(r => r.irrelevant === 1).length;
|
|
560
565
|
const activeRows = rows.filter(r => r.irrelevant !== 1);
|
|
566
|
+
// Step 0.5: Detect active-prefixes — prefixes where at least one entry has active=1.
|
|
567
|
+
// Non-active entries in these prefixes are still shown (as compact titles) but don't get expansion slots.
|
|
568
|
+
const activePrefixes = new Set();
|
|
569
|
+
for (const r of activeRows) {
|
|
570
|
+
if (r.active === 1)
|
|
571
|
+
activePrefixes.add(r.prefix);
|
|
572
|
+
}
|
|
573
|
+
// Step 0.6: Cascading related-suppression — entries in OTHER prefixes that are thematically
|
|
574
|
+
// related ONLY to suppressed (non-active) entries get demoted to title-only too.
|
|
575
|
+
// Logic: collect tags from non-active entries, subtract tags from active entries,
|
|
576
|
+
// then find entries in other prefixes that share ONLY the remaining "suppressed" tags.
|
|
577
|
+
const relatedSuppressed = new Set();
|
|
578
|
+
if (activePrefixes.size > 0) {
|
|
579
|
+
const activeEntryIds = [];
|
|
580
|
+
const nonActiveEntryIds = [];
|
|
581
|
+
for (const r of activeRows) {
|
|
582
|
+
if (activePrefixes.has(r.prefix)) {
|
|
583
|
+
if (r.active === 1)
|
|
584
|
+
activeEntryIds.push(r.id);
|
|
585
|
+
else
|
|
586
|
+
nonActiveEntryIds.push(r.id);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (nonActiveEntryIds.length > 0 && activeEntryIds.length > 0) {
|
|
590
|
+
// Fetch tags for active and non-active entries (root-level tags + child tags)
|
|
591
|
+
const activeTagRows = activeEntryIds.length > 0
|
|
592
|
+
? this.db.prepare(`SELECT DISTINCT tag FROM memory_tags WHERE ${activeEntryIds.map(() => "entry_id = ? OR entry_id LIKE ?").join(" OR ")}`).all(...activeEntryIds.flatMap(id => [id, `${id}.%`]))
|
|
593
|
+
: [];
|
|
594
|
+
const activeTags = new Set(activeTagRows.map(r => r.tag));
|
|
595
|
+
const nonActiveTagRows = this.db.prepare(`SELECT DISTINCT tag FROM memory_tags WHERE ${nonActiveEntryIds.map(() => "entry_id = ? OR entry_id LIKE ?").join(" OR ")}`).all(...nonActiveEntryIds.flatMap(id => [id, `${id}.%`]));
|
|
596
|
+
// Suppressed tags = tags that appear in non-active entries but NOT in active entries
|
|
597
|
+
const suppressedTags = nonActiveTagRows.map(r => r.tag).filter(t => !activeTags.has(t));
|
|
598
|
+
if (suppressedTags.length > 0) {
|
|
599
|
+
// Find root entries in OTHER prefixes that have ONLY suppressed tags (no active tags)
|
|
600
|
+
const prefixList = [...activePrefixes];
|
|
601
|
+
const otherEntries = activeRows.filter(r => !activePrefixes.has(r.prefix) && r.obsolete !== 1);
|
|
602
|
+
if (otherEntries.length > 0) {
|
|
603
|
+
const otherIds = otherEntries.map(r => r.id);
|
|
604
|
+
const otherTagMap = this.fetchTagsBulk(otherIds);
|
|
605
|
+
// Also fetch child tags for better coverage
|
|
606
|
+
const otherChildIds = otherIds.flatMap(id => {
|
|
607
|
+
const children = this.db.prepare("SELECT id FROM memory_nodes WHERE root_id = ?").all(id);
|
|
608
|
+
return children.map(c => c.id);
|
|
609
|
+
});
|
|
610
|
+
const childTagMap = otherChildIds.length > 0 ? this.fetchTagsBulk(otherChildIds) : new Map();
|
|
611
|
+
// Merge child tags into parent
|
|
612
|
+
for (const e of otherEntries) {
|
|
613
|
+
const rootTags = otherTagMap.get(e.id) ?? [];
|
|
614
|
+
const childNodes = this.db.prepare("SELECT id FROM memory_nodes WHERE root_id = ?").all(e.id);
|
|
615
|
+
const allTags = new Set(rootTags);
|
|
616
|
+
for (const cn of childNodes) {
|
|
617
|
+
const ct = childTagMap.get(cn.id);
|
|
618
|
+
if (ct)
|
|
619
|
+
ct.forEach(t => allTags.add(t));
|
|
620
|
+
}
|
|
621
|
+
if (allTags.size === 0)
|
|
622
|
+
continue;
|
|
623
|
+
// Check: does this entry have ANY active tags?
|
|
624
|
+
const hasActiveTags = [...allTags].some(t => activeTags.has(t));
|
|
625
|
+
const hasSuppressedTags = [...allTags].some(t => suppressedTags.includes(t));
|
|
626
|
+
// Only suppress if entry shares suppressed tags but NO active tags
|
|
627
|
+
if (hasSuppressedTags && !hasActiveTags) {
|
|
628
|
+
relatedSuppressed.add(e.id);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
561
635
|
// Step 1: Separate obsolete from non-obsolete FIRST
|
|
562
636
|
const obsoleteRows = activeRows.filter(r => r.obsolete === 1);
|
|
563
637
|
const nonObsoleteRows = activeRows.filter(r => r.obsolete !== 1);
|
|
@@ -614,10 +688,15 @@ export class HmemStore {
|
|
|
614
688
|
const expandedIds = new Set();
|
|
615
689
|
const isEssentials = opts.mode === "essentials";
|
|
616
690
|
// Per prefix: top N newest + top M most-accessed — slot counts scale with prefix size
|
|
617
|
-
for (const [, prefixRows] of byPrefix) {
|
|
618
|
-
|
|
691
|
+
for (const [prefix, prefixRows] of byPrefix) {
|
|
692
|
+
// In active-prefixes, only active entries compete for expansion slots.
|
|
693
|
+
// Related-suppressed entries in OTHER prefixes also don't compete.
|
|
694
|
+
const candidateRows = activePrefixes.has(prefix)
|
|
695
|
+
? prefixRows.filter(r => r.active === 1)
|
|
696
|
+
: prefixRows.filter(r => !relatedSuppressed.has(r.id));
|
|
697
|
+
const { newestCount, accessCount } = this.calcV2Slots(candidateRows.length, isEssentials, fraction);
|
|
619
698
|
// Newest: skip cached AND hidden entries, fill from fresh entries only
|
|
620
|
-
const uncachedRows =
|
|
699
|
+
const uncachedRows = candidateRows.filter(r => !cached.has(r.id) && !hidden.has(r.id));
|
|
621
700
|
for (const r of uncachedRows.slice(0, newestCount)) {
|
|
622
701
|
expandedIds.add(r.id);
|
|
623
702
|
}
|
|
@@ -630,9 +709,18 @@ export class HmemStore {
|
|
|
630
709
|
for (const r of mostAccessed)
|
|
631
710
|
expandedIds.add(r.id);
|
|
632
711
|
}
|
|
633
|
-
// Global:
|
|
712
|
+
// Global: uncached+unhidden favorites/pinned + all active entries
|
|
634
713
|
for (const r of nonObsoleteRows) {
|
|
635
714
|
if ((r.favorite === 1 || r.pinned === 1) && !cached.has(r.id) && !hidden.has(r.id)) {
|
|
715
|
+
// In active-prefixes, only active entries get expansion even if favorite/pinned
|
|
716
|
+
if (!activePrefixes.has(r.prefix) || r.active === 1) {
|
|
717
|
+
// Related-suppressed entries don't get expansion even if favorite/pinned
|
|
718
|
+
if (!relatedSuppressed.has(r.id)) {
|
|
719
|
+
expandedIds.add(r.id);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
if (r.active === 1) {
|
|
636
724
|
expandedIds.add(r.id);
|
|
637
725
|
}
|
|
638
726
|
}
|
|
@@ -643,6 +731,13 @@ export class HmemStore {
|
|
|
643
731
|
const nodeCounts = this.db.prepare("SELECT root_id, COUNT(*) as cnt FROM memory_nodes GROUP BY root_id ORDER BY cnt DESC LIMIT ?").all(topSubnodeCount);
|
|
644
732
|
for (const row of nodeCounts) {
|
|
645
733
|
if (!hidden.has(row.root_id)) {
|
|
734
|
+
// In active-prefixes, don't expand non-active entries even if they have many sub-nodes
|
|
735
|
+
const entryRow = nonObsoleteRows.find(r => r.id === row.root_id);
|
|
736
|
+
if (entryRow && activePrefixes.has(entryRow.prefix) && entryRow.active !== 1)
|
|
737
|
+
continue;
|
|
738
|
+
// Related-suppressed entries don't get topSubnode expansion either
|
|
739
|
+
if (relatedSuppressed.has(row.root_id))
|
|
740
|
+
continue;
|
|
646
741
|
expandedIds.add(row.root_id);
|
|
647
742
|
topSubnodeIds.add(row.root_id);
|
|
648
743
|
}
|
|
@@ -659,9 +754,17 @@ export class HmemStore {
|
|
|
659
754
|
// Step 4: Build visible rows (hidden entries completely excluded)
|
|
660
755
|
// - Expanded entries: full content with children
|
|
661
756
|
// - Cached entries: title-only (no expansion, no children)
|
|
757
|
+
// - Non-active in active-prefixes: title-only
|
|
758
|
+
// - Related-suppressed in other prefixes: title-only
|
|
662
759
|
const expandedNonObsolete = nonObsoleteRows.filter(r => expandedIds.has(r.id));
|
|
663
760
|
const cachedVisible = nonObsoleteRows.filter(r => cached.has(r.id) && !expandedIds.has(r.id) && !hidden.has(r.id));
|
|
664
|
-
const
|
|
761
|
+
const nonActiveVisible = activePrefixes.size > 0
|
|
762
|
+
? nonObsoleteRows.filter(r => activePrefixes.has(r.prefix) && r.active !== 1 && !expandedIds.has(r.id) && !cached.has(r.id) && !hidden.has(r.id))
|
|
763
|
+
: [];
|
|
764
|
+
const relatedSuppressedVisible = relatedSuppressed.size > 0
|
|
765
|
+
? nonObsoleteRows.filter(r => relatedSuppressed.has(r.id) && !expandedIds.has(r.id) && !cached.has(r.id) && !hidden.has(r.id))
|
|
766
|
+
: [];
|
|
767
|
+
const visibleRows = [...expandedNonObsolete, ...cachedVisible, ...nonActiveVisible, ...relatedSuppressedVisible, ...visibleObsolete];
|
|
665
768
|
const visibleIds = new Set(visibleRows.map(r => r.id));
|
|
666
769
|
// titles_only: V2 selection applies, but skip link resolution
|
|
667
770
|
if (opts.titlesOnly) {
|
|
@@ -1187,7 +1290,7 @@ export class HmemStore {
|
|
|
1187
1290
|
if (key === "links" && Array.isArray(val)) {
|
|
1188
1291
|
params.push(JSON.stringify(val));
|
|
1189
1292
|
}
|
|
1190
|
-
else if (key === "obsolete" || key === "favorite" || key === "irrelevant") {
|
|
1293
|
+
else if (key === "obsolete" || key === "favorite" || key === "irrelevant" || key === "active") {
|
|
1191
1294
|
params.push(val ? 1 : 0);
|
|
1192
1295
|
}
|
|
1193
1296
|
else {
|
|
@@ -1221,7 +1324,7 @@ export class HmemStore {
|
|
|
1221
1324
|
* For sub-nodes: updates node content only.
|
|
1222
1325
|
* Does NOT modify children — use appendChildren to extend the tree.
|
|
1223
1326
|
*/
|
|
1224
|
-
updateNode(id, newContent, links, obsolete, favorite, curatorBypass, irrelevant, tags, pinned) {
|
|
1327
|
+
updateNode(id, newContent, links, obsolete, favorite, curatorBypass, irrelevant, tags, pinned, active) {
|
|
1225
1328
|
this.guardCorrupted();
|
|
1226
1329
|
const trimmed = newContent.trim();
|
|
1227
1330
|
if (id.includes(".")) {
|
|
@@ -1338,6 +1441,10 @@ export class HmemStore {
|
|
|
1338
1441
|
sets.push("pinned = ?");
|
|
1339
1442
|
params.push(pinned ? 1 : 0);
|
|
1340
1443
|
}
|
|
1444
|
+
if (active !== undefined) {
|
|
1445
|
+
sets.push("active = ?");
|
|
1446
|
+
params.push(active ? 1 : 0);
|
|
1447
|
+
}
|
|
1341
1448
|
if (sets.length === 0) {
|
|
1342
1449
|
// Only tags to update — no SQL UPDATE needed
|
|
1343
1450
|
if (tags !== undefined) {
|
|
@@ -1987,6 +2094,7 @@ export class HmemStore {
|
|
|
1987
2094
|
obsolete: row.obsolete === 1,
|
|
1988
2095
|
favorite: row.favorite === 1,
|
|
1989
2096
|
irrelevant: row.irrelevant === 1,
|
|
2097
|
+
active: row.active === 1,
|
|
1990
2098
|
pinned: row.pinned === 1,
|
|
1991
2099
|
children,
|
|
1992
2100
|
};
|