pi-hermes-memory 0.7.12 → 0.7.13
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hermes-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.13",
|
|
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",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizeMemoryLookupText(text: string): string {
|
|
2
|
+
let normalized = text.trim();
|
|
3
|
+
if (!normalized) return "";
|
|
4
|
+
|
|
5
|
+
const firstNonEmptyLine = normalized
|
|
6
|
+
.split(/\r?\n/)
|
|
7
|
+
.map((line) => line.trim())
|
|
8
|
+
.find((line) => line.length > 0);
|
|
9
|
+
if (firstNonEmptyLine) normalized = firstNonEmptyLine;
|
|
10
|
+
|
|
11
|
+
normalized = normalized.replace(/^\S+\s+\[[^\]]+\]\s+/u, "");
|
|
12
|
+
normalized = normalized.replace(/^(\[[^\]]+\])\s+\1(\s+|$)/, "$1 ");
|
|
13
|
+
|
|
14
|
+
return normalized.trim();
|
|
15
|
+
}
|
|
@@ -15,6 +15,7 @@ import * as fs from "node:fs/promises";
|
|
|
15
15
|
import * as path from "node:path";
|
|
16
16
|
import * as os from "node:os";
|
|
17
17
|
import { scanContent } from "./content-scanner.js";
|
|
18
|
+
import { normalizeMemoryLookupText } from "./memory-lookup.js";
|
|
18
19
|
import {
|
|
19
20
|
ENTRY_DELIMITER,
|
|
20
21
|
DEFAULT_MEMORY_CHAR_LIMIT,
|
|
@@ -117,32 +118,8 @@ export class MemoryStore {
|
|
|
117
118
|
correctedTo?: string;
|
|
118
119
|
project?: string;
|
|
119
120
|
}): Promise<MemoryResult> {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const scanError = scanContent(content);
|
|
124
|
-
if (scanError) return { success: false, error: scanError };
|
|
125
|
-
|
|
126
|
-
const categoryTag = "[" + options.category + "]";
|
|
127
|
-
const parts = [categoryTag + " " + content];
|
|
128
|
-
if (options.failureReason) parts.push("Failed: " + options.failureReason);
|
|
129
|
-
if (options.toolState) parts.push("Tool state: " + options.toolState);
|
|
130
|
-
if (options.correctedTo) parts.push("Corrected to: " + options.correctedTo);
|
|
131
|
-
if (options.project) parts.push("Project: " + options.project);
|
|
132
|
-
|
|
133
|
-
const failureText = parts.join(" — ");
|
|
134
|
-
const today = new Date().toISOString().split("T")[0];
|
|
135
|
-
const encoded = this.encodeEntry(failureText, today, today);
|
|
136
|
-
|
|
137
|
-
this.failureEntries.push(encoded);
|
|
138
|
-
await this.saveToDisk("failure");
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
success: true,
|
|
142
|
-
target: "failure",
|
|
143
|
-
message: "Failure memory saved: " + options.category,
|
|
144
|
-
entry_count: this.failureEntries.length,
|
|
145
|
-
};
|
|
121
|
+
const failureText = this.buildFailureMemoryText(content, options);
|
|
122
|
+
return this._add("failure", failureText, undefined, 1, "Failure memory saved: " + options.category);
|
|
146
123
|
}
|
|
147
124
|
|
|
148
125
|
getFailureEntries(maxAgeDays = 7): string[] {
|
|
@@ -158,7 +135,13 @@ export class MemoryStore {
|
|
|
158
135
|
.map((entry) => this.stripMetadata(entry));
|
|
159
136
|
}
|
|
160
137
|
|
|
161
|
-
private async _add(
|
|
138
|
+
private async _add(
|
|
139
|
+
target: "memory" | "user" | "failure",
|
|
140
|
+
content: string,
|
|
141
|
+
signal?: AbortSignal,
|
|
142
|
+
_retriesLeft = 1,
|
|
143
|
+
addedMessage = "Entry added.",
|
|
144
|
+
): Promise<MemoryResult> {
|
|
162
145
|
content = content.trim();
|
|
163
146
|
if (!content) return { success: false, error: "Content cannot be empty." };
|
|
164
147
|
|
|
@@ -194,7 +177,7 @@ export class MemoryStore {
|
|
|
194
177
|
// CRITICAL: reload from disk — child process modified files, our arrays are stale
|
|
195
178
|
await this.loadFromDisk();
|
|
196
179
|
// Retry the add exactly once (retriesLeft = 0 means no more consolidation)
|
|
197
|
-
return this._add(target, content, signal, _retriesLeft - 1);
|
|
180
|
+
return this._add(target, content, signal, _retriesLeft - 1, addedMessage);
|
|
198
181
|
}
|
|
199
182
|
} catch {
|
|
200
183
|
// Consolidation failed — fall through to error
|
|
@@ -207,7 +190,7 @@ export class MemoryStore {
|
|
|
207
190
|
this.setEntries(target, entries);
|
|
208
191
|
await this.saveToDisk(target);
|
|
209
192
|
|
|
210
|
-
return this.successResponse(target,
|
|
193
|
+
return this.successResponse(target, addedMessage);
|
|
211
194
|
}
|
|
212
195
|
|
|
213
196
|
private async fifoEvictAndAdd(
|
|
@@ -253,7 +236,7 @@ export class MemoryStore {
|
|
|
253
236
|
}
|
|
254
237
|
|
|
255
238
|
async replace(target: "memory" | "user" | "failure", oldText: string, newContent: string): Promise<MemoryResult> {
|
|
256
|
-
oldText = oldText
|
|
239
|
+
oldText = normalizeMemoryLookupText(oldText);
|
|
257
240
|
newContent = newContent.trim();
|
|
258
241
|
if (!oldText) return { success: false, error: "old_text cannot be empty." };
|
|
259
242
|
if (!newContent) return { success: false, error: "new_content cannot be empty. Use 'remove' to delete entries." };
|
|
@@ -299,18 +282,18 @@ export class MemoryStore {
|
|
|
299
282
|
}
|
|
300
283
|
|
|
301
284
|
async remove(target: "memory" | "user" | "failure", oldText: string): Promise<MemoryResult> {
|
|
302
|
-
oldText = oldText
|
|
285
|
+
oldText = normalizeMemoryLookupText(oldText);
|
|
303
286
|
if (!oldText) return { success: false, error: "old_text cannot be empty." };
|
|
304
287
|
|
|
305
288
|
const entries = this.entriesFor(target);
|
|
306
|
-
const matches = entries.filter((e) => e.includes(oldText));
|
|
289
|
+
const matches = entries.filter((e) => this.stripMetadata(e).includes(oldText));
|
|
307
290
|
|
|
308
291
|
if (matches.length === 0) return { success: false, error: `No entry matched '${oldText}'.` };
|
|
309
292
|
if (matches.length > 1 && new Set(matches).size > 1) {
|
|
310
293
|
return {
|
|
311
294
|
success: false,
|
|
312
295
|
error: `Multiple entries matched '${oldText}'. Be more specific.`,
|
|
313
|
-
matches: matches.map((e) => e.slice(0, 80) + (e.length > 80 ? "..." : "")),
|
|
296
|
+
matches: matches.map((e) => this.stripMetadata(e).slice(0, 80) + (this.stripMetadata(e).length > 80 ? "..." : "")),
|
|
314
297
|
};
|
|
315
298
|
}
|
|
316
299
|
|
|
@@ -392,6 +375,23 @@ export class MemoryStore {
|
|
|
392
375
|
return this.decodeEntry(text).text;
|
|
393
376
|
}
|
|
394
377
|
|
|
378
|
+
private buildFailureMemoryText(content: string, options: {
|
|
379
|
+
category: MemoryCategory;
|
|
380
|
+
failureReason?: string;
|
|
381
|
+
toolState?: string;
|
|
382
|
+
correctedTo?: string;
|
|
383
|
+
project?: string;
|
|
384
|
+
}): string {
|
|
385
|
+
const trimmedContent = content.trim();
|
|
386
|
+
const categoryTag = "[" + options.category + "]";
|
|
387
|
+
const parts = [categoryTag + " " + trimmedContent];
|
|
388
|
+
if (options.failureReason) parts.push("Failed: " + options.failureReason);
|
|
389
|
+
if (options.toolState) parts.push("Tool state: " + options.toolState);
|
|
390
|
+
if (options.correctedTo) parts.push("Corrected to: " + options.correctedTo);
|
|
391
|
+
if (options.project) parts.push("Project: " + options.project);
|
|
392
|
+
return parts.join(" — ");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
395
|
private successResponse(target: "memory" | "user" | "failure", message?: string): MemoryResult {
|
|
396
396
|
const entries = this.entriesFor(target);
|
|
397
397
|
const current = this.charCount(target);
|
|
@@ -401,7 +401,6 @@ export class MemoryStore {
|
|
|
401
401
|
const resp: MemoryResult = {
|
|
402
402
|
success: true,
|
|
403
403
|
target,
|
|
404
|
-
entries,
|
|
405
404
|
usage: `${pct}% — ${current}/${limit} chars`,
|
|
406
405
|
entry_count: entries.length,
|
|
407
406
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
2
|
import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
3
|
+
import { normalizeMemoryLookupText } from './memory-lookup.js';
|
|
3
4
|
import type { MemoryCategory } from '../types.js';
|
|
4
5
|
|
|
5
6
|
const MEMORY_SELECT_COLUMNS = `
|
|
@@ -421,10 +422,12 @@ export function replaceSyncedMemories(
|
|
|
421
422
|
},
|
|
422
423
|
): SqliteMemoryUpdateResult {
|
|
423
424
|
const db = dbManager.getDb();
|
|
425
|
+
const normalizedOldText = normalizeMemoryLookupText(oldText);
|
|
426
|
+
if (!normalizedOldText) return { matched: 0, updated: 0, entries: [] };
|
|
424
427
|
const params: unknown[] = [];
|
|
425
428
|
const conditions = buildScopeConditions(params, updates.target, updates.project ?? undefined);
|
|
426
429
|
conditions.push(`content LIKE ? ESCAPE '\\'`);
|
|
427
|
-
params.push(`%${escapeLikePattern(
|
|
430
|
+
params.push(`%${escapeLikePattern(normalizedOldText)}%`);
|
|
428
431
|
|
|
429
432
|
const rows = db.prepare(`
|
|
430
433
|
SELECT ${MEMORY_SELECT_COLUMNS}
|
|
@@ -490,10 +493,12 @@ export function removeSyncedMemories(
|
|
|
490
493
|
options: SqliteMemoryRemoveOptions,
|
|
491
494
|
): SqliteMemoryRemoveResult {
|
|
492
495
|
const db = dbManager.getDb();
|
|
496
|
+
const normalizedOldText = normalizeMemoryLookupText(oldText);
|
|
497
|
+
if (!normalizedOldText) return { matched: 0, removed: 0 };
|
|
493
498
|
const params: unknown[] = [];
|
|
494
499
|
const conditions = buildScopeConditions(params, options.target, options.project ?? undefined);
|
|
495
500
|
conditions.push(`content LIKE ? ESCAPE '\\'`);
|
|
496
|
-
params.push(`%${escapeLikePattern(
|
|
501
|
+
params.push(`%${escapeLikePattern(normalizedOldText)}%`);
|
|
497
502
|
|
|
498
503
|
const matchingIds = db.prepare(`
|
|
499
504
|
SELECT id
|