@j0hanz/memdb 1.2.4 → 1.2.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 +7 -2
- package/dist/core/db.d.ts +5 -3
- package/dist/core/db.js +81 -34
- package/dist/core/memory-read.js +31 -19
- package/dist/core/memory-write.js +54 -27
- package/dist/core/relationships.js +5 -16
- package/dist/core/search.js +60 -54
- package/dist/index.js +71 -30
- package/dist/instructions.md +54 -82
- package/dist/protocol-version-guard.d.ts +3 -0
- package/dist/protocol-version-guard.js +32 -21
- package/dist/stdio-transport.d.ts +3 -0
- package/dist/stdio-transport.js +46 -34
- package/dist/tools.js +129 -118
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ Search memories by content and tags.
|
|
|
161
161
|
| :-------- | :----- | :------- | :------ | :---------------------------------------- |
|
|
162
162
|
| `query` | string | Yes | - | Search query (1-1000 chars, max 50 terms) |
|
|
163
163
|
|
|
164
|
-
**Returns:** Array of search results (`Memory` + `relevance`).
|
|
164
|
+
**Returns:** Array of search results (`Memory` + `relevance`, includes `tags`).
|
|
165
165
|
|
|
166
166
|
Notes:
|
|
167
167
|
|
|
@@ -177,7 +177,11 @@ Retrieve a specific memory by its hash.
|
|
|
177
177
|
| :-------- | :----- | :------- | :------ | :------------------ |
|
|
178
178
|
| `hash` | string | Yes | - | MD5 hash (32 chars) |
|
|
179
179
|
|
|
180
|
-
**Returns:** `Memory
|
|
180
|
+
**Returns:** `Memory` (includes `tags`).
|
|
181
|
+
|
|
182
|
+
Notes:
|
|
183
|
+
|
|
184
|
+
- Updates `accessed_at` on read.
|
|
181
185
|
|
|
182
186
|
### `delete_memory`
|
|
183
187
|
|
|
@@ -242,6 +246,7 @@ All memory-shaped responses include:
|
|
|
242
246
|
- `id`: integer ID
|
|
243
247
|
- `content`: original content string
|
|
244
248
|
- `summary`: optional summary (currently unset by tools)
|
|
249
|
+
- `tags`: string array
|
|
245
250
|
- `created_at`: timestamp string
|
|
246
251
|
- `accessed_at`: timestamp string
|
|
247
252
|
- `hash`: MD5 hash
|
package/dist/core/db.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatabaseSync, type StatementSync } from 'node:sqlite';
|
|
2
|
-
import { type Memory, type SearchResult } from '../types.js';
|
|
2
|
+
import { type Memory, type Relationship, type SearchResult } from '../types.js';
|
|
3
3
|
export type DbRow = Record<string, unknown>;
|
|
4
4
|
export declare const db: DatabaseSync;
|
|
5
5
|
export declare const closeDb: () => void;
|
|
@@ -12,5 +12,7 @@ export declare const executeRun: (stmt: StatementSync, ...params: SqlParam[]) =>
|
|
|
12
12
|
};
|
|
13
13
|
export declare const withImmediateTransaction: <T>(operation: () => T) => T;
|
|
14
14
|
export declare const toSafeInteger: (value: unknown, field: string) => number;
|
|
15
|
-
export declare const mapRowToMemory: (row: DbRow) => Memory;
|
|
16
|
-
export declare const mapRowToSearchResult: (row: DbRow) => SearchResult;
|
|
15
|
+
export declare const mapRowToMemory: (row: DbRow, tags?: string[]) => Memory;
|
|
16
|
+
export declare const mapRowToSearchResult: (row: DbRow, tags?: string[]) => SearchResult;
|
|
17
|
+
export declare const mapRowToRelationship: (row: DbRow) => Relationship;
|
|
18
|
+
export declare const loadTagsForMemoryIds: (memoryIds: readonly number[]) => Map<number, string[]>;
|
package/dist/core/db.js
CHANGED
|
@@ -67,23 +67,18 @@ const FTS_SYNC_SQL = `
|
|
|
67
67
|
WHERE id NOT IN (SELECT rowid FROM memories_fts);
|
|
68
68
|
`;
|
|
69
69
|
const withTimeout = async (promise, ms, message) => {
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
let timeout;
|
|
71
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
72
|
+
timeout = setTimeout(() => {
|
|
73
|
+
reject(new Error(message));
|
|
74
|
+
}, ms);
|
|
75
|
+
});
|
|
74
76
|
try {
|
|
75
|
-
|
|
76
|
-
promise,
|
|
77
|
-
new Promise((_, reject) => {
|
|
78
|
-
controller.signal.addEventListener('abort', () => {
|
|
79
|
-
reject(new Error(message));
|
|
80
|
-
});
|
|
81
|
-
}),
|
|
82
|
-
]);
|
|
83
|
-
return result;
|
|
77
|
+
return await Promise.race([promise, timeoutPromise]);
|
|
84
78
|
}
|
|
85
79
|
finally {
|
|
86
|
-
|
|
80
|
+
if (timeout)
|
|
81
|
+
clearTimeout(timeout);
|
|
87
82
|
}
|
|
88
83
|
};
|
|
89
84
|
const ensureDbDirectory = async (dbPath) => {
|
|
@@ -146,26 +141,22 @@ const enforceStatementCacheLimit = () => {
|
|
|
146
141
|
const isDbRow = (value) => {
|
|
147
142
|
return typeof value === 'object' && value !== null;
|
|
148
143
|
};
|
|
144
|
+
const assertDbRow = (value) => {
|
|
145
|
+
if (!isDbRow(value)) {
|
|
146
|
+
throw new Error('Invalid row');
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
};
|
|
149
150
|
const toDbRowArray = (value) => {
|
|
150
151
|
if (!Array.isArray(value)) {
|
|
151
152
|
throw new Error('Expected rows array');
|
|
152
153
|
}
|
|
153
|
-
|
|
154
|
-
for (const row of value) {
|
|
155
|
-
if (!isDbRow(row)) {
|
|
156
|
-
throw new Error('Invalid row');
|
|
157
|
-
}
|
|
158
|
-
rows.push(row);
|
|
159
|
-
}
|
|
160
|
-
return rows;
|
|
154
|
+
return value.map(assertDbRow);
|
|
161
155
|
};
|
|
162
156
|
const toDbRowOrUndefined = (value) => {
|
|
163
157
|
if (value === undefined)
|
|
164
158
|
return undefined;
|
|
165
|
-
|
|
166
|
-
throw new Error('Invalid row');
|
|
167
|
-
}
|
|
168
|
-
return value;
|
|
159
|
+
return assertDbRow(value);
|
|
169
160
|
};
|
|
170
161
|
const toRunResult = (value) => {
|
|
171
162
|
if (typeof value !== 'object' || value === null) {
|
|
@@ -206,13 +197,18 @@ export const withImmediateTransaction = (operation) => {
|
|
|
206
197
|
}
|
|
207
198
|
};
|
|
208
199
|
const createFieldError = (field) => new Error(`Invalid ${field}`);
|
|
200
|
+
const assertFiniteNumber = (value, field) => {
|
|
201
|
+
if (!Number.isFinite(value)) {
|
|
202
|
+
throw createFieldError(field);
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
};
|
|
209
206
|
const toNumber = (value, field) => {
|
|
210
|
-
if (typeof value === 'number'
|
|
211
|
-
return value;
|
|
207
|
+
if (typeof value === 'number') {
|
|
208
|
+
return assertFiniteNumber(value, field);
|
|
209
|
+
}
|
|
212
210
|
if (typeof value === 'bigint') {
|
|
213
|
-
|
|
214
|
-
if (Number.isFinite(numeric))
|
|
215
|
-
return numeric;
|
|
211
|
+
return assertFiniteNumber(Number(value), field);
|
|
216
212
|
}
|
|
217
213
|
throw createFieldError(field);
|
|
218
214
|
};
|
|
@@ -246,17 +242,68 @@ const toOptionalNumber = (value, field) => {
|
|
|
246
242
|
return undefined;
|
|
247
243
|
return toNumber(value, field);
|
|
248
244
|
};
|
|
249
|
-
export const mapRowToMemory = (row) => ({
|
|
245
|
+
export const mapRowToMemory = (row, tags = []) => ({
|
|
250
246
|
id: toSafeInteger(row.id, 'id'),
|
|
251
247
|
content: toString(row.content, 'content'),
|
|
252
248
|
summary: toOptionalString(row.summary, 'summary'),
|
|
249
|
+
tags,
|
|
253
250
|
importance: toSafeInteger(row.importance ?? 0, 'importance'),
|
|
254
251
|
memory_type: toMemoryType(row.memory_type ?? 'general', 'memory_type'),
|
|
255
252
|
created_at: toString(row.created_at, 'created_at'),
|
|
256
253
|
accessed_at: toString(row.accessed_at, 'accessed_at'),
|
|
257
254
|
hash: toString(row.hash, 'hash'),
|
|
258
255
|
});
|
|
259
|
-
export const mapRowToSearchResult = (row) => ({
|
|
260
|
-
...mapRowToMemory(row),
|
|
256
|
+
export const mapRowToSearchResult = (row, tags = []) => ({
|
|
257
|
+
...mapRowToMemory(row, tags),
|
|
261
258
|
relevance: toOptionalNumber(row.relevance, 'relevance') ?? 0,
|
|
262
259
|
});
|
|
260
|
+
export const mapRowToRelationship = (row) => ({
|
|
261
|
+
id: toSafeInteger(row.id, 'id'),
|
|
262
|
+
from_hash: toString(row.from_hash, 'from_hash'),
|
|
263
|
+
to_hash: toString(row.to_hash, 'to_hash'),
|
|
264
|
+
relation_type: toString(row.relation_type, 'relation_type'),
|
|
265
|
+
created_at: toString(row.created_at, 'created_at'),
|
|
266
|
+
});
|
|
267
|
+
const tagsSelectStatements = [];
|
|
268
|
+
const getSelectTagsStatement = (idCount) => {
|
|
269
|
+
const cached = tagsSelectStatements[idCount];
|
|
270
|
+
if (cached)
|
|
271
|
+
return cached;
|
|
272
|
+
const placeholders = Array.from({ length: idCount }, () => '?').join(', ');
|
|
273
|
+
const stmt = db.prepare(`SELECT memory_id, tag FROM tags WHERE memory_id IN (${placeholders}) ORDER BY memory_id, tag`);
|
|
274
|
+
tagsSelectStatements[idCount] = stmt;
|
|
275
|
+
return stmt;
|
|
276
|
+
};
|
|
277
|
+
const dedupeIds = (ids) => {
|
|
278
|
+
const seen = new Set();
|
|
279
|
+
const unique = [];
|
|
280
|
+
for (const id of ids) {
|
|
281
|
+
if (seen.has(id))
|
|
282
|
+
continue;
|
|
283
|
+
seen.add(id);
|
|
284
|
+
unique.push(id);
|
|
285
|
+
}
|
|
286
|
+
return unique;
|
|
287
|
+
};
|
|
288
|
+
const pushToMapArray = (map, key, value) => {
|
|
289
|
+
const existing = map.get(key);
|
|
290
|
+
if (existing) {
|
|
291
|
+
existing.push(value);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
map.set(key, [value]);
|
|
295
|
+
};
|
|
296
|
+
export const loadTagsForMemoryIds = (memoryIds) => {
|
|
297
|
+
const uniqueIds = dedupeIds(memoryIds);
|
|
298
|
+
if (uniqueIds.length === 0)
|
|
299
|
+
return new Map();
|
|
300
|
+
const stmt = getSelectTagsStatement(uniqueIds.length);
|
|
301
|
+
const rows = executeAll(stmt, ...uniqueIds);
|
|
302
|
+
const tagsById = new Map();
|
|
303
|
+
for (const row of rows) {
|
|
304
|
+
const memoryId = toSafeInteger(row.memory_id, 'memory_id');
|
|
305
|
+
const tag = toString(row.tag, 'tag');
|
|
306
|
+
pushToMapArray(tagsById, memoryId, tag);
|
|
307
|
+
}
|
|
308
|
+
return tagsById;
|
|
309
|
+
};
|
package/dist/core/memory-read.js
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
|
-
import { db, executeGet, executeRun, mapRowToMemory, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
1
|
+
import { db, executeGet, executeRun, loadTagsForMemoryIds, mapRowToMemory, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
2
2
|
const stmtGetMemoryByHash = db.prepare('SELECT * FROM memories WHERE hash = ?');
|
|
3
|
+
const stmtTouchMemoryByHash = db.prepare('UPDATE memories SET accessed_at = CURRENT_TIMESTAMP WHERE hash = ?');
|
|
3
4
|
const stmtDeleteMemoryByHash = db.prepare('DELETE FROM memories WHERE hash = ?');
|
|
4
5
|
export const getMemory = (hash) => {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
return withImmediateTransaction(() => {
|
|
7
|
+
executeRun(stmtTouchMemoryByHash, hash);
|
|
8
|
+
const row = executeGet(stmtGetMemoryByHash, hash);
|
|
9
|
+
if (!row)
|
|
10
|
+
return undefined;
|
|
11
|
+
const id = toSafeInteger(row.id, 'id');
|
|
12
|
+
const tags = loadTagsForMemoryIds([id]).get(id) ?? [];
|
|
13
|
+
return mapRowToMemory(row, tags);
|
|
14
|
+
});
|
|
7
15
|
};
|
|
8
16
|
export const deleteMemory = (hash) => {
|
|
9
17
|
const result = executeRun(stmtDeleteMemoryByHash, hash);
|
|
10
18
|
return { changes: toSafeInteger(result.changes, 'changes') };
|
|
11
19
|
};
|
|
20
|
+
const deleteMemoryForBatch = (hash) => {
|
|
21
|
+
try {
|
|
22
|
+
const result = deleteMemory(hash);
|
|
23
|
+
const deleted = result.changes > 0;
|
|
24
|
+
return deleted
|
|
25
|
+
? { item: { hash, deleted: true }, succeeded: true }
|
|
26
|
+
: {
|
|
27
|
+
item: { hash, deleted: false, error: 'Memory not found' },
|
|
28
|
+
succeeded: false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
33
|
+
return { item: { hash, deleted: false, error: message }, succeeded: false };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
12
36
|
export const deleteMemories = (hashes) => {
|
|
13
37
|
const results = [];
|
|
14
38
|
let succeeded = 0;
|
|
15
39
|
let failed = 0;
|
|
16
40
|
return withImmediateTransaction(() => {
|
|
17
41
|
for (const hash of hashes) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
succeeded++;
|
|
23
|
-
}
|
|
24
|
-
else {
|
|
25
|
-
results.push({ hash, deleted: false, error: 'Memory not found' });
|
|
26
|
-
failed++;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
catch (err) {
|
|
30
|
-
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
31
|
-
results.push({ hash, deleted: false, error: message });
|
|
32
|
-
failed++;
|
|
33
|
-
}
|
|
42
|
+
const outcome = deleteMemoryForBatch(hash);
|
|
43
|
+
results.push(outcome.item);
|
|
44
|
+
succeeded += outcome.succeeded ? 1 : 0;
|
|
45
|
+
failed += outcome.succeeded ? 0 : 1;
|
|
34
46
|
}
|
|
35
47
|
return { results, succeeded, failed };
|
|
36
48
|
});
|
|
@@ -89,31 +89,55 @@ const createMemoryInTransaction = (input) => {
|
|
|
89
89
|
return { id, hash, isNew };
|
|
90
90
|
};
|
|
91
91
|
export const createMemories = (items) => {
|
|
92
|
+
return withImmediateTransaction(() => createMemoriesInTransaction(items));
|
|
93
|
+
};
|
|
94
|
+
const withSavepoint = (name, fn) => {
|
|
95
|
+
db.exec(`SAVEPOINT ${name}`);
|
|
96
|
+
try {
|
|
97
|
+
const result = fn();
|
|
98
|
+
db.exec(`RELEASE ${name}`);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
db.exec(`ROLLBACK TO ${name}`);
|
|
103
|
+
db.exec(`RELEASE ${name}`);
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const createMemoriesInTransaction = (items) => {
|
|
92
108
|
const results = [];
|
|
93
109
|
let succeeded = 0;
|
|
94
110
|
let failed = 0;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
results.push({ ok: true, index: i, hash, isNew });
|
|
104
|
-
succeeded++;
|
|
105
|
-
db.exec('RELEASE mem_item');
|
|
106
|
-
}
|
|
107
|
-
catch (err) {
|
|
108
|
-
db.exec('ROLLBACK TO mem_item');
|
|
109
|
-
db.exec('RELEASE mem_item');
|
|
110
|
-
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
111
|
-
results.push({ ok: false, index: i, error: message });
|
|
112
|
-
failed++;
|
|
113
|
-
}
|
|
111
|
+
for (let i = 0; i < items.length; i++) {
|
|
112
|
+
const item = items[i];
|
|
113
|
+
if (!item)
|
|
114
|
+
continue;
|
|
115
|
+
const result = processCreateMemoriesItem(i, item);
|
|
116
|
+
results.push(result);
|
|
117
|
+
if (result.ok) {
|
|
118
|
+
succeeded++;
|
|
114
119
|
}
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
else {
|
|
121
|
+
failed++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { results, succeeded, failed };
|
|
125
|
+
};
|
|
126
|
+
const processCreateMemoriesItem = (index, item) => {
|
|
127
|
+
const savepointName = `mem_item_${index}`;
|
|
128
|
+
try {
|
|
129
|
+
const created = withSavepoint(savepointName, () => createMemoryInTransaction(item));
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
index,
|
|
133
|
+
hash: created.hash,
|
|
134
|
+
isNew: created.isNew,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
const message = err instanceof Error ? err.message : 'Unknown error';
|
|
139
|
+
return { ok: false, index, error: message };
|
|
140
|
+
}
|
|
117
141
|
};
|
|
118
142
|
const stmtDeleteTagsForMemory = db.prepare('DELETE FROM tags WHERE memory_id = ?');
|
|
119
143
|
const stmtUpdateContent = db.prepare('UPDATE memories SET content = ?, hash = ? WHERE id = ?');
|
|
@@ -121,6 +145,14 @@ const replaceTags = (memoryId, tags) => {
|
|
|
121
145
|
executeRun(stmtDeleteTagsForMemory, memoryId);
|
|
122
146
|
insertTags(memoryId, normalizeTags(tags, MAX_TAGS));
|
|
123
147
|
};
|
|
148
|
+
const assertNoDuplicateOnUpdate = (oldHash, newHash) => {
|
|
149
|
+
if (newHash === oldHash)
|
|
150
|
+
return;
|
|
151
|
+
const existingId = findMemoryIdByHash(newHash);
|
|
152
|
+
if (existingId !== undefined) {
|
|
153
|
+
throw new Error('Content already exists as another memory');
|
|
154
|
+
}
|
|
155
|
+
};
|
|
124
156
|
export const updateMemory = (hash, options) => {
|
|
125
157
|
const memoryId = findMemoryIdByHash(hash);
|
|
126
158
|
if (memoryId === undefined)
|
|
@@ -128,12 +160,7 @@ export const updateMemory = (hash, options) => {
|
|
|
128
160
|
return withImmediateTransaction(() => {
|
|
129
161
|
const newHash = buildHash(options.content);
|
|
130
162
|
// Check if new content would create a duplicate
|
|
131
|
-
|
|
132
|
-
const existingId = findMemoryIdByHash(newHash);
|
|
133
|
-
if (existingId !== undefined) {
|
|
134
|
-
throw new Error('Content already exists as another memory');
|
|
135
|
-
}
|
|
136
|
-
}
|
|
163
|
+
assertNoDuplicateOnUpdate(hash, newHash);
|
|
137
164
|
// Update content and hash
|
|
138
165
|
executeRun(stmtUpdateContent, options.content, newHash, memoryId);
|
|
139
166
|
// Update tags if provided, otherwise preserve existing tags
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { db, executeAll, executeGet, executeRun, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
1
|
+
import { db, executeAll, executeGet, executeRun, mapRowToRelationship, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
2
2
|
const stmtFindMemoryIdByHash = db.prepare('SELECT id FROM memories WHERE hash = ?');
|
|
3
3
|
const findMemoryIdByHash = (hash) => {
|
|
4
4
|
const row = executeGet(stmtFindMemoryIdByHash, hash);
|
|
@@ -13,18 +13,6 @@ const requireMemoryId = (hash) => {
|
|
|
13
13
|
}
|
|
14
14
|
return id;
|
|
15
15
|
};
|
|
16
|
-
const toString = (value, field) => {
|
|
17
|
-
if (typeof value === 'string')
|
|
18
|
-
return value;
|
|
19
|
-
throw new Error(`Invalid ${field}`);
|
|
20
|
-
};
|
|
21
|
-
const mapRowToRelationship = (row) => ({
|
|
22
|
-
id: toSafeInteger(row.id, 'id'),
|
|
23
|
-
from_hash: toString(row.from_hash, 'from_hash'),
|
|
24
|
-
to_hash: toString(row.to_hash, 'to_hash'),
|
|
25
|
-
relation_type: toString(row.relation_type, 'relation_type'),
|
|
26
|
-
created_at: toString(row.created_at, 'created_at'),
|
|
27
|
-
});
|
|
28
16
|
const stmtInsertRelationship = db.prepare(`
|
|
29
17
|
INSERT OR IGNORE INTO relationships (from_memory_id, to_memory_id, relation_type)
|
|
30
18
|
VALUES (?, ?, ?)
|
|
@@ -60,13 +48,14 @@ const buildGetRelationshipsQuery = (direction) => {
|
|
|
60
48
|
JOIN memories mf ON r.from_memory_id = mf.id
|
|
61
49
|
JOIN memories mt ON r.to_memory_id = mt.id
|
|
62
50
|
`;
|
|
51
|
+
const orderBy = ' ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id';
|
|
63
52
|
switch (direction) {
|
|
64
53
|
case 'outgoing':
|
|
65
|
-
return `${baseSelect} WHERE mf.hash =
|
|
54
|
+
return `${baseSelect} WHERE mf.hash = ?${orderBy}`;
|
|
66
55
|
case 'incoming':
|
|
67
|
-
return `${baseSelect} WHERE mt.hash =
|
|
56
|
+
return `${baseSelect} WHERE mt.hash = ?${orderBy}`;
|
|
68
57
|
case 'both':
|
|
69
|
-
return `${baseSelect} WHERE mf.hash = ? OR mt.hash =
|
|
58
|
+
return `${baseSelect} WHERE mf.hash = ? OR mt.hash = ?${orderBy}`;
|
|
70
59
|
}
|
|
71
60
|
};
|
|
72
61
|
const stmtGetRelationships = {
|
package/dist/core/search.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { db, executeAll, mapRowToSearchResult, prepareCached, toSafeInteger, } from './db.js';
|
|
1
|
+
import { db, executeAll, loadTagsForMemoryIds, mapRowToRelationship, mapRowToSearchResult, prepareCached, toSafeInteger, } from './db.js';
|
|
2
2
|
const MAX_QUERY_TOKENS = 50;
|
|
3
3
|
const DEFAULT_LIMIT = 100;
|
|
4
4
|
const RECENCY_DECAY_DAYS = 7;
|
|
@@ -65,25 +65,16 @@ const QUERY_INVALID_TOKENS = ['fts5', 'syntax error'];
|
|
|
65
65
|
const isSearchIndexMissing = (message) => INDEX_MISSING_TOKENS.some((token) => message.includes(token));
|
|
66
66
|
const isSearchQueryInvalid = (message) => QUERY_INVALID_TOKENS.some((token) => message.includes(token));
|
|
67
67
|
const getErrorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
68
|
-
const SEARCH_ERROR_MAP = [
|
|
69
|
-
{
|
|
70
|
-
matches: isSearchIndexMissing,
|
|
71
|
-
build: () => new Error('Search index unavailable. Ensure FTS5 is enabled and the index is ' +
|
|
72
|
-
'initialized.'),
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
matches: isSearchQueryInvalid,
|
|
76
|
-
build: (message) => new Error('Invalid search query syntax. Check for unbalanced quotes or special ' +
|
|
77
|
-
'characters. ' +
|
|
78
|
-
`Details: ${message}`),
|
|
79
|
-
},
|
|
80
|
-
];
|
|
81
68
|
const toSearchError = (err) => {
|
|
82
69
|
const message = getErrorMessage(err);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
70
|
+
if (isSearchIndexMissing(message)) {
|
|
71
|
+
return new Error('Search index unavailable. Ensure FTS5 is enabled and the index is ' +
|
|
72
|
+
'initialized.');
|
|
73
|
+
}
|
|
74
|
+
if (isSearchQueryInvalid(message)) {
|
|
75
|
+
return new Error('Invalid search query syntax. Check for unbalanced quotes or special ' +
|
|
76
|
+
'characters. ' +
|
|
77
|
+
`Details: ${message}`);
|
|
87
78
|
}
|
|
88
79
|
return undefined;
|
|
89
80
|
};
|
|
@@ -93,13 +84,17 @@ const executeSearch = (sql, params) => {
|
|
|
93
84
|
return executeAll(stmt, ...params);
|
|
94
85
|
}
|
|
95
86
|
catch (err) {
|
|
96
|
-
|
|
97
|
-
if (mappedError) {
|
|
98
|
-
throw mappedError;
|
|
99
|
-
}
|
|
100
|
-
throw err;
|
|
87
|
+
throw toSearchError(err) ?? err;
|
|
101
88
|
}
|
|
102
89
|
};
|
|
90
|
+
const mapRowsToSearchResultsWithTags = (rows) => {
|
|
91
|
+
const ids = rows.map((row) => toSafeInteger(row.id, 'id'));
|
|
92
|
+
const tagsById = loadTagsForMemoryIds(ids);
|
|
93
|
+
return rows.map((row) => {
|
|
94
|
+
const id = toSafeInteger(row.id, 'id');
|
|
95
|
+
return mapRowToSearchResult(row, tagsById.get(id) ?? []);
|
|
96
|
+
});
|
|
97
|
+
};
|
|
103
98
|
export const searchMemories = (input) => {
|
|
104
99
|
const tokens = tokenizeQuery(input.query);
|
|
105
100
|
if (tokens.length === 0) {
|
|
@@ -107,22 +102,10 @@ export const searchMemories = (input) => {
|
|
|
107
102
|
}
|
|
108
103
|
const { sql, params } = buildSearchQuery(tokens);
|
|
109
104
|
const rows = executeSearch(sql, params);
|
|
110
|
-
return rows
|
|
105
|
+
return mapRowsToSearchResultsWithTags(rows);
|
|
111
106
|
};
|
|
112
107
|
const MAX_RECALL_DEPTH = 3;
|
|
113
108
|
const MAX_RECALL_MEMORIES = 50;
|
|
114
|
-
const toString = (value, field) => {
|
|
115
|
-
if (typeof value === 'string')
|
|
116
|
-
return value;
|
|
117
|
-
throw new Error(`Invalid ${field}`);
|
|
118
|
-
};
|
|
119
|
-
const mapRowToRelationship = (row) => ({
|
|
120
|
-
id: toSafeInteger(row.id, 'id'),
|
|
121
|
-
from_hash: toString(row.from_hash, 'from_hash'),
|
|
122
|
-
to_hash: toString(row.to_hash, 'to_hash'),
|
|
123
|
-
relation_type: toString(row.relation_type, 'relation_type'),
|
|
124
|
-
created_at: toString(row.created_at, 'created_at'),
|
|
125
|
-
});
|
|
126
109
|
const buildRecallQuery = (seedCount, depth) => {
|
|
127
110
|
const seedPlaceholders = Array.from({ length: seedCount }, () => '?').join(', ');
|
|
128
111
|
const safeDepth = Math.min(Math.max(0, depth), MAX_RECALL_DEPTH);
|
|
@@ -166,30 +149,53 @@ const buildRelationshipsQuery = (memoryCount) => {
|
|
|
166
149
|
JOIN memories mt ON r.to_memory_id = mt.id
|
|
167
150
|
WHERE r.from_memory_id IN (${placeholders})
|
|
168
151
|
AND r.to_memory_id IN (${placeholders})
|
|
152
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
169
153
|
`;
|
|
170
154
|
return { sql };
|
|
171
155
|
};
|
|
156
|
+
const executeWithSql = (sql, params) => {
|
|
157
|
+
const stmt = db.prepare(sql);
|
|
158
|
+
return executeAll(stmt, ...params);
|
|
159
|
+
};
|
|
160
|
+
const executeRecall = (seedIds, depth) => {
|
|
161
|
+
const { sql } = buildRecallQuery(seedIds.length, depth);
|
|
162
|
+
return executeWithSql(sql, seedIds);
|
|
163
|
+
};
|
|
164
|
+
const loadRelationshipsForMemoryIds = (memoryIds) => {
|
|
165
|
+
const { sql } = buildRelationshipsQuery(memoryIds.length);
|
|
166
|
+
const rows = executeWithSql(sql, [...memoryIds, ...memoryIds]);
|
|
167
|
+
return rows.map(mapRowToRelationship);
|
|
168
|
+
};
|
|
169
|
+
const getRecallDepth = (depth) => depth ?? 1;
|
|
170
|
+
const emptyRecallResult = (depth) => ({
|
|
171
|
+
memories: [],
|
|
172
|
+
relationships: [],
|
|
173
|
+
depth,
|
|
174
|
+
});
|
|
175
|
+
const recallAtDepthZero = (searchResults, depth) => {
|
|
176
|
+
if (depth !== 0)
|
|
177
|
+
return undefined;
|
|
178
|
+
return { memories: searchResults, relationships: [], depth };
|
|
179
|
+
};
|
|
180
|
+
const recallAtPositiveDepth = (searchResults, depth) => {
|
|
181
|
+
const seedIds = searchResults.map((m) => m.id);
|
|
182
|
+
const recallRows = executeRecall(seedIds, depth);
|
|
183
|
+
const memories = mapRowsToSearchResultsWithTags(recallRows);
|
|
184
|
+
if (memories.length === 0) {
|
|
185
|
+
return emptyRecallResult(depth);
|
|
186
|
+
}
|
|
187
|
+
const relationships = loadRelationshipsForMemoryIds(memories.map((m) => m.id));
|
|
188
|
+
return { memories, relationships, depth };
|
|
189
|
+
};
|
|
172
190
|
export const recallMemories = (input) => {
|
|
173
|
-
const depth = input.depth ?? 1;
|
|
174
191
|
const searchResults = searchMemories({ query: input.query });
|
|
175
192
|
if (searchResults.length === 0) {
|
|
176
|
-
return
|
|
177
|
-
}
|
|
178
|
-
if (depth === 0) {
|
|
179
|
-
return { memories: searchResults, relationships: [], depth };
|
|
193
|
+
return emptyRecallResult(getRecallDepth(input.depth));
|
|
180
194
|
}
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const memories = recallRows.map(mapRowToSearchResult);
|
|
186
|
-
const allMemoryIds = memories.map((m) => m.id);
|
|
187
|
-
if (allMemoryIds.length === 0) {
|
|
188
|
-
return { memories: [], relationships: [], depth };
|
|
195
|
+
const depth = getRecallDepth(input.depth);
|
|
196
|
+
const depthZeroResult = recallAtDepthZero(searchResults, depth);
|
|
197
|
+
if (depthZeroResult) {
|
|
198
|
+
return depthZeroResult;
|
|
189
199
|
}
|
|
190
|
-
|
|
191
|
-
const relStmt = db.prepare(relSql);
|
|
192
|
-
const relRows = executeAll(relStmt, ...allMemoryIds, ...allMemoryIds);
|
|
193
|
-
const relationships = relRows.map(mapRowToRelationship);
|
|
194
|
-
return { memories, relationships, depth };
|
|
200
|
+
return recallAtPositiveDepth(searchResults, depth);
|
|
195
201
|
};
|