hmem-mcp 3.3.0 → 3.5.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-config.js +1 -1
- package/dist/hmem-store.d.ts +3 -1
- package/dist/hmem-store.js +82 -3
- package/dist/hmem-store.js.map +1 -1
- package/dist/mcp-server.js +19 -3
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
- package/skills/hmem-write/SKILL.md +32 -2
package/dist/hmem-config.js
CHANGED
|
@@ -32,7 +32,7 @@ export const DEFAULT_PREFIX_DESCRIPTIONS = {
|
|
|
32
32
|
O: "(O)riginal context — raw session history with progressive summarization (auto-generated by flush_context)",
|
|
33
33
|
};
|
|
34
34
|
export const DEFAULT_CONFIG = {
|
|
35
|
-
maxCharsPerLevel: [
|
|
35
|
+
maxCharsPerLevel: [200, 2_500, 10_000, 25_000, 50_000],
|
|
36
36
|
maxDepth: 5,
|
|
37
37
|
defaultReadLimit: 100,
|
|
38
38
|
prefixes: { ...DEFAULT_PREFIXES },
|
package/dist/hmem-store.d.ts
CHANGED
|
@@ -196,7 +196,7 @@ export declare class HmemStore {
|
|
|
196
196
|
* Each indented line → its own memory_nodes row with compound ID
|
|
197
197
|
* Multiple lines at the same indent depth → siblings (new capability)
|
|
198
198
|
*/
|
|
199
|
-
write(prefix: string, content: string, links?: string[], minRole?: AgentRole, favorite?: boolean, tags?: string[], pinned?: boolean): WriteResult;
|
|
199
|
+
write(prefix: string, content: string, links?: string[], minRole?: AgentRole, favorite?: boolean, tags?: string[], pinned?: boolean, force?: boolean): WriteResult;
|
|
200
200
|
/**
|
|
201
201
|
* Write a linear entry with explicit content at each level (no tree branching).
|
|
202
202
|
* Used by flush_context for O-prefix entries. Each level is a single node forming
|
|
@@ -379,6 +379,8 @@ export declare class HmemStore {
|
|
|
379
379
|
private parseTimeWindow;
|
|
380
380
|
private buildRoleFilter;
|
|
381
381
|
private nextSeq;
|
|
382
|
+
/** Clear all active markers — called at MCP server start so each session starts neutral. */
|
|
383
|
+
clearAllActive(): void;
|
|
382
384
|
/** Auto-resolve linked entries on an entry (extracted for reuse in chain resolution). */
|
|
383
385
|
private resolveEntryLinks;
|
|
384
386
|
/** Get child nodes created after a given ISO timestamp (for "new since last session" detection). */
|
package/dist/hmem-store.js
CHANGED
|
@@ -224,7 +224,7 @@ export class HmemStore {
|
|
|
224
224
|
* Each indented line → its own memory_nodes row with compound ID
|
|
225
225
|
* Multiple lines at the same indent depth → siblings (new capability)
|
|
226
226
|
*/
|
|
227
|
-
write(prefix, content, links, minRole = "worker", favorite, tags, pinned) {
|
|
227
|
+
write(prefix, content, links, minRole = "worker", favorite, tags, pinned, force) {
|
|
228
228
|
this.guardCorrupted();
|
|
229
229
|
prefix = prefix.toUpperCase();
|
|
230
230
|
if (!this.cfg.prefixes[prefix]) {
|
|
@@ -265,6 +265,77 @@ export class HmemStore {
|
|
|
265
265
|
throw new Error("Tags are required. Provide at least 1 tag (3+ recommended) for discoverability. Example: tags=['#hmem', '#sqlite', '#bug']");
|
|
266
266
|
}
|
|
267
267
|
const validatedTags = this.validateTags(tags);
|
|
268
|
+
// Duplicate detection: check for existing entries with significant tag overlap
|
|
269
|
+
if (prefix !== "O" && !force) { // O-entries are auto-generated, skip check
|
|
270
|
+
const tagPlaceholders = validatedTags.map(() => "?").join(", ");
|
|
271
|
+
const overlapRows = this.db.prepare(`
|
|
272
|
+
SELECT
|
|
273
|
+
CASE WHEN mt.entry_id LIKE '%.%'
|
|
274
|
+
THEN SUBSTR(mt.entry_id, 1, INSTR(mt.entry_id, '.') - 1)
|
|
275
|
+
ELSE mt.entry_id END as root_id,
|
|
276
|
+
COUNT(DISTINCT mt.tag) as shared
|
|
277
|
+
FROM memory_tags mt
|
|
278
|
+
JOIN memories m ON m.id = (
|
|
279
|
+
CASE WHEN mt.entry_id LIKE '%.%'
|
|
280
|
+
THEN SUBSTR(mt.entry_id, 1, INSTR(mt.entry_id, '.') - 1)
|
|
281
|
+
ELSE mt.entry_id END
|
|
282
|
+
)
|
|
283
|
+
WHERE mt.tag IN (${tagPlaceholders})
|
|
284
|
+
AND m.prefix = ?
|
|
285
|
+
AND m.obsolete != 1
|
|
286
|
+
AND m.irrelevant != 1
|
|
287
|
+
GROUP BY root_id
|
|
288
|
+
HAVING shared >= 2
|
|
289
|
+
ORDER BY shared DESC
|
|
290
|
+
LIMIT 3
|
|
291
|
+
`).all(...validatedTags, prefix);
|
|
292
|
+
// Phase 2: FTS5 title similarity (fallback for entries with different tags)
|
|
293
|
+
let ftsMatches = [];
|
|
294
|
+
if (overlapRows.length === 0) {
|
|
295
|
+
const words = level1.replace(/[^a-zA-Z0-9äöüÄÖÜß\s-]/g, " ")
|
|
296
|
+
.split(/\s+/)
|
|
297
|
+
.filter(w => w.length > 3)
|
|
298
|
+
.slice(0, 4);
|
|
299
|
+
if (words.length >= 2) {
|
|
300
|
+
try {
|
|
301
|
+
const andQuery = words.join(" AND ");
|
|
302
|
+
const ftsHits = this.db.prepare(`
|
|
303
|
+
SELECT rm.root_id FROM hmem_fts_rowid_map rm
|
|
304
|
+
JOIN hmem_fts f ON f.rowid = rm.fts_rowid
|
|
305
|
+
WHERE hmem_fts MATCH ? LIMIT 5
|
|
306
|
+
`).all(andQuery);
|
|
307
|
+
const seen = new Set();
|
|
308
|
+
for (const hit of ftsHits) {
|
|
309
|
+
if (seen.has(hit.root_id))
|
|
310
|
+
continue;
|
|
311
|
+
seen.add(hit.root_id);
|
|
312
|
+
const row = this.db.prepare("SELECT id, title, prefix FROM memories WHERE id = ? AND prefix = ? AND obsolete != 1 AND irrelevant != 1").get(hit.root_id, prefix);
|
|
313
|
+
if (row)
|
|
314
|
+
ftsMatches.push({ root_id: row.id, title: row.title });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch { /* FTS5 might not exist */ }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (overlapRows.length > 0 || ftsMatches.length > 0) {
|
|
321
|
+
const parts = [];
|
|
322
|
+
if (overlapRows.length > 0) {
|
|
323
|
+
const tagHits = overlapRows.map(r => {
|
|
324
|
+
const row = this.db.prepare("SELECT title FROM memories WHERE id = ?").get(r.root_id);
|
|
325
|
+
return ` ${r.root_id} (${r.shared}/${validatedTags.length} shared tags) ${row?.title ?? ""}`;
|
|
326
|
+
}).join("\n");
|
|
327
|
+
parts.push(`Tag overlap:\n${tagHits}`);
|
|
328
|
+
}
|
|
329
|
+
if (ftsMatches.length > 0) {
|
|
330
|
+
const ftsHits = ftsMatches.map(r => ` ${r.root_id} ${r.title}`).join("\n");
|
|
331
|
+
parts.push(`Similar titles:\n${ftsHits}`);
|
|
332
|
+
}
|
|
333
|
+
const bestMatch = overlapRows[0]?.root_id ?? ftsMatches[0]?.root_id;
|
|
334
|
+
throw new Error(`Similar ${prefix}-entries already exist:\n${parts.join("\n")}\n\n` +
|
|
335
|
+
`If this belongs to an existing entry, use: append_memory(id="${bestMatch}", content="...")\n` +
|
|
336
|
+
`If this is intentionally a NEW entry, retry with: force=true`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
268
339
|
// Run in a transaction
|
|
269
340
|
this.db.transaction(() => {
|
|
270
341
|
insertRoot.run(rootId, prefix, seq, timestamp, timestamp, title, level1, links ? JSON.stringify(links) : null, minRole, favorite ? 1 : 0, pinned ? 1 : 0);
|
|
@@ -1995,6 +2066,10 @@ export class HmemStore {
|
|
|
1995
2066
|
const row = this.db.prepare("SELECT MAX(seq) as maxSeq FROM memories WHERE prefix = ?").get(prefix);
|
|
1996
2067
|
return (row?.maxSeq || 0) + 1;
|
|
1997
2068
|
}
|
|
2069
|
+
/** Clear all active markers — called at MCP server start so each session starts neutral. */
|
|
2070
|
+
clearAllActive() {
|
|
2071
|
+
this.db.prepare("UPDATE memories SET active = 0 WHERE active = 1").run();
|
|
2072
|
+
}
|
|
1998
2073
|
/** Auto-resolve linked entries on an entry (extracted for reuse in chain resolution). */
|
|
1999
2074
|
resolveEntryLinks(entry, opts) {
|
|
2000
2075
|
const linkDepth = opts.resolveLinks === false ? 0 : (opts.linkDepth ?? 1);
|
|
@@ -2044,9 +2119,13 @@ export class HmemStore {
|
|
|
2044
2119
|
const row = this.db.prepare("SELECT id FROM memories WHERE prefix = 'O' AND active = 1 AND obsolete != 1 AND irrelevant != 1 LIMIT 1").get();
|
|
2045
2120
|
if (row)
|
|
2046
2121
|
return row.id;
|
|
2047
|
-
//
|
|
2122
|
+
// Find active project for context
|
|
2123
|
+
const activeProject = this.db.prepare("SELECT id, title FROM memories WHERE prefix = 'P' AND active = 1 AND obsolete != 1 LIMIT 1").get();
|
|
2048
2124
|
const today = new Date().toISOString().substring(0, 10);
|
|
2049
|
-
const
|
|
2125
|
+
const projectName = activeProject?.title?.split("|")[0]?.trim() ?? "unassigned";
|
|
2126
|
+
const tags = ["#session"];
|
|
2127
|
+
const links = activeProject ? [activeProject.id] : undefined;
|
|
2128
|
+
const result = this.writeLinear("O", { l1: `${projectName} — Session ${today}` }, tags, links);
|
|
2050
2129
|
this.db.prepare("UPDATE memories SET active = 1, updated_at = ? WHERE id = ?")
|
|
2051
2130
|
.run(new Date().toISOString(), result.id);
|
|
2052
2131
|
return result.id;
|