@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.
@@ -1,4 +1,4 @@
1
- import { executeAll, executeGet, executeRun, mapRowToRelationship, prepareCached, requireMemoryIdByHash, toSafeInteger, withImmediateTransaction, } from './db.js';
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 stmtInsertRelationship = prepareCached(`
10
- INSERT OR IGNORE INTO relationships (from_memory_id, to_memory_id, relation_type)
11
- VALUES (?, ?, ?)
12
- RETURNING id
13
- `);
14
- const inserted = executeGet(stmtInsertRelationship, fromId, toId, input.relation_type);
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 stmtFindRelationshipId = prepareCached(`
19
- SELECT id FROM relationships
20
- WHERE from_memory_id = ? AND to_memory_id = ? AND relation_type = ?
21
- `);
22
- const existing = executeGet(stmtFindRelationshipId, fromId, toId, input.relation_type);
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 stmt = prepareCached(buildGetRelationshipsQuery(direction));
49
- const params = direction === 'both' ? [input.hash, input.hash] : [input.hash];
50
- const rows = executeAll(stmt, ...params);
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 stmtDeleteRelationship = prepareCached(`
55
- DELETE FROM relationships
56
- WHERE from_memory_id = (SELECT id FROM memories WHERE hash = ?)
57
- AND to_memory_id = (SELECT id FROM memories WHERE hash = ?)
58
- AND relation_type = ?
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
  };
@@ -1,20 +1,41 @@
1
- import { executeAll, loadTagsForMemoryIds, mapRowToRelationship, mapRowToSearchResult, prepareCached, toSafeInteger, } from './db.js';
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 throwIfAborted = (signal) => {
9
- if (signal && typeof signal.throwIfAborted === 'function') {
10
- signal.throwIfAborted();
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 escapeFtsToken = (token) => `"${token.replace(/"/g, '""')}"`;
26
- const buildFtsQuery = (tokens) => tokens.length === 0 ? '""' : tokens.map(escapeFtsToken).join(' OR ');
27
- const buildSearchQuery = (tokens) => {
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 { sql, params: [ftsQuery, JSON.stringify(tokens), DEFAULT_LIMIT] };
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 ids = rows.map((row) => toSafeInteger(row.id, 'id'));
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
- return rows.map((row) => {
91
- const id = toSafeInteger(row.id, 'id');
92
- return mapRowToSearchResult(row, tagsById.get(id) ?? []);
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 { sql, params } = buildSearchQuery(tokens);
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
- const sql = buildRecallQuery();
163
- return executeWithSql(sql, [
164
- JSON.stringify(seedIds),
165
- depth,
166
- MAX_RECALL_MEMORIES,
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 sql = buildRelationshipsQuery();
173
- const rows = executeWithSql(sql, [JSON.stringify(memoryIds)]);
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 seedIds = searchResults.map((m) => m.id);
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 relationships = loadRelationshipsForMemoryIds(memories.map((m) => m.id));
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 { version } = pkg;
15
- return Promise.resolve(typeof version === 'string' ? version : undefined);
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.exit(code);
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.on(signal, () => {
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.on('uncaughtException', (err, origin) => {
186
- logger.error(`Uncaught exception (${origin}):`, err);
187
- process.exit(1);
202
+ process.once('uncaughtException', (err, origin) => {
203
+ handleFatalError(`Uncaught exception (${origin}):`, err);
188
204
  });
189
- process.on('unhandledRejection', (reason) => {
190
- logger.error('Unhandled rejection:', reason);
191
- process.exit(1);
205
+ process.once('unhandledRejection', (reason) => {
206
+ handleFatalError('Unhandled rejection:', reason);
192
207
  });
193
208
  };
194
209
  void main();
@@ -1,59 +1,55 @@
1
- # memdb Instructions
1
+ # memdb INSTRUCTIONS
2
2
 
3
- > Guidance for the Agent: These instructions are available as a resource (`internal://instructions`) or prompt (`get-help`). Load them when you are unsure about tool usage.
3
+ > Available as resource `internal://instructions`. Load when unsure about tool usage.
4
4
 
5
- ## 1. Core Capability
5
+ ## CORE CAPABILITY
6
6
 
7
- - **Domain:** Local SQLite-backed memory store with vector-like text search and graph relationships.
8
- - **Primary Resources:** `Memory`, `Relationship`, `Stats`.
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
- ## 2. The "Golden Path" Workflows (Critical)
11
+ ## THE "GOLDEN PATH" WORKFLOWS (CRITICAL)
11
12
 
12
- _Describe the standard order of operations using ONLY tools that exist._
13
+ ### WORKFLOW A: Recall & Exploration
13
14
 
14
- ### Workflow A: Recall & Exploration
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
- 1. Call `search_memories` to find entry points by content/tags.
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
- ### Workflow B: Knowledge Management
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
- 1. Call `store_memory` (single) or `store_memories` (batch) to add context.
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
- ## 3. Tool Nuances & Gotchas
29
+ `search_memories` — `query` 1–1000 chars. Broaden if no results.
29
30
 
30
- _Do NOT repeat JSON schema. Focus on behavior and pitfalls._
31
+ `recall` `depth` 0–3 (default 1). Higher depth = more latency. 0 = no traversal.
31
32
 
32
- - **`search_memories`**
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
- - **`recall`**
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
- - **`store_memory` / `store_memories`**
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
- - **`update_memory`**
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
- - **`create_relationship`**
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
- ## 4. Error Handling Strategy
43
+ `delete_memory` / `delete_memories` / `delete_relationship` — Destructive. E_NOT_FOUND if missing.
56
44
 
57
- - **`E_NOT_FOUND`**: The hash doesn't exist. Re-run search/recall.
58
- - **`E_TIMEOUT`**: Operation took too long (>5s default). Reduce batch size or depth.
59
- - **`E_INVALID_ARG`**: Check inputs against schema (e.g. valid hashes).
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
- protocolLogSink?.(toProtocolLogLevel(level), formatted);
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;