midas-memory-mcp 0.0.3

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/dist/memory.js ADDED
@@ -0,0 +1,472 @@
1
+ /** Midas core — a faithful TypeScript port of `midas/memory.py`'s shipping behaviour:
2
+ * recall ranks by relevance × importance × recency with the scale-free parsimony floor
3
+ * (`minRelevanceRatio` 0.3, measured-safe), optional BM25+RRF hybrid (cached index),
4
+ * typed belief revision (supersession chains), policy-gated `capture`, budgeted lean
5
+ * `buildContext` with the dated "Today is …" anchor, and no-LLM selective forgetting. */
6
+ import { randomUUID } from "node:crypto";
7
+ import { BM25 } from "./bm25.js";
8
+ import { cosine, HashingEmbedder } from "./embeddings.js";
9
+ import { contentImportance } from "./importance.js";
10
+ import { InMemoryStore } from "./store.js";
11
+ import { MEMORY_PROVENANCE } from "./types.js";
12
+ export const RECENCY_HALF_LIFE_DAYS = 30.0;
13
+ export const DEFAULT_RECALL_LIMIT = 6;
14
+ export const DURABLE_KINDS = ["fact", "preference", "constraint"];
15
+ // Update/change cues for typed belief revision — same pattern set as the Python module.
16
+ const UPDATE_RE = /\b(actually|instead|now|currently|updated|changed|change|new|replaced|replace|taken over|take over|handed over|hand over|signed off|resigned|stepped down|stepped up|postponed|delayed|moved to|moved from|rescheduled|cancelled|no longer|not anymore|never)\b|\bwas\b.*\bbut now\b/i;
17
+ const SUPERSEDE_STOPWORDS = new Set([
18
+ "the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by", "is",
19
+ "are", "was", "were", "be", "been", "being", "have", "has", "had", "do", "does", "did", "will",
20
+ "would", "could", "should", "may", "might", "must", "can", "we", "our", "us", "it", "its",
21
+ "this", "that", "these", "those", "i", "you", "he", "she", "they", "them",
22
+ ]);
23
+ const ENTITY_STOPWORDS = new Set([
24
+ "project", "user", "client", "team", "ticket", "issue", "task", "feature", "release", "launch",
25
+ "date", "database", "service", "server", "environment",
26
+ "january", "february", "march", "april", "may", "june", "july", "august", "september",
27
+ "october", "november", "december",
28
+ ]);
29
+ const PROPER_ENTITY = /\b[A-Z][A-Za-z0-9_-]{2,}\b/g;
30
+ const PROVENANCE_RANK = {
31
+ planning: 0,
32
+ observation: 1,
33
+ action: 2,
34
+ user_confirmation: 3,
35
+ };
36
+ function contentWords(text) {
37
+ const out = new Set();
38
+ for (const w of text.split(/\s+/)) {
39
+ const lw = w.toLowerCase();
40
+ if (w.length > 3 && !SUPERSEDE_STOPWORDS.has(lw) && /^[a-z0-9]+$/i.test(w))
41
+ out.add(lw);
42
+ }
43
+ return out;
44
+ }
45
+ function properEntities(text) {
46
+ const out = new Set();
47
+ for (const m of text.matchAll(PROPER_ENTITY)) {
48
+ const lw = m[0].toLowerCase();
49
+ if (!ENTITY_STOPWORDS.has(lw))
50
+ out.add(lw);
51
+ }
52
+ return out;
53
+ }
54
+ function intersects(a, b) {
55
+ for (const x of a)
56
+ if (b.has(x))
57
+ return true;
58
+ return false;
59
+ }
60
+ export const DEFAULT_POLICY = {
61
+ minImportance: 2,
62
+ acceptKinds: ["note", "chat", "fact", "preference", "constraint", "mission"],
63
+ dedupThreshold: 0.95,
64
+ };
65
+ export class Memory {
66
+ store;
67
+ embedder;
68
+ wRelevance;
69
+ wImportance;
70
+ wRecency;
71
+ supersede;
72
+ supersedeThreshold;
73
+ supersedePool;
74
+ supersedeMargin;
75
+ supersedeLowerThreshold;
76
+ minRelevanceRatio;
77
+ importanceScorer;
78
+ policy;
79
+ now;
80
+ bm25Cache = null;
81
+ constructor(opts = {}) {
82
+ this.store = opts.store ?? new InMemoryStore();
83
+ this.embedder = opts.embedder ?? new HashingEmbedder();
84
+ this.wRelevance = opts.wRelevance ?? 1.0;
85
+ this.wImportance = opts.wImportance ?? 0.3;
86
+ this.wRecency = opts.wRecency ?? 0.2;
87
+ this.supersede = opts.supersede ?? false;
88
+ this.supersedeThreshold = opts.supersedeThreshold ?? 0.55;
89
+ this.supersedePool = opts.supersedePool ?? 20;
90
+ this.supersedeMargin = opts.supersedeMargin ?? 0.05;
91
+ this.supersedeLowerThreshold = opts.supersedeLowerThreshold ?? 0.3;
92
+ this.minRelevanceRatio = Math.max(0, opts.minRelevanceRatio ?? 0.3);
93
+ this.importanceScorer = opts.importanceScorer ?? null;
94
+ this.policy = opts.policy ?? DEFAULT_POLICY;
95
+ this.now = opts.now ?? (() => Date.now() / 1000);
96
+ }
97
+ remember(content, opts = {}) {
98
+ content = (content ?? "").trim();
99
+ if (!content)
100
+ throw new Error("Memory.remember: `content` is required");
101
+ const embedding = this.embedder.embed(content);
102
+ const ts = opts.createdAt ?? this.now();
103
+ const importance = this.deriveImportance(opts.importance ?? null, content);
104
+ const provenance = opts.provenance ?? "observation";
105
+ if (!MEMORY_PROVENANCE.includes(provenance)) {
106
+ throw new Error(`invalid memory provenance '${provenance}'`);
107
+ }
108
+ const record = {
109
+ id: randomUUID(),
110
+ content,
111
+ kind: opts.kind ?? "note",
112
+ importance,
113
+ source: opts.source ?? null,
114
+ provenance,
115
+ actor: opts.actor ?? null,
116
+ metadata: opts.metadata ?? {},
117
+ createdAt: ts,
118
+ updatedAt: ts,
119
+ embedding,
120
+ supersededBy: null,
121
+ };
122
+ this.store.put(record);
123
+ if (this.supersede)
124
+ this.maybeSupersede(record);
125
+ return record;
126
+ }
127
+ capture(content, opts = {}) {
128
+ content = (content ?? "").trim();
129
+ if (!content)
130
+ return { stored: false, reason: "empty content", record: null, importance: 0 };
131
+ const scorer = this.importanceScorer ?? contentImportance;
132
+ const importance = clampImportance(scorer(content));
133
+ const kind = opts.kind ?? "chat";
134
+ if (!this.policy.acceptKinds.includes(kind)) {
135
+ return { stored: false, reason: `kind '${kind}' not accepted by policy`, record: null, importance };
136
+ }
137
+ if (importance < this.policy.minImportance) {
138
+ return {
139
+ stored: false,
140
+ reason: `below relevance floor (importance ${importance} < ${this.policy.minImportance})`,
141
+ record: null,
142
+ importance,
143
+ };
144
+ }
145
+ if (this.policy.dedupThreshold > 0) {
146
+ const near = this.store.search(this.embedder.embed(content), { limit: 1 });
147
+ if (near.length && near[0][0] >= this.policy.dedupThreshold) {
148
+ const existing = near[0][1];
149
+ const upgraded = this.upgradeProvenance(existing, opts);
150
+ return {
151
+ stored: false,
152
+ reason: "near-duplicate of an existing memory" + (upgraded ? " (provenance upgraded)" : ""),
153
+ record: existing,
154
+ importance,
155
+ };
156
+ }
157
+ }
158
+ const record = this.remember(content, { ...opts, kind, importance });
159
+ return { stored: true, reason: "stored", record, importance };
160
+ }
161
+ upgradeProvenance(record, opts) {
162
+ const provenance = opts.provenance ?? "observation";
163
+ if (PROVENANCE_RANK[provenance] <= PROVENANCE_RANK[record.provenance])
164
+ return false;
165
+ record.provenance = provenance;
166
+ record.actor = opts.actor ?? record.actor;
167
+ record.source = opts.source ?? record.source;
168
+ record.metadata = { ...record.metadata, ...(opts.metadata ?? {}) };
169
+ record.updatedAt = opts.createdAt ?? this.now();
170
+ this.store.put(record);
171
+ return true;
172
+ }
173
+ deriveImportance(importance, content) {
174
+ if (importance !== null && importance !== undefined && importance > 0) {
175
+ return clampImportance(importance);
176
+ }
177
+ return clampImportance(this.importanceScorer ? this.importanceScorer(content) : 1);
178
+ }
179
+ recall(query, opts = {}) {
180
+ const limit = opts.limit ?? DEFAULT_RECALL_LIMIT;
181
+ const qEmb = this.embedder.embed(query);
182
+ const now = opts.now ?? this.now();
183
+ const predicate = (r) => {
184
+ if (opts.kind && r.kind !== opts.kind)
185
+ return false;
186
+ if (opts.minImportance != null && r.importance < opts.minImportance)
187
+ return false;
188
+ if (opts.metadataFilter) {
189
+ for (const [k, v] of Object.entries(opts.metadataFilter)) {
190
+ if (r.metadata?.[k] !== v)
191
+ return false;
192
+ }
193
+ }
194
+ return true;
195
+ };
196
+ const pool = opts.pool ?? Math.max(limit * 5, limit);
197
+ let candidates;
198
+ let relevanceMap = null;
199
+ if (opts.hybrid) {
200
+ [candidates, relevanceMap] = this.hybridCandidates(query, qEmb, pool, predicate);
201
+ }
202
+ else {
203
+ candidates = this.store.search(qEmb, { limit: pool, predicate });
204
+ }
205
+ if (this.supersede)
206
+ candidates = this.resolveCandidates(candidates);
207
+ if (!candidates.length)
208
+ return [];
209
+ const relevances = candidates.map(([cos, r]) => relevanceMap ? relevanceMap.get(r.id) ?? Math.max(0, cos) : Math.max(0, cos));
210
+ const ratio = opts.minRelevanceRatio ?? this.minRelevanceRatio;
211
+ const ratioFloor = ratio > 0 && relevances.length ? ratio * Math.max(...relevances) : null;
212
+ const hits = [];
213
+ for (let i = 0; i < candidates.length; i++) {
214
+ const [, record] = candidates[i];
215
+ const relevance = relevances[i];
216
+ if (opts.minRelevance != null && relevance < opts.minRelevance)
217
+ continue;
218
+ if (ratioFloor !== null && relevance < ratioFloor)
219
+ continue;
220
+ const importanceNorm = (record.importance - 1) / 4;
221
+ const ageDays = Math.max(0, (now - record.updatedAt) / 86_400);
222
+ const recency = Math.exp(-ageDays / RECENCY_HALF_LIFE_DAYS);
223
+ hits.push({
224
+ record,
225
+ score: this.wRelevance * relevance + this.wImportance * importanceNorm + this.wRecency * recency,
226
+ relevance,
227
+ importanceNorm,
228
+ recency,
229
+ });
230
+ }
231
+ hits.sort((a, b) => b.score - a.score || b.record.updatedAt - a.record.updatedAt);
232
+ return hits.slice(0, limit);
233
+ }
234
+ hybridCandidates(query, qEmb, pool, predicate) {
235
+ const semantic = this.store.search(qEmb, { limit: pool, predicate });
236
+ const records = this.store.all().filter(predicate);
237
+ const allowed = new Map(records.map((r) => [r.id, r]));
238
+ const bmScores = new Map([...this.bm25Index().scores(query)].filter(([rid]) => allowed.has(rid)));
239
+ const bmRanked = [...bmScores.keys()].sort((a, b) => bmScores.get(b) - bmScores.get(a));
240
+ const byId = new Map(semantic.map(([cos, r]) => [r.id, [cos, r]]));
241
+ for (const rid of bmRanked.slice(0, pool)) {
242
+ if (!byId.has(rid)) {
243
+ const rec = allowed.get(rid);
244
+ const cos = rec.embedding ? cosine(qEmb, rec.embedding) : 0;
245
+ byId.set(rid, [cos, rec]);
246
+ }
247
+ }
248
+ // Reciprocal-rank fusion (k=60), normalised to [0,1] — same as the Python implementation.
249
+ const K = 60;
250
+ const denseRank = new Map(semantic.map(([, r], i) => [r.id, i]));
251
+ const bmRank = new Map(bmRanked.map((rid, i) => [rid, i]));
252
+ const raw = new Map();
253
+ let top = 0;
254
+ for (const rid of byId.keys()) {
255
+ const d = denseRank.has(rid) ? 1 / (K + denseRank.get(rid) + 1) : 0;
256
+ const b = bmRank.has(rid) ? 1 / (K + bmRank.get(rid) + 1) : 0;
257
+ raw.set(rid, d + b);
258
+ top = Math.max(top, d + b);
259
+ }
260
+ const relevanceMap = new Map();
261
+ for (const [rid, s] of raw)
262
+ relevanceMap.set(rid, top > 0 ? s / top : 0);
263
+ return [[...byId.values()], relevanceMap];
264
+ }
265
+ bm25Index() {
266
+ const version = this.store.version;
267
+ if (!this.bm25Cache || this.bm25Cache[0] !== version) {
268
+ this.bm25Cache = [version, new BM25(this.store.all())];
269
+ }
270
+ return this.bm25Cache[1];
271
+ }
272
+ maybeSupersede(newRecord) {
273
+ if (!newRecord.embedding)
274
+ return;
275
+ if (newRecord.kind === "chat")
276
+ return; // chat never revises (the safe Python default)
277
+ const hasCue = UPDATE_RE.test(newRecord.content);
278
+ const effectiveThreshold = hasCue ? this.supersedeLowerThreshold : this.supersedeThreshold;
279
+ const candidates = this.store.search(newRecord.embedding, {
280
+ limit: this.supersedePool,
281
+ predicate: (r) => r.id !== newRecord.id && r.kind === newRecord.kind,
282
+ });
283
+ const byHead = new Map();
284
+ for (const [score, candidate] of candidates) {
285
+ if (score < effectiveThreshold)
286
+ continue;
287
+ const head = this.resolveHead(candidate);
288
+ if (head.id === newRecord.id || head.supersededBy !== null)
289
+ continue;
290
+ if (normalize(head.content) === normalize(newRecord.content))
291
+ continue;
292
+ const ne = properEntities(newRecord.content);
293
+ const he = properEntities(head.content);
294
+ if (ne.size && he.size && !intersects(ne, he))
295
+ continue;
296
+ if (score < this.supersedeThreshold && !intersects(contentWords(newRecord.content), contentWords(head.content))) {
297
+ continue;
298
+ }
299
+ const prev = byHead.get(head.id);
300
+ if (!prev || score > prev[0])
301
+ byHead.set(head.id, [score, head]);
302
+ }
303
+ if (!byHead.size)
304
+ return;
305
+ const ranked = [...byHead.values()].sort((a, b) => b[0] - a[0]);
306
+ if (ranked.length > 1 && ranked[0][0] - ranked[1][0] < this.supersedeMargin)
307
+ return;
308
+ const head = ranked[0][1];
309
+ head.supersededBy = newRecord.id;
310
+ this.store.put(head);
311
+ }
312
+ resolveHead(record) {
313
+ const seen = new Set();
314
+ let current = record;
315
+ while (current.supersededBy !== null && !seen.has(current.id)) {
316
+ seen.add(current.id);
317
+ const next = this.store.get(current.supersededBy);
318
+ if (!next)
319
+ break;
320
+ current = next;
321
+ }
322
+ return current;
323
+ }
324
+ resolveCandidates(candidates) {
325
+ const best = new Map();
326
+ for (const [score, record] of candidates) {
327
+ const head = this.resolveHead(record);
328
+ const prev = best.get(head.id);
329
+ if (!prev || score > prev[0])
330
+ best.set(head.id, [score, head]);
331
+ }
332
+ return [...best.values()];
333
+ }
334
+ buildContext(query, opts = {}) {
335
+ const budget = opts.tokenBudget ?? 512;
336
+ const now = opts.now ?? this.now();
337
+ const hits = this.recall(query, { ...opts, now });
338
+ let records = hits.map((h) => h.record);
339
+ if (this.supersede)
340
+ records = records.filter((r) => this.resolveHead(r).id === r.id);
341
+ const chosen = [];
342
+ let used = 0;
343
+ for (const record of records) {
344
+ const line = formatRecord(record, { maxChars: opts.maxRecordChars ?? 600, now });
345
+ const cost = approxTokens(line);
346
+ if (used + cost > budget)
347
+ break;
348
+ chosen.push([record, line, cost]);
349
+ used += cost;
350
+ }
351
+ if (!chosen.length)
352
+ return { text: "", records: [], tokens: 0 };
353
+ let body = chosen.map(([, line]) => line).join("\n");
354
+ if (opts.header !== false) {
355
+ let head = "# Memory - relevant context (use silently to stay consistent)";
356
+ head += `\n# Today is ${fmtDate(now)}`;
357
+ body = head + "\n" + body;
358
+ }
359
+ return { text: body, records: chosen.map(([r]) => r), tokens: used };
360
+ }
361
+ forget(recordId) {
362
+ const target = this.store.get(recordId);
363
+ if (!target)
364
+ return false;
365
+ for (const r of this.store.all()) {
366
+ if (r.supersededBy === recordId) {
367
+ r.supersededBy = target.supersededBy;
368
+ this.store.put(r);
369
+ }
370
+ }
371
+ return this.store.delete(recordId);
372
+ }
373
+ forgetMatching(query, opts = {}) {
374
+ const minRelevance = opts.minRelevance ?? 0.5;
375
+ const hits = this.recall(query, {
376
+ limit: opts.limit ?? 20,
377
+ minRelevance,
378
+ metadataFilter: opts.metadataFilter ?? null,
379
+ });
380
+ const matched = hits.filter((h) => h.relevance >= minRelevance).map((h) => h.record);
381
+ if (!opts.dryRun)
382
+ for (const r of matched)
383
+ this.forget(r.id);
384
+ return matched;
385
+ }
386
+ memoryValue(record, now) {
387
+ now = now ?? this.now();
388
+ const importanceNorm = (record.importance - 1) / 4;
389
+ const ageDays = Math.max(0, (now - record.updatedAt) / 86_400);
390
+ const recency = Math.exp(-ageDays / RECENCY_HALF_LIFE_DAYS);
391
+ const denom = this.wImportance + this.wRecency;
392
+ return denom > 0 ? (this.wImportance * importanceNorm + this.wRecency * recency) / denom : 0;
393
+ }
394
+ tier(record, now) {
395
+ now = now ?? this.now();
396
+ const ageDays = Math.max(0, (now - record.updatedAt) / 86_400);
397
+ if (ageDays <= 1)
398
+ return "short";
399
+ if (ageDays <= 7)
400
+ return "medium";
401
+ return "long";
402
+ }
403
+ forgetDecayed(opts = {}) {
404
+ const now = this.now();
405
+ const records = this.store.all();
406
+ const pointedTo = new Set(records.map((r) => r.supersededBy).filter((x) => x !== null));
407
+ const protectImportance = opts.protectImportance ?? 4;
408
+ const protectedRec = (r) => DURABLE_KINDS.includes(r.kind) ||
409
+ r.importance >= protectImportance ||
410
+ r.supersededBy !== null ||
411
+ pointedTo.has(r.id);
412
+ const candidates = records.filter((r) => !protectedRec(r));
413
+ const value = new Map(candidates.map((r) => [r.id, this.memoryValue(r, now)]));
414
+ candidates.sort((a, b) => value.get(a.id) - value.get(b.id));
415
+ const toDrop = [];
416
+ const dropped = new Set();
417
+ if (opts.minValue != null) {
418
+ for (const r of candidates) {
419
+ if (value.get(r.id) < opts.minValue) {
420
+ toDrop.push(r.id);
421
+ dropped.add(r.id);
422
+ }
423
+ }
424
+ }
425
+ if (opts.maxRecords != null) {
426
+ let surviving = records.length - dropped.size;
427
+ for (const r of candidates) {
428
+ if (surviving <= opts.maxRecords)
429
+ break;
430
+ if (dropped.has(r.id))
431
+ continue;
432
+ toDrop.push(r.id);
433
+ dropped.add(r.id);
434
+ surviving -= 1;
435
+ }
436
+ }
437
+ return toDrop.filter((rid) => this.store.delete(rid));
438
+ }
439
+ }
440
+ function normalize(text) {
441
+ return text.split(/\s+/).join(" ").toLowerCase();
442
+ }
443
+ function clampImportance(value) {
444
+ if (!Number.isFinite(value))
445
+ return 1;
446
+ return Math.max(1, Math.min(5, Math.round(value)));
447
+ }
448
+ export function fmtDate(ts) {
449
+ return new Date(ts * 1000).toISOString().slice(0, 10); // UTC, same as Python
450
+ }
451
+ function relativeAge(createdAt, now) {
452
+ const days = Math.floor((now - createdAt) / 86_400);
453
+ if (days <= 0)
454
+ return "today";
455
+ if (days < 14)
456
+ return `${days}d ago`;
457
+ if (days < 60)
458
+ return `${Math.floor(days / 7)}w ago`;
459
+ if (days < 730)
460
+ return `${Math.floor(days / 30)}mo ago`;
461
+ return `${Math.floor(days / 365)}y ago`;
462
+ }
463
+ export function formatRecord(record, opts = {}) {
464
+ let date = fmtDate(record.createdAt);
465
+ if (opts.now != null)
466
+ date = `${date}, ${relativeAge(record.createdAt, opts.now)}`;
467
+ const body = record.content.split(/\s+/).join(" ").slice(0, opts.maxChars ?? 600);
468
+ return `- [${record.kind} | ${date}] ${body}`;
469
+ }
470
+ export function approxTokens(text) {
471
+ return Math.max(1, Math.floor(text.split(/\s+/).length * 1.3));
472
+ }
@@ -0,0 +1,5 @@
1
+ /** The injected agent policy and capture parameters — verbatim the same text the Python MCP
2
+ * server injects (token-lean: 198 approx tokens), so behaviour is identical across servers. */
3
+ import type { MemoryPolicy } from "./memory.js";
4
+ export declare const AGENT_MEMORY_INSTRUCTIONS: string;
5
+ export declare function policySummary(policy: MemoryPolicy): string;
package/dist/policy.js ADDED
@@ -0,0 +1,19 @@
1
+ export const AGENT_MEMORY_INSTRUCTIONS = "Use Midas memory on every task. It is local, source-traceable, and uses no LLM at ingest/query.\n\n" +
2
+ "1) RECALL FIRST. Call `build_context` with the user's goal; use the returned facts silently. Use " +
3
+ "`recall`/`inspect_memory` only when you need audit details.\n\n" +
4
+ "2) CAPTURE DURABLE SIGNAL. Call `capture` for reusable facts, decisions, preferences, constraints, " +
5
+ "corrections, and completed actions. Skip pure small talk. Midas scores, dedups, and rejects trivia, " +
6
+ "so capture can be brief and does not need an LLM. Set kind/provenance accurately; use " +
7
+ 'provenance="user_confirmation" only for explicit user confirmation.\n\n' +
8
+ "3) GUARD ACTIONS. Memory may guide planning, but before external/destructive actions based on memory, " +
9
+ "call `check_memory_use`. If it is not allowed, ask the user to confirm in this turn.\n\n" +
10
+ "4) FORGET ON REQUEST. Use `forget_matching` as a dry-run first, show matches, then repeat with " +
11
+ "dry_run=false after confirmation.\n\n" +
12
+ "Midas stores verbatim source records and bounds memory automatically; compact context is for cheap " +
13
+ "reader prompts, audit tools are for traceability.";
14
+ export function policySummary(policy) {
15
+ return (`keep items scoring importance >= ${policy.minImportance}/5, ` +
16
+ `kinds ${JSON.stringify(policy.acceptKinds)}, ` +
17
+ `skip near-duplicates (cosine >= ${policy.dedupThreshold}); ` +
18
+ "guard external/destructive actions to user_confirmation provenance");
19
+ }
@@ -0,0 +1,35 @@
1
+ import type { MemoryRecord } from "./types.js";
2
+ export type Predicate = (record: MemoryRecord) => boolean;
3
+ export declare class InMemoryStore {
4
+ protected records: Map<string, MemoryRecord>;
5
+ /** Monotonic change counter — lets Memory cache derived indexes (BM25) cheaply. */
6
+ version: number;
7
+ put(record: MemoryRecord): void;
8
+ get(recordId: string): MemoryRecord | null;
9
+ delete(recordId: string): boolean;
10
+ all(): MemoryRecord[];
11
+ clear(): void;
12
+ search(embedding: Float32Array, opts: {
13
+ limit: number;
14
+ predicate?: Predicate;
15
+ }): Array<[number, MemoryRecord]>;
16
+ }
17
+ export declare class SQLiteStore extends InMemoryStore {
18
+ private db;
19
+ private dataVersion;
20
+ constructor(path: string);
21
+ private currentDataVersion;
22
+ /** Reload the mirror when ANOTHER connection (e.g. the Python server) wrote the file. */
23
+ private refreshIfStale;
24
+ private load;
25
+ put(record: MemoryRecord): void;
26
+ get(recordId: string): MemoryRecord | null;
27
+ all(): MemoryRecord[];
28
+ search(embedding: Float32Array, opts: {
29
+ limit: number;
30
+ predicate?: Predicate;
31
+ }): Array<[number, MemoryRecord]>;
32
+ delete(recordId: string): boolean;
33
+ clear(): void;
34
+ close(): void;
35
+ }
package/dist/store.js ADDED
@@ -0,0 +1,159 @@
1
+ /** Stores — `InMemoryStore` (exact cosine scan) and `SQLiteStore` (node:sqlite persistence).
2
+ *
3
+ * SQLiteStore uses the SAME table schema and float32-blob encoding as the Python
4
+ * `midas/sqlite_store.py`, and the same `PRAGMA data_version` staleness probe — so a TypeScript
5
+ * MCP server and a Python one can share a single memory file, live, in both directions. */
6
+ import { DatabaseSync } from "node:sqlite";
7
+ import { cosine } from "./embeddings.js";
8
+ export class InMemoryStore {
9
+ records = new Map();
10
+ /** Monotonic change counter — lets Memory cache derived indexes (BM25) cheaply. */
11
+ version = 0;
12
+ put(record) {
13
+ this.records.set(record.id, record);
14
+ this.version += 1;
15
+ }
16
+ get(recordId) {
17
+ return this.records.get(recordId) ?? null;
18
+ }
19
+ delete(recordId) {
20
+ const existed = this.records.delete(recordId);
21
+ if (existed)
22
+ this.version += 1;
23
+ return existed;
24
+ }
25
+ all() {
26
+ return [...this.records.values()];
27
+ }
28
+ clear() {
29
+ this.records.clear();
30
+ this.version += 1;
31
+ }
32
+ search(embedding, opts) {
33
+ const scored = [];
34
+ for (const r of this.records.values()) {
35
+ if (r.embedding === null)
36
+ continue;
37
+ if (opts.predicate && !opts.predicate(r))
38
+ continue;
39
+ scored.push([cosine(embedding, r.embedding), r]);
40
+ }
41
+ scored.sort((a, b) => b[0] - a[0]);
42
+ return scored.slice(0, opts.limit);
43
+ }
44
+ }
45
+ export class SQLiteStore extends InMemoryStore {
46
+ db;
47
+ dataVersion;
48
+ constructor(path) {
49
+ super();
50
+ this.db = new DatabaseSync(path);
51
+ this.db.exec("PRAGMA journal_mode=WAL");
52
+ this.db.exec(`
53
+ CREATE TABLE IF NOT EXISTS memories (
54
+ id TEXT PRIMARY KEY,
55
+ content TEXT NOT NULL,
56
+ kind TEXT NOT NULL,
57
+ importance INTEGER NOT NULL,
58
+ source TEXT,
59
+ provenance TEXT NOT NULL DEFAULT 'observation',
60
+ actor TEXT,
61
+ metadata_json TEXT NOT NULL DEFAULT '{}',
62
+ created_at REAL NOT NULL,
63
+ updated_at REAL NOT NULL,
64
+ superseded_by TEXT,
65
+ embedding BLOB
66
+ )
67
+ `);
68
+ this.load();
69
+ this.dataVersion = this.currentDataVersion();
70
+ }
71
+ currentDataVersion() {
72
+ const row = this.db.prepare("PRAGMA data_version").get();
73
+ return Number(row.data_version);
74
+ }
75
+ /** Reload the mirror when ANOTHER connection (e.g. the Python server) wrote the file. */
76
+ refreshIfStale() {
77
+ const v = this.currentDataVersion();
78
+ if (v === this.dataVersion)
79
+ return;
80
+ this.dataVersion = v;
81
+ this.records.clear();
82
+ this.version += 1;
83
+ this.load();
84
+ }
85
+ load() {
86
+ const rows = this.db
87
+ .prepare("SELECT id, content, kind, importance, source, provenance, actor, metadata_json, " +
88
+ "created_at, updated_at, superseded_by, embedding FROM memories")
89
+ .all();
90
+ for (const row of rows) {
91
+ super.put(rowToRecord(row));
92
+ }
93
+ }
94
+ put(record) {
95
+ this.refreshIfStale();
96
+ super.put(record);
97
+ const blob = record.embedding
98
+ ? new Uint8Array(record.embedding.buffer.slice(0), record.embedding.byteOffset, record.embedding.byteLength)
99
+ : null;
100
+ this.db
101
+ .prepare(`INSERT INTO memories (id, content, kind, importance, source, provenance, actor,
102
+ metadata_json, created_at, updated_at, superseded_by, embedding)
103
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
104
+ ON CONFLICT(id) DO UPDATE SET
105
+ content=excluded.content, kind=excluded.kind, importance=excluded.importance,
106
+ source=excluded.source, provenance=excluded.provenance, actor=excluded.actor,
107
+ metadata_json=excluded.metadata_json,
108
+ created_at=excluded.created_at, updated_at=excluded.updated_at,
109
+ superseded_by=excluded.superseded_by, embedding=excluded.embedding`)
110
+ .run(record.id, record.content, record.kind, record.importance, record.source, record.provenance, record.actor, JSON.stringify(record.metadata ?? {}), record.createdAt, record.updatedAt, record.supersededBy, blob);
111
+ }
112
+ get(recordId) {
113
+ this.refreshIfStale();
114
+ return super.get(recordId);
115
+ }
116
+ all() {
117
+ this.refreshIfStale();
118
+ return super.all();
119
+ }
120
+ search(embedding, opts) {
121
+ this.refreshIfStale();
122
+ return super.search(embedding, opts);
123
+ }
124
+ delete(recordId) {
125
+ this.refreshIfStale();
126
+ const existed = super.delete(recordId);
127
+ if (existed)
128
+ this.db.prepare("DELETE FROM memories WHERE id = ?").run(recordId);
129
+ return existed;
130
+ }
131
+ clear() {
132
+ super.clear();
133
+ this.db.exec("DELETE FROM memories");
134
+ }
135
+ close() {
136
+ this.db.close();
137
+ }
138
+ }
139
+ function rowToRecord(row) {
140
+ let embedding = null;
141
+ if (row.embedding && row.embedding.byteLength >= 4) {
142
+ const bytes = new Uint8Array(row.embedding); // copy → aligned buffer
143
+ embedding = new Float32Array(bytes.buffer, 0, Math.floor(bytes.byteLength / 4));
144
+ }
145
+ return {
146
+ id: row.id,
147
+ content: row.content,
148
+ kind: row.kind,
149
+ importance: row.importance,
150
+ source: row.source,
151
+ provenance: (row.provenance || "observation"),
152
+ actor: row.actor,
153
+ metadata: row.metadata_json ? JSON.parse(row.metadata_json) : {},
154
+ createdAt: row.created_at,
155
+ updatedAt: row.updated_at,
156
+ supersededBy: row.superseded_by,
157
+ embedding,
158
+ };
159
+ }