hmem-mcp 6.1.0 → 6.1.2
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 +22 -0
- package/dist/hmem-store.js +94 -1
- package/dist/hmem-store.js.map +1 -1
- package/dist/mcp-server.js +101 -24
- package/dist/mcp-server.js.map +1 -1
- package/package.json +1 -1
package/dist/hmem-store.d.ts
CHANGED
|
@@ -435,6 +435,14 @@ export declare class HmemStore {
|
|
|
435
435
|
peekAppendTopLevelIds(parentId: string, content: string): string[];
|
|
436
436
|
/** Clear all active markers — called at MCP server start so each session starts neutral. */
|
|
437
437
|
clearAllActive(): void;
|
|
438
|
+
/**
|
|
439
|
+
* Atomically set ONE project as the active P-entry in this agent's DB.
|
|
440
|
+
* Deactivates all other P-entries in the same .hmem file. Multi-agent isolation
|
|
441
|
+
* happens at the .hmem-file level (each agent has its own DB), so within a single
|
|
442
|
+
* file there must only ever be one active project — otherwise getActiveProject()
|
|
443
|
+
* (LIMIT 1) becomes nondeterministic and log-exchange routes to the wrong O-entry.
|
|
444
|
+
*/
|
|
445
|
+
setActiveProject(id: string): void;
|
|
438
446
|
/** Auto-resolve linked entries on an entry (extracted for reuse in chain resolution). */
|
|
439
447
|
private resolveEntryLinks;
|
|
440
448
|
/** Get child nodes created after a given ISO timestamp (for "new since last session" detection). */
|
|
@@ -579,6 +587,20 @@ export declare class HmemStore {
|
|
|
579
587
|
* Rewrites all IDs, parent_ids, root_ids, tags, and FTS rowid map entries.
|
|
580
588
|
*/
|
|
581
589
|
private _moveSubtree;
|
|
590
|
+
/**
|
|
591
|
+
* Rename an entire L2 session subtree (L2 node + all L3/L4/L5 descendants)
|
|
592
|
+
* to a new id prefix. Updates memory_nodes (id, parent_id, seq), memory_tags,
|
|
593
|
+
* and hmem_fts_rowid_map. The root-level parent of the L2 node stays at oId.
|
|
594
|
+
* Caller must ensure newId does not yet exist.
|
|
595
|
+
*/
|
|
596
|
+
private _renameL2Subtree;
|
|
597
|
+
/**
|
|
598
|
+
* Reorder L2 sessions under an O-entry so their seq matches chronological
|
|
599
|
+
* order by created_at (ascending). Uses 2-phase rename via _TMP staging IDs
|
|
600
|
+
* to avoid collisions during renumbering. Returns the number of sessions
|
|
601
|
+
* actually renamed.
|
|
602
|
+
*/
|
|
603
|
+
reorderSessionsByDate(oId: string): number;
|
|
582
604
|
/**
|
|
583
605
|
* Remove empty L2 (sessions) and L3 (batches) nodes in an O-entry.
|
|
584
606
|
*/
|
package/dist/hmem-store.js
CHANGED
|
@@ -2370,6 +2370,21 @@ export class HmemStore {
|
|
|
2370
2370
|
clearAllActive() {
|
|
2371
2371
|
this.db.prepare("UPDATE memories SET active = 0 WHERE active = 1").run();
|
|
2372
2372
|
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Atomically set ONE project as the active P-entry in this agent's DB.
|
|
2375
|
+
* Deactivates all other P-entries in the same .hmem file. Multi-agent isolation
|
|
2376
|
+
* happens at the .hmem-file level (each agent has its own DB), so within a single
|
|
2377
|
+
* file there must only ever be one active project — otherwise getActiveProject()
|
|
2378
|
+
* (LIMIT 1) becomes nondeterministic and log-exchange routes to the wrong O-entry.
|
|
2379
|
+
*/
|
|
2380
|
+
setActiveProject(id) {
|
|
2381
|
+
const now = new Date().toISOString();
|
|
2382
|
+
const tx = this.db.transaction(() => {
|
|
2383
|
+
this.db.prepare("UPDATE memories SET active = 0, updated_at = ? WHERE prefix = 'P' AND active = 1 AND id != ?").run(now, id);
|
|
2384
|
+
this.db.prepare("UPDATE memories SET active = 1, updated_at = ? WHERE id = ?").run(now, id);
|
|
2385
|
+
});
|
|
2386
|
+
tx();
|
|
2387
|
+
}
|
|
2373
2388
|
/** Auto-resolve linked entries on an entry (extracted for reuse in chain resolution). */
|
|
2374
2389
|
resolveEntryLinks(entry, opts) {
|
|
2375
2390
|
const linkDepth = opts.resolveLinks === false ? 0 : (opts.linkDepth ?? 1);
|
|
@@ -2752,6 +2767,7 @@ export class HmemStore {
|
|
|
2752
2767
|
if (!targetExists) {
|
|
2753
2768
|
return { moved: 0, errors: [`Target ${targetOId} does not exist`] };
|
|
2754
2769
|
}
|
|
2770
|
+
const l2MovedIntoTarget = new Set();
|
|
2755
2771
|
const doMove = this.db.transaction(() => {
|
|
2756
2772
|
for (const nodeId of nodeIds) {
|
|
2757
2773
|
const node = this.readNode(nodeId);
|
|
@@ -2768,6 +2784,7 @@ export class HmemStore {
|
|
|
2768
2784
|
if (node.depth === 2) {
|
|
2769
2785
|
// L2 session — re-parent directly under target O
|
|
2770
2786
|
this._moveSubtree(nodeId, sourceOId, targetOId, targetOId, 2);
|
|
2787
|
+
l2MovedIntoTarget.add(targetOId);
|
|
2771
2788
|
}
|
|
2772
2789
|
else if (node.depth === 3) {
|
|
2773
2790
|
// L3 batch — find/create session in target O
|
|
@@ -2788,6 +2805,12 @@ export class HmemStore {
|
|
|
2788
2805
|
this._cleanupEmptyParents(sourceOId);
|
|
2789
2806
|
moved++;
|
|
2790
2807
|
}
|
|
2808
|
+
// After moving L2 sessions into a target O, re-sort siblings by created_at
|
|
2809
|
+
// so the moved session lands in its chronologically correct slot rather than
|
|
2810
|
+
// at the end of the seq space.
|
|
2811
|
+
for (const oId of l2MovedIntoTarget) {
|
|
2812
|
+
this.reorderSessionsByDate(oId);
|
|
2813
|
+
}
|
|
2791
2814
|
});
|
|
2792
2815
|
doMove();
|
|
2793
2816
|
return { moved, errors };
|
|
@@ -2876,6 +2899,74 @@ export class HmemStore {
|
|
|
2876
2899
|
const timestamp = new Date().toISOString();
|
|
2877
2900
|
this.db.prepare("UPDATE memories SET updated_at = ? WHERE id = ?").run(timestamp, targetOId);
|
|
2878
2901
|
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Rename an entire L2 session subtree (L2 node + all L3/L4/L5 descendants)
|
|
2904
|
+
* to a new id prefix. Updates memory_nodes (id, parent_id, seq), memory_tags,
|
|
2905
|
+
* and hmem_fts_rowid_map. The root-level parent of the L2 node stays at oId.
|
|
2906
|
+
* Caller must ensure newId does not yet exist.
|
|
2907
|
+
*/
|
|
2908
|
+
_renameL2Subtree(oId, oldId, newId) {
|
|
2909
|
+
const nodes = this.db.prepare("SELECT id, parent_id FROM memory_nodes WHERE id = ? OR id LIKE ?").all(oldId, `${oldId}.%`);
|
|
2910
|
+
const newSeqMatch = newId.match(/\.(\d+)$/);
|
|
2911
|
+
const newSeq = newSeqMatch ? parseInt(newSeqMatch[1], 10) : null;
|
|
2912
|
+
for (const n of nodes) {
|
|
2913
|
+
const nid = n.id === oldId ? newId : n.id.replace(oldId + ".", newId + ".");
|
|
2914
|
+
const pid = n.id === oldId
|
|
2915
|
+
? oId
|
|
2916
|
+
: (n.parent_id === oldId ? newId : n.parent_id.replace(oldId + ".", newId + "."));
|
|
2917
|
+
if (n.id === oldId && newSeq !== null) {
|
|
2918
|
+
this.db.prepare("UPDATE memory_nodes SET id = ?, parent_id = ?, seq = ? WHERE id = ?")
|
|
2919
|
+
.run(nid, pid, newSeq, n.id);
|
|
2920
|
+
}
|
|
2921
|
+
else {
|
|
2922
|
+
this.db.prepare("UPDATE memory_nodes SET id = ?, parent_id = ? WHERE id = ?")
|
|
2923
|
+
.run(nid, pid, n.id);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
// Tags
|
|
2927
|
+
const tagRows = this.db.prepare("SELECT entry_id, tag FROM memory_tags WHERE entry_id = ? OR entry_id LIKE ?").all(oldId, `${oldId}.%`);
|
|
2928
|
+
for (const t of tagRows) {
|
|
2929
|
+
const newEntryId = t.entry_id === oldId ? newId : t.entry_id.replace(oldId + ".", newId + ".");
|
|
2930
|
+
this.db.prepare("DELETE FROM memory_tags WHERE entry_id = ? AND tag = ?").run(t.entry_id, t.tag);
|
|
2931
|
+
this.db.prepare("INSERT OR IGNORE INTO memory_tags (entry_id, tag) VALUES (?, ?)").run(newEntryId, t.tag);
|
|
2932
|
+
}
|
|
2933
|
+
// FTS rowid map
|
|
2934
|
+
const ftsRows = this.db.prepare("SELECT fts_rowid, node_id FROM hmem_fts_rowid_map WHERE node_id = ? OR node_id LIKE ?").all(oldId, `${oldId}.%`);
|
|
2935
|
+
for (const f of ftsRows) {
|
|
2936
|
+
const newNodeId = f.node_id === oldId ? newId : f.node_id.replace(oldId + ".", newId + ".");
|
|
2937
|
+
this.db.prepare("UPDATE hmem_fts_rowid_map SET node_id = ? WHERE fts_rowid = ?").run(newNodeId, f.fts_rowid);
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Reorder L2 sessions under an O-entry so their seq matches chronological
|
|
2942
|
+
* order by created_at (ascending). Uses 2-phase rename via _TMP staging IDs
|
|
2943
|
+
* to avoid collisions during renumbering. Returns the number of sessions
|
|
2944
|
+
* actually renamed.
|
|
2945
|
+
*/
|
|
2946
|
+
reorderSessionsByDate(oId) {
|
|
2947
|
+
const sessions = this.db.prepare("SELECT id, seq, created_at FROM memory_nodes WHERE parent_id = ? AND depth = 2 ORDER BY created_at ASC, seq ASC").all(oId);
|
|
2948
|
+
const renames = [];
|
|
2949
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
2950
|
+
const desiredSeq = i + 1;
|
|
2951
|
+
if (sessions[i].seq !== desiredSeq) {
|
|
2952
|
+
renames.push({ from: sessions[i].id, to: `${oId}.${desiredSeq}` });
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
if (renames.length === 0)
|
|
2956
|
+
return 0;
|
|
2957
|
+
const tx = this.db.transaction(() => {
|
|
2958
|
+
// Phase 1: move every affected session into staging
|
|
2959
|
+
for (let i = 0; i < renames.length; i++) {
|
|
2960
|
+
this._renameL2Subtree(oId, renames[i].from, `${oId}._TMP${i}`);
|
|
2961
|
+
}
|
|
2962
|
+
// Phase 2: rename staging → final
|
|
2963
|
+
for (let i = 0; i < renames.length; i++) {
|
|
2964
|
+
this._renameL2Subtree(oId, `${oId}._TMP${i}`, renames[i].to);
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2967
|
+
tx();
|
|
2968
|
+
return renames.length;
|
|
2969
|
+
}
|
|
2879
2970
|
/**
|
|
2880
2971
|
* Remove empty L2 (sessions) and L3 (batches) nodes in an O-entry.
|
|
2881
2972
|
*/
|
|
@@ -4050,7 +4141,9 @@ export function resolveHmemPath(cwdOverride) {
|
|
|
4050
4141
|
// Priority 2: CWD discovery
|
|
4051
4142
|
const cwd = cwdOverride || process.cwd();
|
|
4052
4143
|
try {
|
|
4053
|
-
const files = fs.readdirSync(cwd
|
|
4144
|
+
const files = fs.readdirSync(cwd, { withFileTypes: true })
|
|
4145
|
+
.filter(e => e.isFile() && e.name.endsWith(".hmem"))
|
|
4146
|
+
.map(e => e.name);
|
|
4054
4147
|
if (files.length === 1)
|
|
4055
4148
|
return path.resolve(cwd, files[0]);
|
|
4056
4149
|
if (files.length > 1) {
|