@pi-unipi/memory 0.1.13 → 2.0.0
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 +39 -40
- package/embedding.ts +7 -7
- package/index.ts +55 -22
- package/package.json +1 -1
- package/storage.ts +55 -30
- package/tools.ts +15 -5
package/README.md
CHANGED
|
@@ -1,37 +1,8 @@
|
|
|
1
1
|
# @pi-unipi/memory
|
|
2
2
|
|
|
3
|
-
Persistent
|
|
3
|
+
Persistent memory that survives across sessions. Stores facts, preferences, and decisions in SQLite with vector search, so the agent remembers what you told it last week.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- **Two-tier storage:** SQLite + sqlite-vec for vector search, markdown files for human-readable memory
|
|
8
|
-
- **Project-scoped + global memory:** Each project gets its own DB, global memories accessible cross-project
|
|
9
|
-
- **Hybrid search:** Vector similarity + fuzzy text matching for best recall
|
|
10
|
-
- **Session injection:** Agent sees memory titles at session start
|
|
11
|
-
- **Auto-consolidation:** Memories extracted during compaction
|
|
12
|
-
- **Update-first:** Prevents memory duplication
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
# All-in-one (includes memory)
|
|
18
|
-
pi install npm:unipi
|
|
19
|
-
|
|
20
|
-
# Standalone
|
|
21
|
-
pi install npm:@pi-unipi/memory
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Tools
|
|
25
|
-
|
|
26
|
-
| Tool | Description |
|
|
27
|
-
|------|-------------|
|
|
28
|
-
| `memory_store` | Store/update memory (project scope) |
|
|
29
|
-
| `memory_search` | Search memories (project scope) |
|
|
30
|
-
| `memory_delete` | Delete memory by ID or title |
|
|
31
|
-
| `memory_list` | List all project memories |
|
|
32
|
-
| `global_memory_store` | Store/update memory (global scope) |
|
|
33
|
-
| `global_memory_search` | Search global memories |
|
|
34
|
-
| `global_memory_list` | List all global memories |
|
|
5
|
+
Two storage tiers: SQLite + sqlite-vec for vector similarity search, markdown files for human-readable memories you can edit by hand. Project-scoped memories stay separate per codebase, global memories are accessible everywhere.
|
|
35
6
|
|
|
36
7
|
## Commands
|
|
37
8
|
|
|
@@ -45,9 +16,31 @@ pi install npm:@pi-unipi/memory
|
|
|
45
16
|
| `/unipi:global-memory-search <term>` | Search global memories |
|
|
46
17
|
| `/unipi:global-memory-list` | List all global memories |
|
|
47
18
|
|
|
48
|
-
##
|
|
19
|
+
## Special Triggers
|
|
20
|
+
|
|
21
|
+
At session start, the agent sees memory titles injected into context. This gives it a summary of what it should remember without loading full memory content.
|
|
22
|
+
|
|
23
|
+
During compaction (if `@pi-unipi/compactor` is installed), memories are auto-extracted from the conversation. The `memory-consolidate` command also triggers this manually.
|
|
49
24
|
|
|
50
|
-
|
|
25
|
+
Memory registers with the info-screen dashboard, showing project memory count, total count, and consolidation count. The footer subscribes to `MEMORY_STORED`, `MEMORY_DELETED`, and `MEMORYCONSOLIDATED` events to display memory stats.
|
|
26
|
+
|
|
27
|
+
## Agent Tools
|
|
28
|
+
|
|
29
|
+
| Tool | Scope | Description |
|
|
30
|
+
|------|-------|-------------|
|
|
31
|
+
| `memory_store` | Project | Store or update a memory |
|
|
32
|
+
| `memory_search` | Project | Search memories by query |
|
|
33
|
+
| `memory_delete` | Project | Delete memory by ID or title |
|
|
34
|
+
| `memory_list` | Project | List all project memories |
|
|
35
|
+
| `global_memory_store` | Global | Store or update global memory |
|
|
36
|
+
| `global_memory_search` | Global | Search global memories |
|
|
37
|
+
| `global_memory_list` | Global | List all global memories |
|
|
38
|
+
|
|
39
|
+
The agent uses `memory_store` when it learns something worth remembering — a user preference, a technical decision, a code pattern. `memory_search` is used to recall relevant context before answering questions.
|
|
40
|
+
|
|
41
|
+
## Memory Format
|
|
42
|
+
|
|
43
|
+
Memories are markdown files with YAML frontmatter:
|
|
51
44
|
|
|
52
45
|
```markdown
|
|
53
46
|
---
|
|
@@ -65,16 +58,18 @@ User prefers short-lived access tokens (15min) with long-lived refresh tokens (3
|
|
|
65
58
|
Always implement token rotation on refresh.
|
|
66
59
|
```
|
|
67
60
|
|
|
68
|
-
|
|
61
|
+
### Naming Convention
|
|
69
62
|
|
|
70
|
-
|
|
63
|
+
Format: `<most_important>_<less_important>_<lesser>`
|
|
71
64
|
|
|
72
65
|
Examples:
|
|
73
66
|
- `auth_jwt_prefer_refresh_tokens`
|
|
74
67
|
- `db_postgres_use_connection_pooling`
|
|
75
68
|
- `style_typescript_strict_mode_always`
|
|
76
69
|
|
|
77
|
-
##
|
|
70
|
+
## Configurables
|
|
71
|
+
|
|
72
|
+
Memory has no configuration file. Storage paths are fixed:
|
|
78
73
|
|
|
79
74
|
```
|
|
80
75
|
~/.unipi/memory/
|
|
@@ -88,7 +83,11 @@ Examples:
|
|
|
88
83
|
|
|
89
84
|
## Dependencies
|
|
90
85
|
|
|
91
|
-
- `better-sqlite3`
|
|
92
|
-
- `sqlite-vec`
|
|
93
|
-
- `js-yaml`
|
|
94
|
-
- `@pi-unipi/core`
|
|
86
|
+
- `better-sqlite3` — SQLite database
|
|
87
|
+
- `sqlite-vec` — Vector search extension
|
|
88
|
+
- `js-yaml` — YAML frontmatter parsing
|
|
89
|
+
- `@pi-unipi/core` — Shared utilities
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
package/embedding.ts
CHANGED
|
@@ -55,7 +55,7 @@ export async function generateEmbedding(
|
|
|
55
55
|
try {
|
|
56
56
|
const truncated = text.slice(0, 8000); // OpenRouter/OpenAI limit ~8192 tokens
|
|
57
57
|
|
|
58
|
-
const body:
|
|
58
|
+
const body: Record<string, unknown> = {
|
|
59
59
|
model: config.model,
|
|
60
60
|
input: truncated,
|
|
61
61
|
};
|
|
@@ -99,8 +99,8 @@ export async function generateEmbedding(
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
return vec;
|
|
102
|
-
} catch (err:
|
|
103
|
-
if (err
|
|
102
|
+
} catch (err: unknown) {
|
|
103
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
104
104
|
// Removed console.warn — timeout causes fallback to fuzzy search.
|
|
105
105
|
} else {
|
|
106
106
|
// Removed console.warn — embedding error causes fallback to fuzzy search.
|
|
@@ -128,7 +128,7 @@ export async function generateEmbeddingsBatch(
|
|
|
128
128
|
try {
|
|
129
129
|
const truncated = texts.map((t) => t.slice(0, 8000));
|
|
130
130
|
|
|
131
|
-
const body:
|
|
131
|
+
const body: Record<string, unknown> = {
|
|
132
132
|
model: config.model,
|
|
133
133
|
input: truncated,
|
|
134
134
|
};
|
|
@@ -153,10 +153,10 @@ export async function generateEmbeddingsBatch(
|
|
|
153
153
|
return texts.map(() => null);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
const data = await response.json() as
|
|
156
|
+
const data = await response.json() as { data?: Array<{ embedding?: number[] }> };
|
|
157
157
|
const dims = config.dimensions;
|
|
158
158
|
|
|
159
|
-
return (data?.data || []).map((item
|
|
159
|
+
return (data?.data || []).map((item) => {
|
|
160
160
|
if (!Array.isArray(item.embedding)) return null;
|
|
161
161
|
const vec = new Float32Array(dims);
|
|
162
162
|
for (let i = 0; i < Math.min(item.embedding.length, dims); i++) {
|
|
@@ -233,7 +233,7 @@ export function bufferToVector(buf: Buffer): Float32Array {
|
|
|
233
233
|
/**
|
|
234
234
|
* Check if embeddings are available (sqlite-vec loaded).
|
|
235
235
|
*/
|
|
236
|
-
export function hasEmbeddings(db:
|
|
236
|
+
export function hasEmbeddings(db: { prepare(sql: string): { get(...args: unknown[]): unknown } }): boolean {
|
|
237
237
|
try {
|
|
238
238
|
db.prepare("SELECT * FROM memories_vec LIMIT 1").get();
|
|
239
239
|
return true;
|
package/index.ts
CHANGED
|
@@ -17,8 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
// Get info registry from global (avoids direct import issues with pi's extension loading)
|
|
19
19
|
function getInfoRegistry() {
|
|
20
|
-
|
|
21
|
-
return g.__unipi_info_registry;
|
|
20
|
+
return globalThis.__unipi_info_registry;
|
|
22
21
|
}
|
|
23
22
|
import {
|
|
24
23
|
MemoryStorage,
|
|
@@ -61,7 +60,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
60
|
});
|
|
62
61
|
|
|
63
62
|
// Register tools and commands
|
|
64
|
-
registerMemoryTools(pi, getStorage,
|
|
63
|
+
registerMemoryTools(pi, getStorage, {
|
|
64
|
+
onRecall: () => { recallDone = true; },
|
|
65
|
+
onStore: () => { storeDone = true; },
|
|
66
|
+
});
|
|
65
67
|
registerMemoryCommands(pi, getStorage);
|
|
66
68
|
|
|
67
69
|
// Session lifecycle
|
|
@@ -186,10 +188,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
186
188
|
});
|
|
187
189
|
|
|
188
190
|
// Inject memory recall reminder at agent start (hidden message, not system prompt)
|
|
189
|
-
pi.on("before_agent_start", async (
|
|
191
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
190
192
|
if (recallDone) return;
|
|
191
193
|
if (!projectStorage) return;
|
|
192
194
|
|
|
195
|
+
// Workflow sandboxes and user presets can change the active tool set. Only
|
|
196
|
+
// instruct the agent to use memory tools that are actually callable now.
|
|
197
|
+
const activeTools = new Set(pi.getActiveTools());
|
|
198
|
+
const canSearch = activeTools.has(MEMORY_TOOLS.SEARCH) || activeTools.has(GLOBAL_SEARCH_ALIAS);
|
|
199
|
+
const canStore = activeTools.has(MEMORY_TOOLS.STORE);
|
|
200
|
+
|
|
201
|
+
if (!canSearch && !canStore) {
|
|
202
|
+
recallDone = true;
|
|
203
|
+
storeDone = true;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
193
207
|
const projectName = getProjectName(ctx.cwd);
|
|
194
208
|
let projectMemories: Array<{ id: string; title: string; type: string }> = [];
|
|
195
209
|
try {
|
|
@@ -199,31 +213,49 @@ export default function (pi: ExtensionAPI) {
|
|
|
199
213
|
return;
|
|
200
214
|
}
|
|
201
215
|
|
|
202
|
-
if (projectMemories.length === 0) {
|
|
203
|
-
recallDone = true; // Nothing to recall
|
|
216
|
+
if (projectMemories.length === 0 && !canStore) {
|
|
217
|
+
recallDone = true; // Nothing to recall and no store tool available
|
|
204
218
|
return;
|
|
205
219
|
}
|
|
206
220
|
|
|
207
|
-
const
|
|
208
|
-
|
|
221
|
+
const lines = [
|
|
222
|
+
"## 🧠 Memory System Active",
|
|
223
|
+
"",
|
|
224
|
+
`You have ${projectMemories.length} memories stored for project "${projectName}".`,
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
if (canSearch && projectMemories.length > 0) {
|
|
228
|
+
const titleList = projectMemories.slice(0, 20).map(m => `- ${m.title}`).join("\n");
|
|
229
|
+
const extra = projectMemories.length > 20 ? `\n... and ${projectMemories.length - 20} more` : "";
|
|
230
|
+
lines.push(
|
|
231
|
+
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
232
|
+
"",
|
|
233
|
+
"Available memories:",
|
|
234
|
+
titleList + extra,
|
|
235
|
+
);
|
|
236
|
+
} else {
|
|
237
|
+
recallDone = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (canStore) {
|
|
241
|
+
lines.push(
|
|
242
|
+
"",
|
|
243
|
+
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
244
|
+
"call `memory_store` to save it for future sessions.",
|
|
245
|
+
);
|
|
246
|
+
} else {
|
|
247
|
+
storeDone = true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
lines.push(
|
|
251
|
+
"",
|
|
252
|
+
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
253
|
+
);
|
|
209
254
|
|
|
210
255
|
return {
|
|
211
256
|
message: {
|
|
212
257
|
customType: "unipi-memory-recall-reminder",
|
|
213
|
-
content:
|
|
214
|
-
"## 🧠 Memory System Active",
|
|
215
|
-
"",
|
|
216
|
-
`You have ${projectMemories.length} memories stored for project "${projectName}".`,
|
|
217
|
-
"**BEFORE starting work**, call `memory_search` with relevant keywords to check for existing context.",
|
|
218
|
-
"",
|
|
219
|
-
"Available memories:",
|
|
220
|
-
titleList + extra,
|
|
221
|
-
"",
|
|
222
|
-
"**AFTER completing the task**, if you learned something non-obvious,",
|
|
223
|
-
"call `memory_store` to save it for future sessions.",
|
|
224
|
-
"",
|
|
225
|
-
"Guardrails: read max 10 memory results per search. Update existing memories instead of creating duplicates.",
|
|
226
|
-
].join("\n"),
|
|
258
|
+
content: lines.join("\n"),
|
|
227
259
|
display: false,
|
|
228
260
|
},
|
|
229
261
|
};
|
|
@@ -232,6 +264,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
232
264
|
// After each agent response, remind LLM to save if it hasn't yet
|
|
233
265
|
pi.on("agent_end", async (_event, _ctx) => {
|
|
234
266
|
if (storeDone || !recallDone) return;
|
|
267
|
+
if (!pi.getActiveTools().includes(MEMORY_TOOLS.STORE)) return;
|
|
235
268
|
|
|
236
269
|
pi.sendMessage(
|
|
237
270
|
{
|
package/package.json
CHANGED
package/storage.ts
CHANGED
|
@@ -13,6 +13,29 @@ import * as path from "node:path";
|
|
|
13
13
|
import * as os from "node:os";
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
|
|
16
|
+
/** Memory row from SQLite queries */
|
|
17
|
+
interface MemoryRow {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
content?: string;
|
|
21
|
+
type?: string;
|
|
22
|
+
project?: string;
|
|
23
|
+
tags?: string;
|
|
24
|
+
created?: string;
|
|
25
|
+
updated?: string;
|
|
26
|
+
embedding?: { buffer: ArrayBuffer };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Search result row from vector queries */
|
|
30
|
+
interface SearchResultRow {
|
|
31
|
+
id: string;
|
|
32
|
+
title: string;
|
|
33
|
+
distance: number;
|
|
34
|
+
rowid?: number;
|
|
35
|
+
title_match?: number;
|
|
36
|
+
content_match?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
/** Memory record interface */
|
|
17
40
|
export interface MemoryRecord {
|
|
18
41
|
id: string;
|
|
@@ -208,12 +231,14 @@ export class MemoryStorage {
|
|
|
208
231
|
try {
|
|
209
232
|
this.initDb(dbPath);
|
|
210
233
|
return; // Success
|
|
211
|
-
} catch (err:
|
|
234
|
+
} catch (err: unknown) {
|
|
235
|
+
const errMsg = err instanceof Error ? err.message : "";
|
|
236
|
+
const errCode = (err instanceof Error && 'code' in err) ? (err as NodeJS.ErrnoException).code : undefined;
|
|
212
237
|
const isTransient =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
238
|
+
errMsg.includes("disk I/O error") ||
|
|
239
|
+
errCode === "SQLITE_IOERR" ||
|
|
240
|
+
errCode === "SQLITE_BUSY" ||
|
|
241
|
+
errMsg.includes("database is locked");
|
|
217
242
|
|
|
218
243
|
this.close();
|
|
219
244
|
|
|
@@ -433,7 +458,7 @@ export class MemoryStorage {
|
|
|
433
458
|
|
|
434
459
|
// Get existing IDs from DB
|
|
435
460
|
const existingIds = new Set(
|
|
436
|
-
(this.db.prepare("SELECT id FROM memories").all() as
|
|
461
|
+
(this.db.prepare("SELECT id FROM memories").all() as MemoryRow[])
|
|
437
462
|
.map(r => r.id)
|
|
438
463
|
);
|
|
439
464
|
|
|
@@ -494,7 +519,7 @@ export class MemoryStorage {
|
|
|
494
519
|
findSimilarByTitle(title: string, threshold = 0.6): Array<{ record: MemoryRecord; similarity: number }> {
|
|
495
520
|
if (!this.db) throw new Error("Storage not initialized");
|
|
496
521
|
|
|
497
|
-
const allRows = this.db.prepare("SELECT id, title FROM memories").all() as
|
|
522
|
+
const allRows = this.db.prepare("SELECT id, title FROM memories").all() as MemoryRow[];
|
|
498
523
|
const results: Array<{ record: MemoryRecord; similarity: number }> = [];
|
|
499
524
|
|
|
500
525
|
const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9]+/g, " ");
|
|
@@ -526,18 +551,18 @@ export class MemoryStorage {
|
|
|
526
551
|
getById(id: string): MemoryRecord | null {
|
|
527
552
|
if (!this.db) throw new Error("Storage not initialized");
|
|
528
553
|
|
|
529
|
-
const row = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id) as
|
|
554
|
+
const row = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id) as MemoryRow | undefined;
|
|
530
555
|
if (!row) return null;
|
|
531
556
|
|
|
532
557
|
return {
|
|
533
558
|
id: row.id,
|
|
534
559
|
title: row.title,
|
|
535
|
-
content: row.content,
|
|
560
|
+
content: row.content ?? "",
|
|
536
561
|
tags: JSON.parse(row.tags || "[]"),
|
|
537
|
-
project: row.project,
|
|
538
|
-
type: row.type,
|
|
539
|
-
created: row.created,
|
|
540
|
-
updated: row.updated,
|
|
562
|
+
project: row.project ?? "",
|
|
563
|
+
type: (row.type ?? "summary") as MemoryRecord["type"],
|
|
564
|
+
created: row.created ?? "",
|
|
565
|
+
updated: row.updated ?? "",
|
|
541
566
|
embedding: row.embedding ? new Float32Array(row.embedding.buffer) : null,
|
|
542
567
|
};
|
|
543
568
|
}
|
|
@@ -549,34 +574,34 @@ export class MemoryStorage {
|
|
|
549
574
|
if (!this.db) throw new Error("Storage not initialized");
|
|
550
575
|
|
|
551
576
|
// Try exact match first
|
|
552
|
-
const exact = this.db.prepare("SELECT * FROM memories WHERE title = ?").get(title) as
|
|
577
|
+
const exact = this.db.prepare("SELECT * FROM memories WHERE title = ?").get(title) as MemoryRow | undefined;
|
|
553
578
|
if (exact) {
|
|
554
579
|
return {
|
|
555
580
|
id: exact.id,
|
|
556
581
|
title: exact.title,
|
|
557
|
-
content: exact.content,
|
|
582
|
+
content: exact.content ?? "",
|
|
558
583
|
tags: JSON.parse(exact.tags || "[]"),
|
|
559
|
-
project: exact.project,
|
|
560
|
-
type: exact.type,
|
|
561
|
-
created: exact.created,
|
|
562
|
-
updated: exact.updated,
|
|
584
|
+
project: exact.project ?? "",
|
|
585
|
+
type: (exact.type ?? "summary") as MemoryRecord["type"],
|
|
586
|
+
created: exact.created ?? "",
|
|
587
|
+
updated: exact.updated ?? "",
|
|
563
588
|
embedding: exact.embedding ? new Float32Array(exact.embedding.buffer) : null,
|
|
564
589
|
};
|
|
565
590
|
}
|
|
566
591
|
|
|
567
592
|
// Try case-insensitive match
|
|
568
|
-
const row = this.db.prepare("SELECT * FROM memories WHERE LOWER(title) = LOWER(?)").get(title) as
|
|
593
|
+
const row = this.db.prepare("SELECT * FROM memories WHERE LOWER(title) = LOWER(?)").get(title) as MemoryRow | undefined;
|
|
569
594
|
if (!row) return null;
|
|
570
595
|
|
|
571
596
|
return {
|
|
572
597
|
id: row.id,
|
|
573
598
|
title: row.title,
|
|
574
|
-
content: row.content,
|
|
599
|
+
content: row.content ?? "",
|
|
575
600
|
tags: JSON.parse(row.tags || "[]"),
|
|
576
|
-
project: row.project,
|
|
577
|
-
type: row.type,
|
|
578
|
-
created: row.created,
|
|
579
|
-
updated: row.updated,
|
|
601
|
+
project: row.project ?? "",
|
|
602
|
+
type: (row.type ?? "summary") as MemoryRecord["type"],
|
|
603
|
+
created: row.created ?? "",
|
|
604
|
+
updated: row.updated ?? "",
|
|
580
605
|
embedding: row.embedding ? new Float32Array(row.embedding.buffer) : null,
|
|
581
606
|
};
|
|
582
607
|
}
|
|
@@ -587,8 +612,8 @@ export class MemoryStorage {
|
|
|
587
612
|
listAll(): Array<{ id: string; title: string; type: string }> {
|
|
588
613
|
if (!this.db) throw new Error("Storage not initialized");
|
|
589
614
|
|
|
590
|
-
const rows = this.db.prepare("SELECT id, title, type FROM memories ORDER BY updated DESC").all() as
|
|
591
|
-
return rows.map((r) => ({ id: r.id, title: r.title, type: r.type }));
|
|
615
|
+
const rows = this.db.prepare("SELECT id, title, type FROM memories ORDER BY updated DESC").all() as MemoryRow[];
|
|
616
|
+
return rows.map((r) => ({ id: r.id, title: r.title, type: r.type ?? "" }));
|
|
592
617
|
}
|
|
593
618
|
|
|
594
619
|
/**
|
|
@@ -647,7 +672,7 @@ export class MemoryStorage {
|
|
|
647
672
|
ORDER BY distance
|
|
648
673
|
LIMIT ?`
|
|
649
674
|
)
|
|
650
|
-
.all(Buffer.from(embedding.buffer), limit * 2) as
|
|
675
|
+
.all(Buffer.from(embedding.buffer), limit * 2) as SearchResultRow[];
|
|
651
676
|
|
|
652
677
|
for (const vr of vecResults) {
|
|
653
678
|
const memoryId = this.rowidToId(Number(vr.rowid));
|
|
@@ -685,11 +710,11 @@ export class MemoryStorage {
|
|
|
685
710
|
`%${query}%`,
|
|
686
711
|
...queryWords.flatMap(w => [`%${w}%`, `%${w}%`]),
|
|
687
712
|
limit * 2
|
|
688
|
-
) as
|
|
713
|
+
) as SearchResultRow[];
|
|
689
714
|
|
|
690
715
|
for (const fr of fuzzyResults) {
|
|
691
716
|
const existing = results.get(fr.id);
|
|
692
|
-
const fuzzyScore = (fr.title_match * 0.7 + fr.content_match * 0.3);
|
|
717
|
+
const fuzzyScore = ((fr.title_match ?? 0) * 0.7 + (fr.content_match ?? 0) * 0.3);
|
|
693
718
|
const record = this.getById(fr.id);
|
|
694
719
|
if (record) {
|
|
695
720
|
const snippet = this.extractSnippet(record.content, query);
|
package/tools.ts
CHANGED
|
@@ -29,14 +29,21 @@ export const MEMORY_TOOLS = {
|
|
|
29
29
|
// Keep old name as alias for backward compat
|
|
30
30
|
export const GLOBAL_SEARCH_ALIAS = "global_memory_search";
|
|
31
31
|
|
|
32
|
+
export interface MemoryToolActivityCallbacks {
|
|
33
|
+
/** Called when a recall-style tool is used (search/list). */
|
|
34
|
+
onRecall?: () => void;
|
|
35
|
+
/** Called when memory state is changed (store/delete). */
|
|
36
|
+
onStore?: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
/**
|
|
33
40
|
* Register memory tools.
|
|
34
|
-
* @param
|
|
41
|
+
* @param activity - callbacks for lifecycle reminders
|
|
35
42
|
*/
|
|
36
43
|
export function registerMemoryTools(
|
|
37
44
|
pi: ExtensionAPI,
|
|
38
45
|
getStorage: () => MemoryStorage,
|
|
39
|
-
|
|
46
|
+
activity?: MemoryToolActivityCallbacks
|
|
40
47
|
): void {
|
|
41
48
|
// --- memory_store tool ---
|
|
42
49
|
pi.registerTool({
|
|
@@ -69,8 +76,8 @@ export function registerMemoryTools(
|
|
|
69
76
|
),
|
|
70
77
|
}),
|
|
71
78
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
79
|
+
activity?.onStore?.(); // Mark store as done for lifecycle
|
|
72
80
|
const storage = getStorage();
|
|
73
|
-
onActivity?.(); // Mark store as done for lifecycle
|
|
74
81
|
|
|
75
82
|
// Step 1: Check for exact duplicate
|
|
76
83
|
const existing = storage.getByTitle(params.title);
|
|
@@ -224,7 +231,7 @@ export function registerMemoryTools(
|
|
|
224
231
|
),
|
|
225
232
|
}),
|
|
226
233
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
227
|
-
|
|
234
|
+
activity?.onRecall?.(); // Mark recall as done for lifecycle
|
|
228
235
|
const limit = params.limit || 10;
|
|
229
236
|
const scope = (params as any).scope || "all";
|
|
230
237
|
|
|
@@ -285,7 +292,7 @@ export function registerMemoryTools(
|
|
|
285
292
|
),
|
|
286
293
|
}),
|
|
287
294
|
async execute(_toolCallId, params, _signal, _onUpdate) {
|
|
288
|
-
|
|
295
|
+
activity?.onRecall?.();
|
|
289
296
|
const results = searchAllProjects(params.query, params.limit || 10);
|
|
290
297
|
|
|
291
298
|
if (results.length === 0) {
|
|
@@ -317,6 +324,7 @@ export function registerMemoryTools(
|
|
|
317
324
|
id: Type.Optional(Type.String({ description: "Memory ID to delete" })),
|
|
318
325
|
}),
|
|
319
326
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
327
|
+
activity?.onStore?.();
|
|
320
328
|
const storage = getStorage();
|
|
321
329
|
|
|
322
330
|
let deleted = false;
|
|
@@ -348,6 +356,7 @@ export function registerMemoryTools(
|
|
|
348
356
|
promptSnippet: "List all project memories.",
|
|
349
357
|
parameters: Type.Object({}),
|
|
350
358
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
359
|
+
activity?.onRecall?.();
|
|
351
360
|
const storage = getStorage();
|
|
352
361
|
const memories = storage.listAll();
|
|
353
362
|
|
|
@@ -384,6 +393,7 @@ export function registerMemoryTools(
|
|
|
384
393
|
promptSnippet: "List all memories across projects.",
|
|
385
394
|
parameters: Type.Object({}),
|
|
386
395
|
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
396
|
+
activity?.onRecall?.();
|
|
387
397
|
const memories = listAllProjects();
|
|
388
398
|
|
|
389
399
|
if (memories.length === 0) {
|