@j0hanz/memdb 1.3.0 → 1.4.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 +663 -205
- package/dist/async-context.d.ts +6 -0
- package/dist/async-context.js +4 -0
- package/dist/config.js +10 -4
- package/dist/core/abort.d.ts +1 -0
- package/dist/core/abort.js +3 -0
- package/dist/core/db.d.ts +5 -3
- package/dist/core/db.js +148 -112
- package/dist/core/memory-read.js +8 -25
- package/dist/core/memory-write.js +35 -28
- package/dist/core/relationships.js +53 -40
- package/dist/core/search.js +148 -76
- package/dist/error-utils.d.ts +1 -0
- package/dist/error-utils.js +28 -0
- package/dist/index.js +27 -12
- package/dist/instructions.md +37 -41
- package/dist/logger.js +6 -2
- package/dist/protocol-version-guard.js +17 -1
- package/dist/stdio-transport.js +6 -15
- package/dist/tools.js +32 -12
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mapRowToRelationship, requireMemoryIdByHash, sqlAll, sqlGet, sqlRun, toSafeInteger, withImmediateTransaction, } from './db.js';
|
|
2
2
|
const buildNotFoundMessage = (hash) => `Memory not found: ${hash}`;
|
|
3
3
|
export const createRelationship = (input) => withImmediateTransaction(() => {
|
|
4
4
|
const fromId = requireMemoryIdByHash(input.from_hash, buildNotFoundMessage(input.from_hash));
|
|
@@ -6,57 +6,70 @@ export const createRelationship = (input) => withImmediateTransaction(() => {
|
|
|
6
6
|
if (fromId === toId) {
|
|
7
7
|
throw new Error('Cannot create self-referential relationship');
|
|
8
8
|
}
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
const inserted = sqlGet `
|
|
10
|
+
INSERT OR IGNORE INTO relationships
|
|
11
|
+
(from_memory_id, to_memory_id, relation_type)
|
|
12
|
+
VALUES (${fromId}, ${toId}, ${input.relation_type})
|
|
13
|
+
RETURNING id
|
|
14
|
+
`;
|
|
15
15
|
if (inserted) {
|
|
16
16
|
return { id: toSafeInteger(inserted.id, 'id'), isNew: true };
|
|
17
17
|
}
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
const existing = sqlGet `
|
|
19
|
+
SELECT id FROM relationships
|
|
20
|
+
WHERE from_memory_id = ${fromId}
|
|
21
|
+
AND to_memory_id = ${toId}
|
|
22
|
+
AND relation_type = ${input.relation_type}
|
|
23
|
+
`;
|
|
23
24
|
if (!existing) {
|
|
24
25
|
throw new Error('Failed to resolve relationship id');
|
|
25
26
|
}
|
|
26
27
|
return { id: toSafeInteger(existing.id, 'id'), isNew: false };
|
|
27
28
|
});
|
|
28
|
-
const buildGetRelationshipsQuery = (direction) => {
|
|
29
|
-
const baseSelect = `
|
|
30
|
-
SELECT r.id, r.relation_type, r.created_at,
|
|
31
|
-
mf.hash as from_hash, mt.hash as to_hash
|
|
32
|
-
FROM relationships r
|
|
33
|
-
JOIN memories mf ON r.from_memory_id = mf.id
|
|
34
|
-
JOIN memories mt ON r.to_memory_id = mt.id
|
|
35
|
-
`;
|
|
36
|
-
const orderBy = ' ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id';
|
|
37
|
-
switch (direction) {
|
|
38
|
-
case 'outgoing':
|
|
39
|
-
return `${baseSelect} WHERE mf.hash = ?${orderBy}`;
|
|
40
|
-
case 'incoming':
|
|
41
|
-
return `${baseSelect} WHERE mt.hash = ?${orderBy}`;
|
|
42
|
-
case 'both':
|
|
43
|
-
return `${baseSelect} WHERE mf.hash = ? OR mt.hash = ?${orderBy}`;
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
29
|
export const getRelationships = (input) => {
|
|
47
30
|
const direction = input.direction ?? 'both';
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
31
|
+
const rows = (() => {
|
|
32
|
+
switch (direction) {
|
|
33
|
+
case 'outgoing':
|
|
34
|
+
return sqlAll `
|
|
35
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
36
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
37
|
+
FROM relationships r
|
|
38
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
39
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
40
|
+
WHERE mf.hash = ${input.hash}
|
|
41
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
42
|
+
`;
|
|
43
|
+
case 'incoming':
|
|
44
|
+
return sqlAll `
|
|
45
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
46
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
47
|
+
FROM relationships r
|
|
48
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
49
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
50
|
+
WHERE mt.hash = ${input.hash}
|
|
51
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
52
|
+
`;
|
|
53
|
+
case 'both':
|
|
54
|
+
return sqlAll `
|
|
55
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
56
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
57
|
+
FROM relationships r
|
|
58
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
59
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
60
|
+
WHERE mf.hash = ${input.hash} OR mt.hash = ${input.hash}
|
|
61
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
51
65
|
return rows.map(mapRowToRelationship);
|
|
52
66
|
};
|
|
53
67
|
export const deleteRelationship = (input) => {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const result = executeRun(stmtDeleteRelationship, input.from_hash, input.to_hash, input.relation_type);
|
|
68
|
+
const result = sqlRun `
|
|
69
|
+
DELETE FROM relationships
|
|
70
|
+
WHERE from_memory_id = (SELECT id FROM memories WHERE hash = ${input.from_hash})
|
|
71
|
+
AND to_memory_id = (SELECT id FROM memories WHERE hash = ${input.to_hash})
|
|
72
|
+
AND relation_type = ${input.relation_type}
|
|
73
|
+
`;
|
|
61
74
|
return { changes: toSafeInteger(result.changes, 'changes') };
|
|
62
75
|
};
|
package/dist/core/search.js
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getErrorMessage } from '../error-utils.js';
|
|
2
|
+
import { throwIfAborted } from './abort.js';
|
|
3
|
+
import { executeAll, loadTagsForMemoryIds, mapRowToRelationship, mapRowToSearchResult, prepareCached, sqlAll, } from './db.js';
|
|
2
4
|
const MAX_QUERY_TOKENS = 50;
|
|
3
5
|
const DEFAULT_LIMIT = 100;
|
|
4
6
|
const MAX_RECALL_DEPTH = 3;
|
|
5
7
|
const MAX_RECALL_MEMORIES = 50;
|
|
6
8
|
const RECENCY_DECAY_DAYS = 7;
|
|
7
9
|
const RECENCY_WEIGHT = 0.15;
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const CASE_FOLD_LOCALE = 'en-US';
|
|
11
|
+
// Hoisted regex for performance
|
|
12
|
+
const WHITESPACE_REGEX = /\s+/;
|
|
13
|
+
const QUOTE_GLOBAL_REGEX = /"/g;
|
|
14
|
+
const QUERY_SEGMENTER = typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function'
|
|
15
|
+
? new Intl.Segmenter(undefined, { granularity: 'word' })
|
|
16
|
+
: undefined;
|
|
17
|
+
const normalizeTagToken = (token) => token.normalize('NFKC').toLocaleLowerCase(CASE_FOLD_LOCALE);
|
|
18
|
+
const splitOnWhitespace = (query) => {
|
|
19
|
+
const trimmed = query.trim();
|
|
20
|
+
if (trimmed.length === 0)
|
|
21
|
+
return [];
|
|
22
|
+
return trimmed.split(WHITESPACE_REGEX);
|
|
23
|
+
};
|
|
24
|
+
const segmentQuery = (query) => {
|
|
25
|
+
if (!QUERY_SEGMENTER)
|
|
26
|
+
return splitOnWhitespace(query);
|
|
27
|
+
const tokens = [];
|
|
28
|
+
for (const segment of QUERY_SEGMENTER.segment(query.trim())) {
|
|
29
|
+
if (!segment.isWordLike)
|
|
30
|
+
continue;
|
|
31
|
+
const token = segment.segment.trim();
|
|
32
|
+
if (token.length > 0)
|
|
33
|
+
tokens.push(token);
|
|
11
34
|
}
|
|
35
|
+
return tokens;
|
|
12
36
|
};
|
|
13
37
|
const tokenizeQuery = (query) => {
|
|
14
|
-
const parts = query
|
|
15
|
-
.trim()
|
|
16
|
-
.split(/\s+/)
|
|
17
|
-
.filter((w) => w.length > 0);
|
|
38
|
+
const parts = segmentQuery(query);
|
|
18
39
|
if (parts.length === 0)
|
|
19
40
|
return [];
|
|
20
41
|
if (parts.length > MAX_QUERY_TOKENS) {
|
|
@@ -22,9 +43,37 @@ const tokenizeQuery = (query) => {
|
|
|
22
43
|
}
|
|
23
44
|
return parts;
|
|
24
45
|
};
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
46
|
+
const normalizeTokensForTags = (tokens) => {
|
|
47
|
+
const len = tokens.length;
|
|
48
|
+
const normalized = [];
|
|
49
|
+
for (let i = 0; i < len; i++) {
|
|
50
|
+
const t = tokens[i];
|
|
51
|
+
if (!t)
|
|
52
|
+
continue;
|
|
53
|
+
const token = normalizeTagToken(t);
|
|
54
|
+
if (token.length > 0)
|
|
55
|
+
normalized.push(token);
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
};
|
|
59
|
+
const escapeFtsToken = (token) => `"${token.replace(QUOTE_GLOBAL_REGEX, '""')}"`;
|
|
60
|
+
const buildFtsQuery = (tokens) => {
|
|
61
|
+
const len = tokens.length;
|
|
62
|
+
if (len === 0)
|
|
63
|
+
return '""';
|
|
64
|
+
const first = tokens[0];
|
|
65
|
+
if (!first)
|
|
66
|
+
return '""';
|
|
67
|
+
let query = escapeFtsToken(first);
|
|
68
|
+
for (let i = 1; i < len; i++) {
|
|
69
|
+
const val = tokens[i];
|
|
70
|
+
if (val) {
|
|
71
|
+
query += ` OR ${escapeFtsToken(val)}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return query;
|
|
75
|
+
};
|
|
76
|
+
const buildSearchQuery = (tokens, tagTokens) => {
|
|
28
77
|
const ftsQuery = buildFtsQuery(tokens);
|
|
29
78
|
const relevanceExpr = '1.0 / (1.0 + abs(bm25(memories_fts)))';
|
|
30
79
|
const recencyBoost = `MAX(0.0, (${RECENCY_DECAY_DAYS}.0 - julianday('now') + julianday(created_at)) / ${RECENCY_DECAY_DAYS}.0) * ${RECENCY_WEIGHT}`;
|
|
@@ -53,7 +102,10 @@ const buildSearchQuery = (tokens) => {
|
|
|
53
102
|
ORDER BY relevance DESC
|
|
54
103
|
LIMIT ?
|
|
55
104
|
`;
|
|
56
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
sql,
|
|
107
|
+
params: [ftsQuery, JSON.stringify(tagTokens), DEFAULT_LIMIT],
|
|
108
|
+
};
|
|
57
109
|
};
|
|
58
110
|
const INDEX_MISSING_TOKENS = [
|
|
59
111
|
'no such module: fts5',
|
|
@@ -62,7 +114,6 @@ const INDEX_MISSING_TOKENS = [
|
|
|
62
114
|
const QUERY_INVALID_TOKENS = ['fts5', 'syntax error'];
|
|
63
115
|
const isSearchIndexMissing = (message) => INDEX_MISSING_TOKENS.some((token) => message.includes(token));
|
|
64
116
|
const isSearchQueryInvalid = (message) => QUERY_INVALID_TOKENS.some((token) => message.includes(token));
|
|
65
|
-
const getErrorMessage = (err) => err instanceof Error ? err.message : String(err);
|
|
66
117
|
const toSearchError = (err) => {
|
|
67
118
|
const message = getErrorMessage(err);
|
|
68
119
|
if (isSearchIndexMissing(message)) {
|
|
@@ -85,19 +136,37 @@ const executeSearch = (sql, params) => {
|
|
|
85
136
|
};
|
|
86
137
|
const enrichSearchResultsWithTags = (rows, signal) => {
|
|
87
138
|
throwIfAborted(signal);
|
|
88
|
-
const
|
|
139
|
+
const count = rows.length;
|
|
140
|
+
if (count === 0)
|
|
141
|
+
return [];
|
|
142
|
+
const ids = new Array(count);
|
|
143
|
+
for (let i = 0; i < count; i++) {
|
|
144
|
+
const row = rows[i];
|
|
145
|
+
if (!row)
|
|
146
|
+
throw new Error('Unreachable');
|
|
147
|
+
// Fast cast for internal DB use; strict validation happens in mapRowToSearchResult
|
|
148
|
+
ids[i] = Number(row.id);
|
|
149
|
+
}
|
|
89
150
|
const tagsById = loadTagsForMemoryIds(ids);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
151
|
+
const results = new Array(count);
|
|
152
|
+
for (let i = 0; i < count; i++) {
|
|
153
|
+
const row = rows[i];
|
|
154
|
+
const id = ids[i];
|
|
155
|
+
if (!row || id === undefined)
|
|
156
|
+
throw new Error('Unreachable');
|
|
157
|
+
// Map tags safely; id validation happens inside mapRowToSearchResult
|
|
158
|
+
const tags = tagsById.get(id);
|
|
159
|
+
results[i] = mapRowToSearchResult(row, tags ?? []);
|
|
160
|
+
}
|
|
161
|
+
return results;
|
|
94
162
|
};
|
|
95
163
|
export const searchMemories = (input, signal) => {
|
|
96
164
|
throwIfAborted(signal);
|
|
97
165
|
const tokens = tokenizeQuery(input.query);
|
|
98
166
|
if (tokens.length === 0)
|
|
99
167
|
throw new Error('Query cannot be empty');
|
|
100
|
-
const
|
|
168
|
+
const tagTokens = normalizeTokensForTags(tokens);
|
|
169
|
+
const { sql, params } = buildSearchQuery(tokens, tagTokens);
|
|
101
170
|
const rows = executeSearch(sql, params);
|
|
102
171
|
throwIfAborted(signal);
|
|
103
172
|
return enrichSearchResultsWithTags(rows, signal);
|
|
@@ -109,68 +178,56 @@ const normalizeRecallDepth = (depth) => {
|
|
|
109
178
|
const asInt = Math.trunc(raw);
|
|
110
179
|
return Math.min(Math.max(0, asInt), MAX_RECALL_DEPTH);
|
|
111
180
|
};
|
|
112
|
-
const buildRecallQuery = () => `
|
|
113
|
-
WITH RECURSIVE connected(memory_id, depth) AS (
|
|
114
|
-
-- Seed memories from search results
|
|
115
|
-
SELECT m.id, 0
|
|
116
|
-
FROM memories m
|
|
117
|
-
WHERE m.id IN (SELECT value FROM json_each(?))
|
|
118
|
-
|
|
119
|
-
UNION
|
|
120
|
-
|
|
121
|
-
-- Follow relationships (both directions) up to max depth
|
|
122
|
-
SELECT
|
|
123
|
-
CASE
|
|
124
|
-
WHEN r.from_memory_id = c.memory_id THEN r.to_memory_id
|
|
125
|
-
ELSE r.from_memory_id
|
|
126
|
-
END,
|
|
127
|
-
c.depth + 1
|
|
128
|
-
FROM relationships r
|
|
129
|
-
JOIN connected c ON (r.from_memory_id = c.memory_id OR r.to_memory_id = c.memory_id)
|
|
130
|
-
WHERE c.depth < ?
|
|
131
|
-
),
|
|
132
|
-
unique_memories AS (
|
|
133
|
-
SELECT DISTINCT memory_id, MIN(depth) as min_depth
|
|
134
|
-
FROM connected
|
|
135
|
-
GROUP BY memory_id
|
|
136
|
-
ORDER BY min_depth
|
|
137
|
-
LIMIT ?
|
|
138
|
-
)
|
|
139
|
-
SELECT m.*, 1.0 / (1.0 + um.min_depth) as relevance
|
|
140
|
-
FROM memories m
|
|
141
|
-
JOIN unique_memories um ON m.id = um.memory_id
|
|
142
|
-
ORDER BY um.min_depth, m.created_at DESC
|
|
143
|
-
`;
|
|
144
|
-
const buildRelationshipsQuery = () => `
|
|
145
|
-
WITH ids(id) AS (SELECT value FROM json_each(?))
|
|
146
|
-
SELECT r.id, r.relation_type, r.created_at,
|
|
147
|
-
mf.hash as from_hash, mt.hash as to_hash
|
|
148
|
-
FROM relationships r
|
|
149
|
-
JOIN ids a ON r.from_memory_id = a.id
|
|
150
|
-
JOIN ids b ON r.to_memory_id = b.id
|
|
151
|
-
JOIN memories mf ON r.from_memory_id = mf.id
|
|
152
|
-
JOIN memories mt ON r.to_memory_id = mt.id
|
|
153
|
-
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
154
|
-
`;
|
|
155
|
-
const executeWithSql = (sql, params) => {
|
|
156
|
-
const stmt = prepareCached(sql);
|
|
157
|
-
return executeAll(stmt, ...params);
|
|
158
|
-
};
|
|
159
181
|
const executeRecall = (seedIds, depth) => {
|
|
160
182
|
if (seedIds.length === 0)
|
|
161
183
|
return [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
184
|
+
return sqlAll `
|
|
185
|
+
WITH RECURSIVE connected(memory_id, depth) AS (
|
|
186
|
+
-- Seed memories from search results
|
|
187
|
+
SELECT m.id, 0
|
|
188
|
+
FROM memories m
|
|
189
|
+
WHERE m.id IN (SELECT value FROM json_each(${JSON.stringify(seedIds)}))
|
|
190
|
+
|
|
191
|
+
UNION
|
|
192
|
+
|
|
193
|
+
-- Follow relationships (both directions) up to max depth
|
|
194
|
+
SELECT
|
|
195
|
+
CASE
|
|
196
|
+
WHEN r.from_memory_id = c.memory_id THEN r.to_memory_id
|
|
197
|
+
ELSE r.from_memory_id
|
|
198
|
+
END,
|
|
199
|
+
c.depth + 1
|
|
200
|
+
FROM relationships r
|
|
201
|
+
JOIN connected c ON (r.from_memory_id = c.memory_id OR r.to_memory_id = c.memory_id)
|
|
202
|
+
WHERE c.depth < ${depth}
|
|
203
|
+
),
|
|
204
|
+
unique_memories AS (
|
|
205
|
+
SELECT DISTINCT memory_id, MIN(depth) as min_depth
|
|
206
|
+
FROM connected
|
|
207
|
+
GROUP BY memory_id
|
|
208
|
+
ORDER BY min_depth
|
|
209
|
+
LIMIT ${MAX_RECALL_MEMORIES}
|
|
210
|
+
)
|
|
211
|
+
SELECT m.*, 1.0 / (1.0 + um.min_depth) as relevance
|
|
212
|
+
FROM memories m
|
|
213
|
+
JOIN unique_memories um ON m.id = um.memory_id
|
|
214
|
+
ORDER BY um.min_depth, m.created_at DESC
|
|
215
|
+
`;
|
|
168
216
|
};
|
|
169
217
|
const loadRelationshipsForMemoryIds = (memoryIds) => {
|
|
170
218
|
if (memoryIds.length === 0)
|
|
171
219
|
return [];
|
|
172
|
-
const
|
|
173
|
-
|
|
220
|
+
const rows = sqlAll `
|
|
221
|
+
WITH ids(id) AS (SELECT value FROM json_each(${JSON.stringify(memoryIds)}))
|
|
222
|
+
SELECT r.id, r.relation_type, r.created_at,
|
|
223
|
+
mf.hash as from_hash, mt.hash as to_hash
|
|
224
|
+
FROM relationships r
|
|
225
|
+
JOIN ids a ON r.from_memory_id = a.id
|
|
226
|
+
JOIN ids b ON r.to_memory_id = b.id
|
|
227
|
+
JOIN memories mf ON r.from_memory_id = mf.id
|
|
228
|
+
JOIN memories mt ON r.to_memory_id = mt.id
|
|
229
|
+
ORDER BY r.relation_type, mf.hash, mt.hash, r.created_at, r.id
|
|
230
|
+
`;
|
|
174
231
|
return rows.map(mapRowToRelationship);
|
|
175
232
|
};
|
|
176
233
|
const emptyRecallResult = (depth) => ({
|
|
@@ -185,14 +242,29 @@ const recallAtDepthZero = (searchResults, depth) => {
|
|
|
185
242
|
};
|
|
186
243
|
const recallAtPositiveDepth = (searchResults, depth, signal) => {
|
|
187
244
|
throwIfAborted(signal);
|
|
188
|
-
const
|
|
245
|
+
const len = searchResults.length;
|
|
246
|
+
const seedIds = new Array(len);
|
|
247
|
+
for (let i = 0; i < len; i++) {
|
|
248
|
+
const res = searchResults[i];
|
|
249
|
+
if (!res)
|
|
250
|
+
throw new Error('Unreachable');
|
|
251
|
+
seedIds[i] = res.id;
|
|
252
|
+
}
|
|
189
253
|
const recallRows = executeRecall(seedIds, depth);
|
|
190
254
|
throwIfAborted(signal);
|
|
191
255
|
const memories = enrichSearchResultsWithTags(recallRows, signal);
|
|
192
256
|
if (memories.length === 0)
|
|
193
257
|
return emptyRecallResult(depth);
|
|
194
258
|
throwIfAborted(signal);
|
|
195
|
-
const
|
|
259
|
+
const memLen = memories.length;
|
|
260
|
+
const memIds = new Array(memLen);
|
|
261
|
+
for (let i = 0; i < memLen; i++) {
|
|
262
|
+
const mem = memories[i];
|
|
263
|
+
if (!mem)
|
|
264
|
+
throw new Error('Unreachable');
|
|
265
|
+
memIds[i] = mem.id;
|
|
266
|
+
}
|
|
267
|
+
const relationships = loadRelationshipsForMemoryIds(memIds);
|
|
196
268
|
return { memories, relationships, depth };
|
|
197
269
|
};
|
|
198
270
|
export const recallMemories = (input, signal) => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const getErrorMessage: (error: unknown) => string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getSystemErrorMessage, getSystemErrorName, inspect } from 'node:util';
|
|
2
|
+
const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
|
|
3
|
+
const toErrnoInfo = (error) => {
|
|
4
|
+
if (typeof error.code === 'string' && error.code.length > 0) {
|
|
5
|
+
return `${error.code}: ${error.message}`;
|
|
6
|
+
}
|
|
7
|
+
if (typeof error.errno === 'number') {
|
|
8
|
+
try {
|
|
9
|
+
const name = getSystemErrorName(error.errno);
|
|
10
|
+
const message = getSystemErrorMessage(error.errno);
|
|
11
|
+
return `${name}: ${message}`;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
};
|
|
19
|
+
const inspectValue = (value) => inspect(value, { breakLength: 120, colors: false, compact: 3, depth: 2 });
|
|
20
|
+
export const getErrorMessage = (error) => {
|
|
21
|
+
if (error instanceof Error) {
|
|
22
|
+
const errnoInfo = toErrnoInfo(error);
|
|
23
|
+
return errnoInfo ?? error.message;
|
|
24
|
+
}
|
|
25
|
+
if (isNonEmptyString(error))
|
|
26
|
+
return error;
|
|
27
|
+
return inspectValue(error);
|
|
28
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { findPackageJSON } from 'node:module';
|
|
4
5
|
import process from 'node:process';
|
|
5
6
|
import { McpServer, ResourceTemplate, } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
7
|
import { SUPPORTED_PROTOCOL_VERSIONS } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
-
import pkg from '../package.json' with { type: 'json' };
|
|
8
8
|
import { closeDb, initDb } from './core/db.js';
|
|
9
9
|
import { attachProtocolLogger, logger } from './logger.js';
|
|
10
10
|
import { ProtocolVersionGuardTransport } from './protocol-version-guard.js';
|
|
11
11
|
import { BatchRejectingStdioServerTransport } from './stdio-transport.js';
|
|
12
12
|
import { registerAllTools } from './tools.js';
|
|
13
|
-
const readPackageVersion = () => {
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
const readPackageVersion = async () => {
|
|
14
|
+
const packageJsonPath = findPackageJSON('..', import.meta.url);
|
|
15
|
+
if (!packageJsonPath)
|
|
16
|
+
return undefined;
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(packageJsonPath, {
|
|
19
|
+
encoding: 'utf-8',
|
|
20
|
+
signal: AbortSignal.timeout(2000),
|
|
21
|
+
});
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return typeof parsed.version === 'string' ? parsed.version : undefined;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
16
28
|
};
|
|
17
29
|
const toNonEmptyTrimmedOrUndefined = (text) => {
|
|
18
30
|
const trimmed = text.trim();
|
|
@@ -113,7 +125,7 @@ const closeServerResources = async () => {
|
|
|
113
125
|
};
|
|
114
126
|
const clearTimerAndExit = (timer, code) => {
|
|
115
127
|
clearTimeout(timer);
|
|
116
|
-
process.
|
|
128
|
+
process.exitCode = code;
|
|
117
129
|
};
|
|
118
130
|
const exitWithShutdownTimer = (timer, code, error) => {
|
|
119
131
|
if (error !== undefined) {
|
|
@@ -176,19 +188,22 @@ const main = async () => {
|
|
|
176
188
|
const registerSignalHandlers = () => {
|
|
177
189
|
const signals = ['SIGTERM', 'SIGINT', 'SIGBREAK'];
|
|
178
190
|
for (const signal of signals) {
|
|
179
|
-
process.
|
|
191
|
+
process.once(signal, () => {
|
|
180
192
|
shutdown(signal);
|
|
181
193
|
});
|
|
182
194
|
}
|
|
183
195
|
};
|
|
196
|
+
const handleFatalError = (message, error) => {
|
|
197
|
+
logger.error(message, error);
|
|
198
|
+
closeDb();
|
|
199
|
+
process.exit(1);
|
|
200
|
+
};
|
|
184
201
|
const registerProcessHandlers = () => {
|
|
185
|
-
process.
|
|
186
|
-
|
|
187
|
-
process.exit(1);
|
|
202
|
+
process.once('uncaughtException', (err, origin) => {
|
|
203
|
+
handleFatalError(`Uncaught exception (${origin}):`, err);
|
|
188
204
|
});
|
|
189
|
-
process.
|
|
190
|
-
|
|
191
|
-
process.exit(1);
|
|
205
|
+
process.once('unhandledRejection', (reason) => {
|
|
206
|
+
handleFatalError('Unhandled rejection:', reason);
|
|
192
207
|
});
|
|
193
208
|
};
|
|
194
209
|
void main();
|
package/dist/instructions.md
CHANGED
|
@@ -1,59 +1,55 @@
|
|
|
1
|
-
# memdb
|
|
1
|
+
# memdb INSTRUCTIONS
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Available as resource `internal://instructions`. Load when unsure about tool usage.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## CORE CAPABILITY
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
7
|
+
- Domain: SQLite-backed memory store with FTS5 search and knowledge graph for AI assistants.
|
|
8
|
+
- Primary Resources: Memory (content+tags+hash), Relationship (directed edges).
|
|
9
|
+
- Tools: `search_memories` `get_memory` `recall` `get_relationships` `memory_stats` (READ); `store_memory` `store_memories` `update_memory` `delete_memory` `delete_memories` `create_relationship` `delete_relationship` (WRITE).
|
|
9
10
|
|
|
10
|
-
##
|
|
11
|
+
## THE "GOLDEN PATH" WORKFLOWS (CRITICAL)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
### WORKFLOW A: Recall & Exploration
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
1. `search_memories` with `{ query }` — find memories by content/tags.
|
|
16
|
+
2. `recall` with `{ query, depth }` — traverse graph connections from hits.
|
|
17
|
+
3. `get_memory` with `{ hash }` — exact retrieval using hash from results.
|
|
18
|
+
NOTE: Never guess hashes. Always search first.
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
2. Call `recall` (depth 1–2) to traverse the knowledge graph from relevant hits.
|
|
18
|
-
3. Call `get_memory` using the `hash` (SHA-256) for exact retrieval.
|
|
19
|
-
> Constraint: Never guess hashes. Always search or recall first.
|
|
20
|
+
### WORKFLOW B: Knowledge Management
|
|
20
21
|
|
|
21
|
-
|
|
22
|
+
1. `store_memory` or `store_memories` (batch ≤50) to persist content with tags.
|
|
23
|
+
2. `create_relationship` to link memories via `{ from_hash, to_hash, relation_type }`.
|
|
24
|
+
3. `update_memory` with `{ hash, content }` to revise — returns new hash.
|
|
25
|
+
4. `delete_memory` / `delete_memories` — confirm with user first.
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
2. Call `create_relationship` to link related memories (directed).
|
|
25
|
-
3. Call `update_memory` to revise; this changes the hash.
|
|
26
|
-
4. Call `delete_memory` only with user confirmation.
|
|
27
|
+
## TOOL NUANCES & GOTCHAS
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
`search_memories` — `query` 1–1000 chars. Broaden if no results.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
`recall` — `depth` 0–3 (default 1). Higher depth = more latency. 0 = no traversal.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
- **Purpose:** Full-text search over content and tags.
|
|
34
|
-
- **Inputs:** `query` string (required).
|
|
35
|
-
- **Common failure modes:** Empty results for too-specific queries; try broader terms.
|
|
33
|
+
`store_memory` / `store_memories` — `content` ≤100K, `tags` 1–100 (no whitespace, ≤50 chars), optional `importance` 0–10, optional `memory_type` (general|fact|plan|decision|reflection|lesson|error|gradient). Idempotent. Batch supports partial success.
|
|
36
34
|
|
|
37
|
-
|
|
38
|
-
- **Purpose:** Graph traversal starting from a defined query.
|
|
39
|
-
- **Inputs:** `query` string; `depth` (default 1, max 3 recommended).
|
|
40
|
-
- **Latency:** Higher depth increases time/token usage significantly.
|
|
35
|
+
`update_memory` — `hash` must exist. New `content` required, `tags` optional. Changes the hash.
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
- **Purpose:** Persist new information.
|
|
44
|
-
- **Inputs:** `content` (text), `tags` (array of strings).
|
|
45
|
-
- **Side effects:** Writes to DB. Idempotent if content/tags identical (same hash).
|
|
37
|
+
`get_memory` — `hash`: 64 hex chars (SHA-256). E_NOT_FOUND if missing.
|
|
46
38
|
|
|
47
|
-
|
|
48
|
-
- **Inputs:** `hash` (must exist), new `content`/`tags`.
|
|
49
|
-
- **Side effects:** Creating a new memory hash; effectively a "move" + "create".
|
|
39
|
+
`create_relationship` — `from_hash`, `to_hash`, `relation_type` (e.g. related_to, causes, depends_on). Both hashes must exist. Idempotent.
|
|
50
40
|
|
|
51
|
-
|
|
52
|
-
- **Inputs:** `from_hash`, `to_hash`, `relation_type` (e.g., "related_to").
|
|
53
|
-
- **Constraint:** Both hashes must exist.
|
|
41
|
+
`get_relationships` — `hash` required, `direction` optional (outgoing|incoming|both).
|
|
54
42
|
|
|
55
|
-
|
|
43
|
+
`delete_memory` / `delete_memories` / `delete_relationship` — Destructive. E_NOT_FOUND if missing.
|
|
56
44
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
`memory_stats` — No input. Returns counts and timestamps.
|
|
46
|
+
|
|
47
|
+
## ERROR HANDLING STRATEGY
|
|
48
|
+
|
|
49
|
+
- `E_NOT_FOUND`: Hash missing. Search/recall first.
|
|
50
|
+
- `E_TIMEOUT`: Reduce batch size or recall depth.
|
|
51
|
+
- Validation error: 64-char hex hash, non-empty content, no whitespace in tags.
|
|
52
|
+
|
|
53
|
+
## RESOURCES
|
|
54
|
+
|
|
55
|
+
- `internal://instructions`: This document.
|
package/dist/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { format } from 'node:util';
|
|
2
|
+
import { getToolContext } from './async-context.js';
|
|
2
3
|
import { config } from './config.js';
|
|
3
4
|
const LEVELS = {
|
|
4
5
|
error: 0,
|
|
@@ -18,12 +19,15 @@ const toProtocolLogLevel = (level) => {
|
|
|
18
19
|
}
|
|
19
20
|
};
|
|
20
21
|
const formatLogMessage = (msg, args) => args.length === 0 ? msg : format(msg, ...args);
|
|
22
|
+
const formatContextPrefix = (context) => context ? `[tool:${context.toolName}] ` : '';
|
|
21
23
|
const createWriter = (level) => (msg, ...args) => {
|
|
22
24
|
if (LEVELS[level] > threshold)
|
|
23
25
|
return;
|
|
24
|
-
console.error(`[${level.toUpperCase()}] ${msg}`, ...args);
|
|
25
26
|
const formatted = formatLogMessage(msg, args);
|
|
26
|
-
|
|
27
|
+
const context = getToolContext();
|
|
28
|
+
const withContext = `${formatContextPrefix(context)}${formatted}`;
|
|
29
|
+
console.error(`[${level.toUpperCase()}] ${withContext}`);
|
|
30
|
+
protocolLogSink?.(toProtocolLogLevel(level), withContext);
|
|
27
31
|
};
|
|
28
32
|
export const attachProtocolLogger = (sink) => {
|
|
29
33
|
protocolLogSink = sink;
|