hmem-mcp 6.1.0 → 6.1.1

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.
@@ -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
  */
@@ -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
  */