pi-hermes-memory 0.7.5 → 0.7.6

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/README.md CHANGED
@@ -363,6 +363,7 @@ Create `~/.pi/agent/hermes-memory-config.json`:
363
363
  "failureInjectionEnabled": true,
364
364
  "failureInjectionMaxAgeDays": 7,
365
365
  "failureInjectionMaxEntries": 5,
366
+ "consolidationTimeoutMs": 60000,
366
367
  "flushOnCompact": true,
367
368
  "flushOnShutdown": true,
368
369
  "flushMinTurns": 6,
@@ -386,6 +387,7 @@ Create `~/.pi/agent/hermes-memory-config.json`:
386
387
  | `reviewEnabled` | `true` | Enable/disable background learning loop |
387
388
  | `memoryOverflowStrategy` | `auto-consolidate` | Behavior when MEMORY.md, USER.md, or project-scoped memory reaches its character limit: `auto-consolidate` runs the existing consolidation flow; `reject` returns an error; `fifo-evict` rotates older entries in file order until the new entry fits |
388
389
  | `autoConsolidate` | `true` | Legacy alias for `memoryOverflowStrategy` when `memoryOverflowStrategy` is not set (`true` = `auto-consolidate`, `false` = `reject`) |
390
+ | `consolidationTimeoutMs` | `60000` | Maximum time in milliseconds for auto-consolidation to complete |
389
391
  | `correctionDetection` | `true` | Detect user corrections and save immediately |
390
392
  | `correctionStrongPatterns` | unset | Optional case-insensitive regex sources replacing strong correction patterns; omitted preserves defaults, invalid entries are ignored |
391
393
  | `correctionWeakPatterns` | unset | Optional case-insensitive regex sources replacing weak correction patterns; omitted preserves defaults, invalid entries are ignored |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hermes-memory",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "🧠 Persistent memory + 🔍 session search + 🛡️ secret scanning for Pi. Token-aware policy-only memory by default, SQLite FTS5 search, auto-consolidation, procedural skills. 368 tests. Ported from Hermes agent.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/config.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  DEFAULT_NUDGE_TOOL_CALLS,
13
13
  DEFAULT_REVIEW_RECENT_MESSAGES,
14
14
  DEFAULT_FLUSH_RECENT_MESSAGES,
15
+ DEFAULT_CONSOLIDATION_TIMEOUT_MS,
15
16
  DEFAULT_FAILURE_INJECTION_MAX_AGE_DAYS,
16
17
  DEFAULT_FAILURE_INJECTION_MAX_ENTRIES,
17
18
  } from "./constants.js";
@@ -41,6 +42,7 @@ const DEFAULT_CONFIG: MemoryConfig = {
41
42
  failureInjectionEnabled: true,
42
43
  failureInjectionMaxAgeDays: DEFAULT_FAILURE_INJECTION_MAX_AGE_DAYS,
43
44
  failureInjectionMaxEntries: DEFAULT_FAILURE_INJECTION_MAX_ENTRIES,
45
+ consolidationTimeoutMs: DEFAULT_CONSOLIDATION_TIMEOUT_MS,
44
46
  nudgeToolCalls: DEFAULT_NUDGE_TOOL_CALLS,
45
47
  projectsMemoryDir: DEFAULT_PROJECTS_MEMORY_DIR,
46
48
  };
@@ -97,6 +99,7 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
97
99
  if (isStringArray(parsed.correctionWeakPatterns)) config.correctionWeakPatterns = parsed.correctionWeakPatterns;
98
100
  if (isStringArray(parsed.correctionNegativePatterns)) config.correctionNegativePatterns = parsed.correctionNegativePatterns;
99
101
  if (isStringArray(parsed.correctionDirectiveWords)) config.correctionDirectiveWords = parsed.correctionDirectiveWords;
102
+ if (typeof parsed.consolidationTimeoutMs === "number") config.consolidationTimeoutMs = parsed.consolidationTimeoutMs;
100
103
  if (typeof parsed.failureInjectionEnabled === "boolean") config.failureInjectionEnabled = parsed.failureInjectionEnabled;
101
104
  if (typeof parsed.failureInjectionMaxAgeDays === "number") config.failureInjectionMaxAgeDays = parsed.failureInjectionMaxAgeDays;
102
105
  if (typeof parsed.failureInjectionMaxEntries === "number") config.failureInjectionMaxEntries = parsed.failureInjectionMaxEntries;
package/src/constants.ts CHANGED
@@ -23,6 +23,7 @@ export const DEFAULT_NUDGE_TOOL_CALLS = 15;
23
23
  export const DEFAULT_REVIEW_RECENT_MESSAGES = 0;
24
24
  export const DEFAULT_FLUSH_RECENT_MESSAGES = 0;
25
25
  export const DEFAULT_SKILL_TRIGGER_TOOL_CALLS = 8;
26
+ export const DEFAULT_CONSOLIDATION_TIMEOUT_MS = 60000;
26
27
  export const DEFAULT_FAILURE_INJECTION_MAX_AGE_DAYS = 7;
27
28
  export const DEFAULT_FAILURE_INJECTION_MAX_ENTRIES = 5;
28
29
 
@@ -17,6 +17,7 @@ export async function triggerConsolidation(
17
17
  store: MemoryStore,
18
18
  target: "memory" | "user" | "failure",
19
19
  signal?: AbortSignal,
20
+ timeoutMs: number = 60000,
20
21
  ): Promise<ConsolidationResult> {
21
22
  const entries =
22
23
  target === "memory" ? store.getMemoryEntries() : store.getUserEntries();
@@ -34,7 +35,7 @@ export async function triggerConsolidation(
34
35
  try {
35
36
  const result = await pi.exec("pi", ["-p", "--no-session", prompt], {
36
37
  signal,
37
- timeout: 60000,
38
+ timeout: timeoutMs,
38
39
  });
39
40
 
40
41
  if (result.code === 0) {
@@ -58,6 +59,7 @@ export async function triggerConsolidation(
58
59
  export function registerConsolidateCommand(
59
60
  pi: ExtensionAPI,
60
61
  store: MemoryStore,
62
+ timeoutMs: number = 60000,
61
63
  ): void {
62
64
  pi.registerCommand("memory-consolidate", {
63
65
  description: "Manually trigger memory consolidation to free up space",
@@ -75,7 +77,7 @@ export function registerConsolidateCommand(
75
77
  continue;
76
78
  }
77
79
 
78
- const result = await triggerConsolidation(pi, store, target, ctx.signal);
80
+ const result = await triggerConsolidation(pi, store, target, ctx.signal, timeoutMs);
79
81
 
80
82
  if (result.consolidated) {
81
83
  await store.loadFromDisk();
package/src/index.ts CHANGED
@@ -111,9 +111,9 @@ export default function (pi: ExtensionAPI) {
111
111
 
112
112
  // ── 7. Setup auto-consolidation (inject consolidator into store) ──
113
113
  store.setConsolidator(async (target, signal) => {
114
- return triggerConsolidation(pi, store, target, signal);
114
+ return triggerConsolidation(pi, store, target, signal, config.consolidationTimeoutMs);
115
115
  });
116
- registerConsolidateCommand(pi, store);
116
+ registerConsolidateCommand(pi, store, config.consolidationTimeoutMs);
117
117
 
118
118
  // ── 8. Setup correction detection ──
119
119
  setupCorrectionDetector(pi, store, projectStore, config, dbManager, projectName);
@@ -67,6 +67,11 @@ export interface SqliteMemoryRemoveResult {
67
67
  removed: number;
68
68
  }
69
69
 
70
+ export interface SqliteMemoryRemoveOptions {
71
+ target: 'memory' | 'user' | 'failure';
72
+ project?: string | null;
73
+ }
74
+
70
75
  export interface ParsedMarkdownMemoryEntry extends SqliteMemorySyncInput {}
71
76
 
72
77
  function today(): string {
@@ -481,10 +486,7 @@ export function replaceSyncedMemories(
481
486
  export function removeSyncedMemories(
482
487
  dbManager: DatabaseManager,
483
488
  oldText: string,
484
- options: {
485
- target: 'memory' | 'user' | 'failure';
486
- project?: string | null;
487
- },
489
+ options: SqliteMemoryRemoveOptions,
488
490
  ): SqliteMemoryRemoveResult {
489
491
  const db = dbManager.getDb();
490
492
  const params: unknown[] = [];
@@ -512,6 +514,42 @@ export function removeSyncedMemories(
512
514
  };
513
515
  }
514
516
 
517
+ /**
518
+ * Exact removal for Markdown entries whose full content is known.
519
+ * Used for FIFO eviction cleanup, where substring matching could remove
520
+ * unrelated SQLite mirror rows that merely contain the evicted text.
521
+ */
522
+ export function removeExactSyncedMemories(
523
+ dbManager: DatabaseManager,
524
+ content: string,
525
+ options: SqliteMemoryRemoveOptions,
526
+ ): SqliteMemoryRemoveResult {
527
+ const db = dbManager.getDb();
528
+ const params: unknown[] = [];
529
+ const conditions = buildScopeConditions(params, options.target, options.project ?? undefined);
530
+ conditions.push('content = ?');
531
+ params.push(content.trim());
532
+
533
+ const matchingIds = db.prepare(`
534
+ SELECT id
535
+ FROM memories
536
+ WHERE ${conditions.join(' AND ')}
537
+ `).all(...params) as Array<{ id: number }>;
538
+
539
+ if (matchingIds.length === 0) {
540
+ return { matched: 0, removed: 0 };
541
+ }
542
+
543
+ const deleteParams = matchingIds.map((row) => row.id);
544
+ const placeholders = deleteParams.map(() => '?').join(', ');
545
+ const result = db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...deleteParams);
546
+
547
+ return {
548
+ matched: matchingIds.length,
549
+ removed: result.changes,
550
+ };
551
+ }
552
+
515
553
  /**
516
554
  * Escape a string for FTS5 query syntax.
517
555
  * Wraps the query in double quotes to treat it as a literal phrase.
@@ -11,6 +11,7 @@ import { MemoryStore } from "../store/memory-store.js";
11
11
  import { DatabaseManager } from "../store/db.js";
12
12
  import {
13
13
  formatFailureMemoryContent,
14
+ removeExactSyncedMemories,
14
15
  removeSyncedMemories,
15
16
  replaceSyncedMemories,
16
17
  syncMemoryEntry,
@@ -159,6 +160,31 @@ async function syncRemoveFromSqlite(
159
160
  }
160
161
  }
161
162
 
163
+ async function syncEvictionsFromSqlite(
164
+ rawTarget: "memory" | "user" | "project" | "failure",
165
+ evictedEntries: string[] | undefined,
166
+ dbManager: DatabaseManager | null,
167
+ projectName?: string | null,
168
+ ): Promise<void> {
169
+ if (!dbManager) return;
170
+ if (!evictedEntries || evictedEntries.length === 0) return;
171
+
172
+ const sqliteTarget = sqliteTargetFor(rawTarget);
173
+ const sqliteProject = sqliteProjectFor(rawTarget, projectName);
174
+
175
+ for (const entry of evictedEntries) {
176
+ try {
177
+ removeExactSyncedMemories(dbManager, entry, {
178
+ target: sqliteTarget,
179
+ project: sqliteProject,
180
+ });
181
+ } catch {
182
+ // FIFO already updated the Markdown source of truth. SQLite is only a
183
+ // best-effort search mirror, so eviction cleanup must not fail the write.
184
+ }
185
+ }
186
+ }
187
+
162
188
  export function registerMemoryTool(
163
189
  pi: ExtensionAPI,
164
190
  store: MemoryStore,
@@ -247,6 +273,7 @@ export function registerMemoryTool(
247
273
  } else {
248
274
  result = await store_.add(target, content);
249
275
  if (result.success) {
276
+ await syncEvictionsFromSqlite(rawTarget, result.evicted_entries, dbManager, projectName);
250
277
  syncWarning = await syncAddToSqlite(rawTarget, content, undefined, undefined, dbManager, projectName);
251
278
  }
252
279
  }
package/src/types.ts CHANGED
@@ -59,6 +59,8 @@ export interface MemoryConfig {
59
59
  failureInjectionMaxEntries: number;
60
60
  /** Tool calls before triggering background review (in addition to turn count). Default: 15 */
61
61
  nudgeToolCalls: number;
62
+ /** Maximum time in milliseconds for auto-consolidation to complete. Default: 60000 */
63
+ consolidationTimeoutMs: number;
62
64
  /** Enable session history search via SQLite FTS5. Default: true */
63
65
  sessionSearchEnabled?: boolean;
64
66
  /** Days to retain session history. Default: 90 */