pi-hermes-memory 0.7.4 → 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 +5 -1
- package/package.json +1 -1
- package/src/config.ts +26 -2
- package/src/constants.ts +1 -0
- package/src/handlers/auto-consolidate.ts +4 -2
- package/src/index.ts +2 -2
- package/src/store/memory-store.ts +55 -7
- package/src/store/sqlite-memory-store.ts +42 -4
- package/src/tools/memory-tool.ts +51 -1
- package/src/types.ts +9 -1
package/README.md
CHANGED
|
@@ -357,11 +357,13 @@ 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,
|
|
363
364
|
"failureInjectionMaxAgeDays": 7,
|
|
364
365
|
"failureInjectionMaxEntries": 5,
|
|
366
|
+
"consolidationTimeoutMs": 60000,
|
|
365
367
|
"flushOnCompact": true,
|
|
366
368
|
"flushOnShutdown": true,
|
|
367
369
|
"flushMinTurns": 6,
|
|
@@ -383,7 +385,9 @@ Create `~/.pi/agent/hermes-memory-config.json`:
|
|
|
383
385
|
| `nudgeToolCalls` | `15` | Tool calls between auto-reviews (OR with turns) |
|
|
384
386
|
| `reviewRecentMessages` | `0` | Recent messages included in background review (`0` = all) |
|
|
385
387
|
| `reviewEnabled` | `true` | Enable/disable background learning loop |
|
|
386
|
-
| `
|
|
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 |
|
|
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 |
|
|
387
391
|
| `correctionDetection` | `true` | Detect user corrections and save immediately |
|
|
388
392
|
| `correctionStrongPatterns` | unset | Optional case-insensitive regex sources replacing strong correction patterns; omitted preserves defaults, invalid entries are ignored |
|
|
389
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.
|
|
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
|
@@ -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,
|
|
@@ -12,10 +12,17 @@ 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";
|
|
18
19
|
|
|
20
|
+
const MEMORY_OVERFLOW_STRATEGIES: readonly MemoryOverflowStrategy[] = ["auto-consolidate", "reject", "fifo-evict"];
|
|
21
|
+
|
|
22
|
+
function isMemoryOverflowStrategy(value: unknown): value is MemoryOverflowStrategy {
|
|
23
|
+
return typeof value === "string" && MEMORY_OVERFLOW_STRATEGIES.includes(value as MemoryOverflowStrategy);
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
const DEFAULT_CONFIG: MemoryConfig = {
|
|
20
27
|
memoryMode: "policy-only",
|
|
21
28
|
memoryPolicyStyle: "full",
|
|
@@ -29,11 +36,13 @@ const DEFAULT_CONFIG: MemoryConfig = {
|
|
|
29
36
|
flushOnShutdown: true,
|
|
30
37
|
flushMinTurns: DEFAULT_FLUSH_MIN_TURNS,
|
|
31
38
|
flushRecentMessages: DEFAULT_FLUSH_RECENT_MESSAGES,
|
|
39
|
+
memoryOverflowStrategy: "auto-consolidate",
|
|
32
40
|
autoConsolidate: true,
|
|
33
41
|
correctionDetection: true,
|
|
34
42
|
failureInjectionEnabled: true,
|
|
35
43
|
failureInjectionMaxAgeDays: DEFAULT_FAILURE_INJECTION_MAX_AGE_DAYS,
|
|
36
44
|
failureInjectionMaxEntries: DEFAULT_FAILURE_INJECTION_MAX_ENTRIES,
|
|
45
|
+
consolidationTimeoutMs: DEFAULT_CONSOLIDATION_TIMEOUT_MS,
|
|
37
46
|
nudgeToolCalls: DEFAULT_NUDGE_TOOL_CALLS,
|
|
38
47
|
projectsMemoryDir: DEFAULT_PROJECTS_MEMORY_DIR,
|
|
39
48
|
};
|
|
@@ -58,6 +67,8 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
58
67
|
const isStringArray = (value: unknown): value is string[] => (
|
|
59
68
|
Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
60
69
|
);
|
|
70
|
+
let hasLegacyAutoConsolidate = false;
|
|
71
|
+
let hasMemoryOverflowStrategy = false;
|
|
61
72
|
if (parsed.memoryMode === "policy-only" || parsed.memoryMode === "legacy-inject") config.memoryMode = parsed.memoryMode;
|
|
62
73
|
if (
|
|
63
74
|
parsed.memoryPolicyStyle === "full" ||
|
|
@@ -75,12 +86,20 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
75
86
|
if (typeof parsed.flushOnShutdown === "boolean") config.flushOnShutdown = parsed.flushOnShutdown;
|
|
76
87
|
if (typeof parsed.flushMinTurns === "number") config.flushMinTurns = parsed.flushMinTurns;
|
|
77
88
|
if (isNonNegativeNumber(parsed.flushRecentMessages)) config.flushRecentMessages = parsed.flushRecentMessages;
|
|
78
|
-
if (typeof parsed.autoConsolidate === "boolean")
|
|
89
|
+
if (typeof parsed.autoConsolidate === "boolean") {
|
|
90
|
+
config.autoConsolidate = parsed.autoConsolidate;
|
|
91
|
+
hasLegacyAutoConsolidate = true;
|
|
92
|
+
}
|
|
93
|
+
if (isMemoryOverflowStrategy(parsed.memoryOverflowStrategy)) {
|
|
94
|
+
config.memoryOverflowStrategy = parsed.memoryOverflowStrategy;
|
|
95
|
+
hasMemoryOverflowStrategy = true;
|
|
96
|
+
}
|
|
79
97
|
if (typeof parsed.correctionDetection === "boolean") config.correctionDetection = parsed.correctionDetection;
|
|
80
98
|
if (isStringArray(parsed.correctionStrongPatterns)) config.correctionStrongPatterns = parsed.correctionStrongPatterns;
|
|
81
99
|
if (isStringArray(parsed.correctionWeakPatterns)) config.correctionWeakPatterns = parsed.correctionWeakPatterns;
|
|
82
100
|
if (isStringArray(parsed.correctionNegativePatterns)) config.correctionNegativePatterns = parsed.correctionNegativePatterns;
|
|
83
101
|
if (isStringArray(parsed.correctionDirectiveWords)) config.correctionDirectiveWords = parsed.correctionDirectiveWords;
|
|
102
|
+
if (typeof parsed.consolidationTimeoutMs === "number") config.consolidationTimeoutMs = parsed.consolidationTimeoutMs;
|
|
84
103
|
if (typeof parsed.failureInjectionEnabled === "boolean") config.failureInjectionEnabled = parsed.failureInjectionEnabled;
|
|
85
104
|
if (typeof parsed.failureInjectionMaxAgeDays === "number") config.failureInjectionMaxAgeDays = parsed.failureInjectionMaxAgeDays;
|
|
86
105
|
if (typeof parsed.failureInjectionMaxEntries === "number") config.failureInjectionMaxEntries = parsed.failureInjectionMaxEntries;
|
|
@@ -88,6 +107,11 @@ export function loadConfig(configPath = DEFAULT_CONFIG_PATH): MemoryConfig {
|
|
|
88
107
|
if (typeof parsed.projectCharLimit === "number") config.projectCharLimit = parsed.projectCharLimit;
|
|
89
108
|
if (typeof parsed.memoryDir === "string") config.memoryDir = parsed.memoryDir;
|
|
90
109
|
if (typeof parsed.projectsMemoryDir === "string") config.projectsMemoryDir = parsed.projectsMemoryDir;
|
|
110
|
+
if (hasMemoryOverflowStrategy) {
|
|
111
|
+
config.autoConsolidate = config.memoryOverflowStrategy === "auto-consolidate";
|
|
112
|
+
} else if (hasLegacyAutoConsolidate) {
|
|
113
|
+
config.memoryOverflowStrategy = config.autoConsolidate ? "auto-consolidate" : "reject";
|
|
114
|
+
}
|
|
91
115
|
return config;
|
|
92
116
|
}
|
|
93
117
|
} catch {
|
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:
|
|
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);
|
|
@@ -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 (
|
|
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
|
-
|
|
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();
|
|
@@ -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.
|
package/src/tools/memory-tool.ts
CHANGED
|
@@ -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,
|
|
@@ -29,6 +30,29 @@ function appendSyncWarning(result: MemoryResult, warning: string): MemoryResult
|
|
|
29
30
|
} as MemoryResult;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function formatMemoryToolText(result: MemoryResult): string {
|
|
34
|
+
const evictedEntries = result.evicted_entries ?? [];
|
|
35
|
+
if (result.success && evictedEntries.length > 0) {
|
|
36
|
+
const lines = [
|
|
37
|
+
result.message ?? `Memory updated. Rotated ${evictedEntries.length} older ${evictedEntries.length === 1 ? "entry" : "entries"} to stay within the limit.`,
|
|
38
|
+
"",
|
|
39
|
+
"Rotated active memory entries:",
|
|
40
|
+
"",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
evictedEntries.forEach((entry, index) => {
|
|
44
|
+
lines.push(`${index + 1}. ${entry}`);
|
|
45
|
+
lines.push("");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
lines.push("If one of these entries should stay active, add it again.");
|
|
49
|
+
if (result.usage) lines.push(`Usage: ${result.usage}`);
|
|
50
|
+
return lines.join("\n").trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return JSON.stringify(result);
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
function sqliteProjectFor(rawTarget: "memory" | "user" | "project" | "failure", projectName?: string | null): string | null | undefined {
|
|
33
57
|
if (rawTarget === "project") return projectName?.trim() || null;
|
|
34
58
|
if (rawTarget === "memory") return null;
|
|
@@ -136,6 +160,31 @@ async function syncRemoveFromSqlite(
|
|
|
136
160
|
}
|
|
137
161
|
}
|
|
138
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
|
+
|
|
139
188
|
export function registerMemoryTool(
|
|
140
189
|
pi: ExtensionAPI,
|
|
141
190
|
store: MemoryStore,
|
|
@@ -224,6 +273,7 @@ export function registerMemoryTool(
|
|
|
224
273
|
} else {
|
|
225
274
|
result = await store_.add(target, content);
|
|
226
275
|
if (result.success) {
|
|
276
|
+
await syncEvictionsFromSqlite(rawTarget, result.evicted_entries, dbManager, projectName);
|
|
227
277
|
syncWarning = await syncAddToSqlite(rawTarget, content, undefined, undefined, dbManager, projectName);
|
|
228
278
|
}
|
|
229
279
|
}
|
|
@@ -305,7 +355,7 @@ export function registerMemoryTool(
|
|
|
305
355
|
}
|
|
306
356
|
|
|
307
357
|
return {
|
|
308
|
-
content: [{ type: "text", text:
|
|
358
|
+
content: [{ type: "text", text: formatMemoryToolText(result) }],
|
|
309
359
|
details: result,
|
|
310
360
|
};
|
|
311
361
|
},
|
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
|
-
/**
|
|
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;
|
|
@@ -55,6 +59,8 @@ export interface MemoryConfig {
|
|
|
55
59
|
failureInjectionMaxEntries: number;
|
|
56
60
|
/** Tool calls before triggering background review (in addition to turn count). Default: 15 */
|
|
57
61
|
nudgeToolCalls: number;
|
|
62
|
+
/** Maximum time in milliseconds for auto-consolidation to complete. Default: 60000 */
|
|
63
|
+
consolidationTimeoutMs: number;
|
|
58
64
|
/** Enable session history search via SQLite FTS5. Default: true */
|
|
59
65
|
sessionSearchEnabled?: boolean;
|
|
60
66
|
/** Days to retain session history. Default: 90 */
|
|
@@ -79,6 +85,8 @@ export interface MemoryResult {
|
|
|
79
85
|
entries?: string[];
|
|
80
86
|
usage?: string;
|
|
81
87
|
entry_count?: number;
|
|
88
|
+
evicted_entries?: string[];
|
|
89
|
+
evicted_count?: number;
|
|
82
90
|
matches?: string[];
|
|
83
91
|
}
|
|
84
92
|
|