pi-hermes-memory 0.7.11 → 0.7.12
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 -0
- package/package.json +1 -1
- package/src/handlers/auto-consolidate.ts +46 -15
- package/src/index.ts +8 -2
- package/src/store/db.ts +3 -0
- package/src/store/fts-query.ts +38 -0
- package/src/store/session-search.ts +14 -16
- package/src/store/sqlite-memory-store.ts +31 -28
package/README.md
CHANGED
|
@@ -278,6 +278,11 @@ By default, the extension indexes your Pi session history into a SQLite database
|
|
|
278
278
|
| `session_search` | Search past conversations — "what did we discuss about auth?" |
|
|
279
279
|
| `memory_search` | Search extended memory store — unlimited capacity, keyword-based |
|
|
280
280
|
|
|
281
|
+
Search behavior notes:
|
|
282
|
+
- Multi-word natural-language queries are supported for both `memory_search` and `session_search`.
|
|
283
|
+
- Exact phrases can be requested with quotes, for example `"memory search"`.
|
|
284
|
+
- Advanced FTS queries with operators like `OR` still work when you need them.
|
|
285
|
+
|
|
281
286
|
Session history is indexed automatically on session shutdown. To bulk-import existing sessions:
|
|
282
287
|
|
|
283
288
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hermes-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.12",
|
|
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",
|
|
@@ -12,24 +12,38 @@ import { MemoryStore } from "../store/memory-store.js";
|
|
|
12
12
|
import { CONSOLIDATION_PROMPT, ENTRY_DELIMITER } from "../constants.js";
|
|
13
13
|
import type { ConsolidationResult } from "../types.js";
|
|
14
14
|
|
|
15
|
+
type MemoryTarget = "memory" | "user" | "failure";
|
|
16
|
+
type ToolMemoryTarget = MemoryTarget | "project";
|
|
17
|
+
|
|
18
|
+
function entriesForTarget(store: MemoryStore, target: MemoryTarget): string[] {
|
|
19
|
+
return target === "user" ? store.getUserEntries() : store.getMemoryEntries();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function labelForTarget(target: MemoryTarget, toolTarget: ToolMemoryTarget): string {
|
|
23
|
+
if (toolTarget === "project") return "Project Memory";
|
|
24
|
+
if (target === "user") return "User Profile";
|
|
25
|
+
if (target === "failure") return "Failure Memory";
|
|
26
|
+
return "Memory";
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
export async function triggerConsolidation(
|
|
16
30
|
pi: ExtensionAPI,
|
|
17
31
|
store: MemoryStore,
|
|
18
|
-
target:
|
|
32
|
+
target: MemoryTarget,
|
|
19
33
|
signal?: AbortSignal,
|
|
20
34
|
timeoutMs: number = 60000,
|
|
35
|
+
toolTarget: ToolMemoryTarget = target,
|
|
21
36
|
): Promise<ConsolidationResult> {
|
|
22
|
-
const entries =
|
|
23
|
-
target === "memory" ? store.getMemoryEntries() : store.getUserEntries();
|
|
37
|
+
const entries = entriesForTarget(store, target);
|
|
24
38
|
const currentContent = entries.join(ENTRY_DELIMITER);
|
|
25
39
|
|
|
26
40
|
const prompt = [
|
|
27
41
|
CONSOLIDATION_PROMPT,
|
|
28
42
|
"",
|
|
29
|
-
`--- Current ${target
|
|
43
|
+
`--- Current ${labelForTarget(target, toolTarget)} Entries ---`,
|
|
30
44
|
currentContent || "(empty)",
|
|
31
45
|
"",
|
|
32
|
-
`Use the memory tool to consolidate. Target: '${
|
|
46
|
+
`Use the memory tool to consolidate. Target: '${toolTarget}'`,
|
|
33
47
|
].join("\n");
|
|
34
48
|
|
|
35
49
|
try {
|
|
@@ -60,30 +74,47 @@ export function registerConsolidateCommand(
|
|
|
60
74
|
pi: ExtensionAPI,
|
|
61
75
|
store: MemoryStore,
|
|
62
76
|
timeoutMs: number = 60000,
|
|
77
|
+
projectStore: MemoryStore | null = null,
|
|
78
|
+
projectName?: string | null,
|
|
63
79
|
): void {
|
|
64
80
|
pi.registerCommand("memory-consolidate", {
|
|
65
81
|
description: "Manually trigger memory consolidation to free up space",
|
|
66
82
|
handler: async (_args, ctx) => {
|
|
67
83
|
const results: string[] = [];
|
|
84
|
+
const targets: Array<{
|
|
85
|
+
label: string;
|
|
86
|
+
store: MemoryStore;
|
|
87
|
+
target: MemoryTarget;
|
|
88
|
+
toolTarget: ToolMemoryTarget;
|
|
89
|
+
}> = [
|
|
90
|
+
{ label: "memory", store, target: "memory", toolTarget: "memory" },
|
|
91
|
+
{ label: "user", store, target: "user", toolTarget: "user" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
if (projectStore) {
|
|
95
|
+
targets.push({
|
|
96
|
+
label: projectName ? `project:${projectName}` : "project",
|
|
97
|
+
store: projectStore,
|
|
98
|
+
target: "memory",
|
|
99
|
+
toolTarget: "project",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
68
102
|
|
|
69
|
-
for (const
|
|
70
|
-
const entries =
|
|
71
|
-
target === "memory"
|
|
72
|
-
? store.getMemoryEntries()
|
|
73
|
-
: store.getUserEntries();
|
|
103
|
+
for (const item of targets) {
|
|
104
|
+
const entries = entriesForTarget(item.store, item.target);
|
|
74
105
|
|
|
75
106
|
if (entries.length === 0) {
|
|
76
|
-
results.push(`${
|
|
107
|
+
results.push(`${item.label}: (empty, nothing to consolidate)`);
|
|
77
108
|
continue;
|
|
78
109
|
}
|
|
79
110
|
|
|
80
|
-
const result = await triggerConsolidation(pi, store, target, ctx.signal, timeoutMs);
|
|
111
|
+
const result = await triggerConsolidation(pi, item.store, item.target, ctx.signal, timeoutMs, item.toolTarget);
|
|
81
112
|
|
|
82
113
|
if (result.consolidated) {
|
|
83
|
-
await store.loadFromDisk();
|
|
84
|
-
results.push(`${
|
|
114
|
+
await item.store.loadFromDisk();
|
|
115
|
+
results.push(`${item.label}: ✅ consolidated`);
|
|
85
116
|
} else {
|
|
86
|
-
results.push(`${
|
|
117
|
+
results.push(`${item.label}: ❌ ${result.error}`);
|
|
87
118
|
}
|
|
88
119
|
}
|
|
89
120
|
|
package/src/index.ts
CHANGED
|
@@ -177,11 +177,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
177
177
|
// ── 6. Setup session-end flush ──
|
|
178
178
|
setupSessionFlush(pi, store, projectStore, config);
|
|
179
179
|
|
|
180
|
-
// ── 7. Setup auto-consolidation (inject consolidator into
|
|
180
|
+
// ── 7. Setup auto-consolidation (inject consolidator into stores) ──
|
|
181
181
|
store.setConsolidator(async (target, signal) => {
|
|
182
182
|
return triggerConsolidation(pi, store, target, signal, config.consolidationTimeoutMs);
|
|
183
183
|
});
|
|
184
|
-
|
|
184
|
+
if (projectStore) {
|
|
185
|
+
projectStore.setConsolidator(async (target, signal) => {
|
|
186
|
+
const toolTarget = target === "memory" ? "project" : target;
|
|
187
|
+
return triggerConsolidation(pi, projectStore, target, signal, config.consolidationTimeoutMs, toolTarget);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
registerConsolidateCommand(pi, store, config.consolidationTimeoutMs, projectStore, projectName);
|
|
185
191
|
|
|
186
192
|
// ── 8. Setup correction detection ──
|
|
187
193
|
setupCorrectionDetector(pi, store, projectStore, config, dbManager, projectName);
|
package/src/store/db.ts
CHANGED
|
@@ -106,6 +106,8 @@ export class DatabaseManager {
|
|
|
106
106
|
|
|
107
107
|
// Enable WAL mode + FK enforcement for each connection.
|
|
108
108
|
db.exec('PRAGMA journal_mode = WAL');
|
|
109
|
+
db.exec('PRAGMA wal_autocheckpoint = 100');
|
|
110
|
+
db.exec('PRAGMA journal_size_limit = 5242880');
|
|
109
111
|
db.exec('PRAGMA foreign_keys = ON');
|
|
110
112
|
|
|
111
113
|
// Create tables and triggers
|
|
@@ -251,6 +253,7 @@ export class DatabaseManager {
|
|
|
251
253
|
*/
|
|
252
254
|
close(): void {
|
|
253
255
|
if (this.db) {
|
|
256
|
+
try { this.db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch { /* best effort */ }
|
|
254
257
|
this.db.close();
|
|
255
258
|
this.db = null;
|
|
256
259
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const FTS5_OPERATOR_PATTERN = /\b(OR|AND|NOT|NEAR)\b/;
|
|
2
|
+
const FTS5_TOKEN_PATTERN = /"([^"]*)"|(\S+)/g;
|
|
3
|
+
const NATURAL_LANGUAGE_CONNECTORS = new Set(['and', 'or', 'not', 'near']);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize natural-language search input into an FTS5 query.
|
|
7
|
+
* Plain terms become individually quoted for implicit AND matching.
|
|
8
|
+
* Explicit quoted phrases are preserved, connector stopwords are ignored in
|
|
9
|
+
* natural-language mode, and raw uppercase FTS5 operators pass through.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeFts5Query(query: string): string {
|
|
12
|
+
const trimmed = query.trim();
|
|
13
|
+
if (trimmed.length === 0) return '';
|
|
14
|
+
|
|
15
|
+
if (FTS5_OPERATOR_PATTERN.test(trimmed)) {
|
|
16
|
+
return trimmed;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const normalizedTerms: string[] = [];
|
|
20
|
+
for (const match of trimmed.matchAll(FTS5_TOKEN_PATTERN)) {
|
|
21
|
+
const phrase = match[1];
|
|
22
|
+
const term = match[2];
|
|
23
|
+
if (phrase === undefined && term && NATURAL_LANGUAGE_CONNECTORS.has(term.toLowerCase())) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const rawValue = phrase ?? term ?? '';
|
|
28
|
+
normalizedTerms.push(`"${rawValue.replace(/"/g, '""')}"`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return normalizedTerms.join(' ');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isFts5QueryError(err: unknown): boolean {
|
|
35
|
+
if (!(err instanceof Error)) return false;
|
|
36
|
+
const msg = err.message.toLowerCase();
|
|
37
|
+
return msg.includes('fts5') || msg.includes('unterminated string');
|
|
38
|
+
}
|
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Escape a string for FTS5 query syntax.
|
|
5
|
-
* Wraps the query in double quotes to treat it as a literal phrase.
|
|
6
|
-
*/
|
|
7
|
-
function escapeFts5Query(query: string): string {
|
|
8
|
-
// If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
|
|
9
|
-
if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
|
|
10
|
-
return query;
|
|
11
|
-
}
|
|
12
|
-
// Otherwise, wrap in double quotes to treat as literal phrase
|
|
13
|
-
return `"${query.replace(/"/g, '""')}"`;
|
|
14
|
-
}
|
|
2
|
+
import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
15
3
|
|
|
16
4
|
/**
|
|
17
5
|
* Search result from session history.
|
|
@@ -52,6 +40,10 @@ export function searchSessions(
|
|
|
52
40
|
query: string,
|
|
53
41
|
options: SessionSearchOptions = {}
|
|
54
42
|
): SessionSearchResult[] {
|
|
43
|
+
if (query.trim().length === 0) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
55
47
|
const db = dbManager.getDb();
|
|
56
48
|
const { limit = 10, project, role, since } = options;
|
|
57
49
|
|
|
@@ -60,8 +52,12 @@ export function searchSessions(
|
|
|
60
52
|
const params: unknown[] = [];
|
|
61
53
|
|
|
62
54
|
// FTS5 match condition — use subquery for reliable rowid matching
|
|
55
|
+
const normalizedQuery = normalizeFts5Query(query);
|
|
56
|
+
if (normalizedQuery.length === 0) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
63
59
|
conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
|
|
64
|
-
params.push(
|
|
60
|
+
params.push(normalizedQuery);
|
|
65
61
|
|
|
66
62
|
// Project filter
|
|
67
63
|
if (project) {
|
|
@@ -119,8 +115,10 @@ export function searchSessions(
|
|
|
119
115
|
snippet: row.snippet,
|
|
120
116
|
}));
|
|
121
117
|
} catch (err) {
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
if (isFts5QueryError(err)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
+
import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
2
3
|
import type { MemoryCategory } from '../types.js';
|
|
3
4
|
|
|
4
5
|
const MEMORY_SELECT_COLUMNS = `
|
|
@@ -550,19 +551,6 @@ export function removeExactSyncedMemories(
|
|
|
550
551
|
};
|
|
551
552
|
}
|
|
552
553
|
|
|
553
|
-
/**
|
|
554
|
-
* Escape a string for FTS5 query syntax.
|
|
555
|
-
* Wraps the query in double quotes to treat it as a literal phrase.
|
|
556
|
-
*/
|
|
557
|
-
function escapeFts5Query(query: string): string {
|
|
558
|
-
// If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
|
|
559
|
-
if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
|
|
560
|
-
return query;
|
|
561
|
-
}
|
|
562
|
-
// Otherwise, wrap in double quotes to treat as literal phrase
|
|
563
|
-
return `"${query.replace(/"/g, '""')}"`;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
554
|
/**
|
|
567
555
|
* Search memories using FTS5.
|
|
568
556
|
*/
|
|
@@ -571,6 +559,10 @@ export function searchMemories(
|
|
|
571
559
|
query: string,
|
|
572
560
|
options: { project?: string; target?: string; category?: MemoryCategory; limit?: number } = {}
|
|
573
561
|
): SqliteMemoryEntry[] {
|
|
562
|
+
if (query.trim().length === 0) {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
|
|
574
566
|
const db = dbManager.getDb();
|
|
575
567
|
const { project, target, category, limit = 10 } = options;
|
|
576
568
|
|
|
@@ -578,8 +570,12 @@ export function searchMemories(
|
|
|
578
570
|
const params: unknown[] = [];
|
|
579
571
|
|
|
580
572
|
// FTS5 match via subquery with escaped query
|
|
573
|
+
const normalizedQuery = normalizeFts5Query(query);
|
|
574
|
+
if (normalizedQuery.length === 0) {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
581
577
|
conditions.push('m.id IN (SELECT rowid FROM memory_fts WHERE memory_fts MATCH ?)');
|
|
582
|
-
params.push(
|
|
578
|
+
params.push(normalizedQuery);
|
|
583
579
|
|
|
584
580
|
if (project !== undefined) {
|
|
585
581
|
if (project === null) {
|
|
@@ -611,20 +607,27 @@ export function searchMemories(
|
|
|
611
607
|
`;
|
|
612
608
|
params.push(limit);
|
|
613
609
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
610
|
+
try {
|
|
611
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
612
|
+
id: number;
|
|
613
|
+
project: string | null;
|
|
614
|
+
target: string;
|
|
615
|
+
category: string | null;
|
|
616
|
+
content: string;
|
|
617
|
+
failure_reason: string | null;
|
|
618
|
+
tool_state: string | null;
|
|
619
|
+
corrected_to: string | null;
|
|
620
|
+
created: string;
|
|
621
|
+
last_referenced: string;
|
|
622
|
+
}>;
|
|
623
|
+
|
|
624
|
+
return rows.map(mapRow);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
if (isFts5QueryError(err)) {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
throw err;
|
|
630
|
+
}
|
|
628
631
|
}
|
|
629
632
|
|
|
630
633
|
/**
|