pi-hermes-memory 0.7.4 → 0.7.5

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
@@ -357,6 +357,7 @@ Create `~/.pi/agent/hermes-memory-config.json`:
357
357
  "nudgeToolCalls": 15,
358
358
  "reviewRecentMessages": 0,
359
359
  "reviewEnabled": true,
360
+ "memoryOverflowStrategy": "auto-consolidate",
360
361
  "autoConsolidate": true,
361
362
  "correctionDetection": true,
362
363
  "failureInjectionEnabled": true,
@@ -383,7 +384,8 @@ Create `~/.pi/agent/hermes-memory-config.json`:
383
384
  | `nudgeToolCalls` | `15` | Tool calls between auto-reviews (OR with turns) |
384
385
  | `reviewRecentMessages` | `0` | Recent messages included in background review (`0` = all) |
385
386
  | `reviewEnabled` | `true` | Enable/disable background learning loop |
386
- | `autoConsolidate` | `true` | Auto-merge when memory hits capacity |
387
+ | `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
+ | `autoConsolidate` | `true` | Legacy alias for `memoryOverflowStrategy` when `memoryOverflowStrategy` is not set (`true` = `auto-consolidate`, `false` = `reject`) |
387
389
  | `correctionDetection` | `true` | Detect user corrections and save immediately |
388
390
  | `correctionStrongPatterns` | unset | Optional case-insensitive regex sources replacing strong correction patterns; omitted preserves defaults, invalid entries are ignored |
389
391
  | `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.4",
3
+ "version": "0.7.5",
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
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import * as os from "node:os";
4
- import type { MemoryConfig } from "./types.js";
4
+ import type { MemoryConfig, MemoryOverflowStrategy } from "./types.js";
5
5
  import {
6
6
  DEFAULT_MEMORY_CHAR_LIMIT,
7
7
  DEFAULT_USER_CHAR_LIMIT,
@@ -16,6 +16,12 @@ import {
16
16
  DEFAULT_FAILURE_INJECTION_MAX_ENTRIES,
17
17
  } from "./constants.js";
18
18
 
19
+ const MEMORY_OVERFLOW_STRATEGIES: readonly MemoryOverflowStrategy[] = ["auto-consolidate", "reject", "fifo-evict"];
20
+
21
+ function isMemoryOverflowStrategy(value: unknown): value is MemoryOverflowStrategy {
22
+ return typeof value === "string" && MEMORY_OVERFLOW_STRATEGIES.includes(value as MemoryOverflowStrategy);
23
+ }
24
+
19
25
  const DEFAULT_CONFIG: MemoryConfig = {
20
26
  memoryMode: "policy-only",
21
27
  memoryPolicyStyle: "full",
@@ -29,6 +35,7 @@ const DEFAULT_CONFIG: MemoryConfig = {
29
35
  flushOnShutdown: true,
30
36
  flushMinTurns: DEFAULT_FLUSH_MIN_TURNS,
31
37
  flushRecentMessages: DEFAULT_FLUSH_RECENT_MESSAGES,
38
+ memoryOverflowStrategy: "auto-consolidate",
32
39
  autoConsolidate: true,
33
40
  correctionDetection: true,
34
41
  failureInjectionEnabled: true,
@@ -58,6 +65,8 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
58
65
  const isStringArray = (value: unknown): value is string[] => (
59
66
  Array.isArray(value) && value.every((item) => typeof item === "string")
60
67
  );
68
+ let hasLegacyAutoConsolidate = false;
69
+ let hasMemoryOverflowStrategy = false;
61
70
  if (parsed.memoryMode === "policy-only" || parsed.memoryMode === "legacy-inject") config.memoryMode = parsed.memoryMode;
62
71
  if (
63
72
  parsed.memoryPolicyStyle === "full" ||
@@ -75,7 +84,14 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
75
84
  if (typeof parsed.flushOnShutdown === "boolean") config.flushOnShutdown = parsed.flushOnShutdown;
76
85
  if (typeof parsed.flushMinTurns === "number") config.flushMinTurns = parsed.flushMinTurns;
77
86
  if (isNonNegativeNumber(parsed.flushRecentMessages)) config.flushRecentMessages = parsed.flushRecentMessages;
78
- if (typeof parsed.autoConsolidate === "boolean") config.autoConsolidate = parsed.autoConsolidate;
87
+ if (typeof parsed.autoConsolidate === "boolean") {
88
+ config.autoConsolidate = parsed.autoConsolidate;
89
+ hasLegacyAutoConsolidate = true;
90
+ }
91
+ if (isMemoryOverflowStrategy(parsed.memoryOverflowStrategy)) {
92
+ config.memoryOverflowStrategy = parsed.memoryOverflowStrategy;
93
+ hasMemoryOverflowStrategy = true;
94
+ }
79
95
  if (typeof parsed.correctionDetection === "boolean") config.correctionDetection = parsed.correctionDetection;
80
96
  if (isStringArray(parsed.correctionStrongPatterns)) config.correctionStrongPatterns = parsed.correctionStrongPatterns;
81
97
  if (isStringArray(parsed.correctionWeakPatterns)) config.correctionWeakPatterns = parsed.correctionWeakPatterns;
@@ -88,6 +104,11 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
88
104
  if (typeof parsed.projectCharLimit === "number") config.projectCharLimit = parsed.projectCharLimit;
89
105
  if (typeof parsed.memoryDir === "string") config.memoryDir = parsed.memoryDir;
90
106
  if (typeof parsed.projectsMemoryDir === "string") config.projectsMemoryDir = parsed.projectsMemoryDir;
107
+ if (hasMemoryOverflowStrategy) {
108
+ config.autoConsolidate = config.memoryOverflowStrategy === "auto-consolidate";
109
+ } else if (hasLegacyAutoConsolidate) {
110
+ config.memoryOverflowStrategy = config.autoConsolidate ? "auto-consolidate" : "reject";
111
+ }
91
112
  return config;
92
113
  }
93
114
  } catch {
@@ -24,7 +24,7 @@ import {
24
24
  MEMORY_FILE,
25
25
  USER_FILE,
26
26
  } from "../constants.js";
27
- import type { MemoryConfig, MemoryResult, MemorySnapshot, ConsolidationResult, MemoryCategory } from "../types.js";
27
+ import type { MemoryConfig, MemoryResult, MemorySnapshot, ConsolidationResult, MemoryCategory, MemoryOverflowStrategy } from "../types.js";
28
28
 
29
29
  export class MemoryStore {
30
30
  private memoryEntries: string[] = [];
@@ -77,6 +77,10 @@ export class MemoryStore {
77
77
  return entries.length ? entries.join(ENTRY_DELIMITER).length : 0;
78
78
  }
79
79
 
80
+ private memoryOverflowStrategy(): MemoryOverflowStrategy {
81
+ return this.config.memoryOverflowStrategy ?? (this.config.autoConsolidate ? "auto-consolidate" : "reject");
82
+ }
83
+
80
84
  // ─── Load from disk ───
81
85
 
82
86
  async loadFromDisk(): Promise<void> {
@@ -176,8 +180,14 @@ export class MemoryStore {
176
180
 
177
181
  const newTotal = [...entries, encoded].join(ENTRY_DELIMITER).length;
178
182
  if (newTotal > limit) {
183
+ const strategy = this.memoryOverflowStrategy();
184
+
185
+ if (strategy === "fifo-evict") {
186
+ return this.fifoEvictAndAdd(target, entries, encoded, content.length, limit);
187
+ }
188
+
179
189
  // Auto-consolidate once if configured — limit retries to prevent infinite loops
180
- if (this.config.autoConsolidate && this.consolidator && _retriesLeft > 0) {
190
+ if (strategy === "auto-consolidate" && this.consolidator && _retriesLeft > 0) {
181
191
  try {
182
192
  const result = await this.consolidator(target, signal);
183
193
  if (result.consolidated) {
@@ -190,11 +200,7 @@ export class MemoryStore {
190
200
  // Consolidation failed — fall through to error
191
201
  }
192
202
  }
193
- const current = this.charCount(target);
194
- return {
195
- success: false,
196
- error: `Memory at ${current}/${limit} chars. Adding this entry (${content.length} chars) would exceed the limit. Replace or remove existing entries first.`,
197
- };
203
+ return this.memoryFullError(target, content.length);
198
204
  }
199
205
 
200
206
  entries.push(encoded);
@@ -204,6 +210,48 @@ export class MemoryStore {
204
210
  return this.successResponse(target, "Entry added.");
205
211
  }
206
212
 
213
+ private async fifoEvictAndAdd(
214
+ target: "memory" | "user" | "failure",
215
+ entries: string[],
216
+ encoded: string,
217
+ contentLength: number,
218
+ limit: number,
219
+ ): Promise<MemoryResult> {
220
+ if (encoded.length > limit) {
221
+ return this.memoryFullError(target, contentLength);
222
+ }
223
+
224
+ const remaining = [...entries];
225
+ const evictedEntries: string[] = [];
226
+
227
+ while ([...remaining, encoded].join(ENTRY_DELIMITER).length > limit && remaining.length > 0) {
228
+ const evicted = remaining.shift()!;
229
+ evictedEntries.push(this.stripMetadata(evicted));
230
+ }
231
+
232
+ remaining.push(encoded);
233
+ this.setEntries(target, remaining);
234
+ await this.saveToDisk(target);
235
+
236
+ return {
237
+ ...this.successResponse(
238
+ target,
239
+ `Memory updated. Rotated ${evictedEntries.length} older ${evictedEntries.length === 1 ? "entry" : "entries"} to stay within the limit.`,
240
+ ),
241
+ evicted_entries: evictedEntries,
242
+ evicted_count: evictedEntries.length,
243
+ };
244
+ }
245
+
246
+ private memoryFullError(target: "memory" | "user" | "failure", contentLength: number): MemoryResult {
247
+ const current = this.charCount(target);
248
+ const limit = this.charLimit(target);
249
+ return {
250
+ success: false,
251
+ error: `Memory at ${current}/${limit} chars. Adding this entry (${contentLength} chars) would exceed the limit. Replace or remove existing entries first.`,
252
+ };
253
+ }
254
+
207
255
  async replace(target: "memory" | "user" | "failure", oldText: string, newContent: string): Promise<MemoryResult> {
208
256
  oldText = oldText.trim();
209
257
  newContent = newContent.trim();
@@ -29,6 +29,29 @@ function appendSyncWarning(result: MemoryResult, warning: string): MemoryResult
29
29
  } as MemoryResult;
30
30
  }
31
31
 
32
+ function formatMemoryToolText(result: MemoryResult): string {
33
+ const evictedEntries = result.evicted_entries ?? [];
34
+ if (result.success && evictedEntries.length > 0) {
35
+ const lines = [
36
+ result.message ?? `Memory updated. Rotated ${evictedEntries.length} older ${evictedEntries.length === 1 ? "entry" : "entries"} to stay within the limit.`,
37
+ "",
38
+ "Rotated active memory entries:",
39
+ "",
40
+ ];
41
+
42
+ evictedEntries.forEach((entry, index) => {
43
+ lines.push(`${index + 1}. ${entry}`);
44
+ lines.push("");
45
+ });
46
+
47
+ lines.push("If one of these entries should stay active, add it again.");
48
+ if (result.usage) lines.push(`Usage: ${result.usage}`);
49
+ return lines.join("\n").trim();
50
+ }
51
+
52
+ return JSON.stringify(result);
53
+ }
54
+
32
55
  function sqliteProjectFor(rawTarget: "memory" | "user" | "project" | "failure", projectName?: string | null): string | null | undefined {
33
56
  if (rawTarget === "project") return projectName?.trim() || null;
34
57
  if (rawTarget === "memory") return null;
@@ -305,7 +328,7 @@ export function registerMemoryTool(
305
328
  }
306
329
 
307
330
  return {
308
- content: [{ type: "text", text: JSON.stringify(result) }],
331
+ content: [{ type: "text", text: formatMemoryToolText(result) }],
309
332
  details: result,
310
333
  };
311
334
  },
package/src/types.ts CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  import type { TextContent } from "@mariozechner/pi-ai";
6
6
 
7
+ export type MemoryOverflowStrategy = "auto-consolidate" | "reject" | "fifo-evict";
8
+
7
9
  export interface MemoryConfig {
8
10
  /** Prompt memory mode. Default: policy-only */
9
11
  memoryMode: "policy-only" | "legacy-inject";
@@ -35,7 +37,9 @@ export interface MemoryConfig {
35
37
  memoryDir?: string;
36
38
  /** Directory for project-scoped memory (relative to ~/.pi/agent). Default: "projects-memory" */
37
39
  projectsMemoryDir?: string;
38
- /** Auto-consolidate when memory is full instead of returning error. Default: true */
40
+ /** Strategy when memory is full. Default: auto-consolidate */
41
+ memoryOverflowStrategy?: MemoryOverflowStrategy;
42
+ /** Legacy alias for memoryOverflowStrategy. Default: true */
39
43
  autoConsolidate: boolean;
40
44
  /** Detect user corrections and trigger immediate memory save. Default: true */
41
45
  correctionDetection: boolean;
@@ -79,6 +83,8 @@ export interface MemoryResult {
79
83
  entries?: string[];
80
84
  usage?: string;
81
85
  entry_count?: number;
86
+ evicted_entries?: string[];
87
+ evicted_count?: number;
82
88
  matches?: string[];
83
89
  }
84
90