@batalabs/virlow-mcp-core 3.18.0 → 3.68.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.
Files changed (73) hide show
  1. package/LICENSE +8 -18
  2. package/README.md +1 -1
  3. package/dist/api.d.ts +32 -0
  4. package/dist/api.d.ts.map +1 -1
  5. package/dist/api.js +38 -5
  6. package/dist/api.js.map +1 -1
  7. package/dist/attrs-extract.d.ts +48 -0
  8. package/dist/attrs-extract.d.ts.map +1 -0
  9. package/dist/attrs-extract.js +153 -0
  10. package/dist/attrs-extract.js.map +1 -0
  11. package/dist/attrs.d.ts +445 -0
  12. package/dist/attrs.d.ts.map +1 -0
  13. package/dist/attrs.js +604 -0
  14. package/dist/attrs.js.map +1 -0
  15. package/dist/cjs/api.d.ts +32 -0
  16. package/dist/cjs/api.js +38 -5
  17. package/dist/cjs/attrs-extract.d.ts +47 -0
  18. package/dist/cjs/attrs-extract.js +159 -0
  19. package/dist/cjs/attrs.d.ts +444 -0
  20. package/dist/cjs/attrs.js +635 -0
  21. package/dist/cjs/index.d.ts +5 -1
  22. package/dist/cjs/index.js +22 -1
  23. package/dist/cjs/memories.d.ts +79 -1
  24. package/dist/cjs/memories.js +249 -15
  25. package/dist/cjs/memory-graph.d.ts +105 -0
  26. package/dist/cjs/memory-graph.js +270 -0
  27. package/dist/cjs/memory-note.d.ts +57 -0
  28. package/dist/cjs/memory-note.js +174 -10
  29. package/dist/cjs/memory-secrets.d.ts +19 -0
  30. package/dist/cjs/memory-secrets.js +108 -0
  31. package/dist/cjs/notes.d.ts +208 -2
  32. package/dist/cjs/notes.js +721 -12
  33. package/dist/http/index.d.ts +4 -0
  34. package/dist/http/index.d.ts.map +1 -0
  35. package/dist/http/index.js +3 -0
  36. package/dist/http/index.js.map +1 -0
  37. package/dist/http/local-origin.d.ts +61 -0
  38. package/dist/http/local-origin.d.ts.map +1 -0
  39. package/dist/http/local-origin.js +92 -0
  40. package/dist/http/local-origin.js.map +1 -0
  41. package/dist/http/server.d.ts +39 -0
  42. package/dist/http/server.d.ts.map +1 -0
  43. package/dist/http/server.js +131 -0
  44. package/dist/http/server.js.map +1 -0
  45. package/dist/index.d.ts +5 -1
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/memories.d.ts +79 -1
  50. package/dist/memories.d.ts.map +1 -1
  51. package/dist/memories.js +249 -15
  52. package/dist/memories.js.map +1 -1
  53. package/dist/memory-graph.d.ts +106 -0
  54. package/dist/memory-graph.d.ts.map +1 -0
  55. package/dist/memory-graph.js +258 -0
  56. package/dist/memory-graph.js.map +1 -0
  57. package/dist/memory-note.d.ts +57 -0
  58. package/dist/memory-note.d.ts.map +1 -1
  59. package/dist/memory-note.js +170 -10
  60. package/dist/memory-note.js.map +1 -1
  61. package/dist/memory-secrets.d.ts +20 -0
  62. package/dist/memory-secrets.d.ts.map +1 -0
  63. package/dist/memory-secrets.js +103 -0
  64. package/dist/memory-secrets.js.map +1 -0
  65. package/dist/notes.d.ts +208 -2
  66. package/dist/notes.d.ts.map +1 -1
  67. package/dist/notes.js +722 -13
  68. package/dist/notes.js.map +1 -1
  69. package/dist/tools.d.ts +8 -2
  70. package/dist/tools.d.ts.map +1 -1
  71. package/dist/tools.js +465 -10
  72. package/dist/tools.js.map +1 -1
  73. package/package.json +21 -5
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.memoriesAllowed = exports.MEMORIES_DISABLED_MESSAGE = exports.EmbeddingCache = exports.filteredNotice = exports.computeExposure = exports.resolveMemoryTree = exports.ensureNamespace = exports.MEMORIES_ROOT_NAME = exports.toCodeEnvelope = exports.serializeMemoryBody = exports.parseMemoryNote = exports.mergeMeta = exports.MEMORY_NOTE_TYPE = exports.SYNC_PAGE_LIMIT = exports.MemoryStore = exports.SEARCH_SCAN_CAP = exports.NotesService = exports.Vault = exports.LockedError = exports.VirlowApi = exports.ApiError = void 0;
3
+ exports.htmlToText = exports.extractLinkIds = exports.buildTitleMatcher = exports.withTags = exports.updateProp = exports.tagsFromAttrs = exports.sortedProps = exports.reorderProps = exports.removeProp = exports.newPropId = exports.encodeSchema = exports.encodeAttrs = exports.emptySchema = exports.emptyAttrs = exports.defineProp = exports.decodeSchema = exports.decodeAttrs = exports.TAGS_PROP = exports.ATTRS_MAX_BYTES = exports.memoriesAllowed = exports.MEMORIES_DISABLED_MESSAGE = exports.EmbeddingCache = exports.filteredNotice = exports.computeExposure = exports.resolveMemoryTree = exports.ensureNamespace = exports.MEMORIES_ROOT_NAME = exports.toCodeEnvelope = exports.serializeMemoryBody = exports.parseMemoryNote = exports.mergeMeta = exports.MEMORY_NOTE_TYPE = exports.SYNC_PAGE_LIMIT = exports.MemoryStore = exports.SEARCH_SCAN_CAP = exports.NotesService = exports.Vault = exports.LockedError = exports.VirlowApi = exports.ApiError = void 0;
4
4
  var api_js_1 = require("./api.js");
5
5
  Object.defineProperty(exports, "ApiError", { enumerable: true, get: function () { return api_js_1.ApiError; } });
6
6
  Object.defineProperty(exports, "VirlowApi", { enumerable: true, get: function () { return api_js_1.VirlowApi; } });
@@ -31,6 +31,27 @@ Object.defineProperty(exports, "EmbeddingCache", { enumerable: true, get: functi
31
31
  var memories_enabled_js_1 = require("./memories-enabled.js");
32
32
  Object.defineProperty(exports, "MEMORIES_DISABLED_MESSAGE", { enumerable: true, get: function () { return memories_enabled_js_1.MEMORIES_DISABLED_MESSAGE; } });
33
33
  Object.defineProperty(exports, "memoriesAllowed", { enumerable: true, get: function () { return memories_enabled_js_1.memoriesAllowed; } });
34
+ var attrs_js_1 = require("./attrs.js");
35
+ Object.defineProperty(exports, "ATTRS_MAX_BYTES", { enumerable: true, get: function () { return attrs_js_1.ATTRS_MAX_BYTES; } });
36
+ Object.defineProperty(exports, "TAGS_PROP", { enumerable: true, get: function () { return attrs_js_1.TAGS_PROP; } });
37
+ Object.defineProperty(exports, "decodeAttrs", { enumerable: true, get: function () { return attrs_js_1.decodeAttrs; } });
38
+ Object.defineProperty(exports, "decodeSchema", { enumerable: true, get: function () { return attrs_js_1.decodeSchema; } });
39
+ Object.defineProperty(exports, "defineProp", { enumerable: true, get: function () { return attrs_js_1.defineProp; } });
40
+ Object.defineProperty(exports, "emptyAttrs", { enumerable: true, get: function () { return attrs_js_1.emptyAttrs; } });
41
+ Object.defineProperty(exports, "emptySchema", { enumerable: true, get: function () { return attrs_js_1.emptySchema; } });
42
+ Object.defineProperty(exports, "encodeAttrs", { enumerable: true, get: function () { return attrs_js_1.encodeAttrs; } });
43
+ Object.defineProperty(exports, "encodeSchema", { enumerable: true, get: function () { return attrs_js_1.encodeSchema; } });
44
+ Object.defineProperty(exports, "newPropId", { enumerable: true, get: function () { return attrs_js_1.newPropId; } });
45
+ Object.defineProperty(exports, "removeProp", { enumerable: true, get: function () { return attrs_js_1.removeProp; } });
46
+ Object.defineProperty(exports, "reorderProps", { enumerable: true, get: function () { return attrs_js_1.reorderProps; } });
47
+ Object.defineProperty(exports, "sortedProps", { enumerable: true, get: function () { return attrs_js_1.sortedProps; } });
48
+ Object.defineProperty(exports, "tagsFromAttrs", { enumerable: true, get: function () { return attrs_js_1.tagsFromAttrs; } });
49
+ Object.defineProperty(exports, "updateProp", { enumerable: true, get: function () { return attrs_js_1.updateProp; } });
50
+ Object.defineProperty(exports, "withTags", { enumerable: true, get: function () { return attrs_js_1.withTags; } });
51
+ var attrs_extract_js_1 = require("./attrs-extract.js");
52
+ Object.defineProperty(exports, "buildTitleMatcher", { enumerable: true, get: function () { return attrs_extract_js_1.buildTitleMatcher; } });
53
+ Object.defineProperty(exports, "extractLinkIds", { enumerable: true, get: function () { return attrs_extract_js_1.extractLinkIds; } });
54
+ Object.defineProperty(exports, "htmlToText", { enumerable: true, get: function () { return attrs_extract_js_1.htmlToText; } });
34
55
  /**
35
56
  * The MCP tool surface is NOT exported here. It lives at the `/tools` subpath,
36
57
  * which is ESM-only, because it depends on @modelcontextprotocol/sdk — an
@@ -2,6 +2,8 @@ import { type Embedder } from '@batalabs/virlow-memory';
2
2
  import { type VirlowApi } from './api.js';
3
3
  import type { EmbeddingCacheLike } from './embedding-cache.js';
4
4
  import { type MemoryMeta } from './memory-note.js';
5
+ import { type MemoryGraph } from './memory-graph.js';
6
+ import type { MemoryRelation } from './memory-note.js';
5
7
  import type { Vault } from './vault.js';
6
8
  /**
7
9
  * Page size for the sync walk over each memory folder.
@@ -33,6 +35,13 @@ export interface SimilarMemory {
33
35
  text: string;
34
36
  similarity: number;
35
37
  }
38
+ /** A fact the new memory ended: it said something single-valued (who leads
39
+ * a project, where someone works) that the new one replaces. */
40
+ export interface ClosedFact {
41
+ id: string;
42
+ label: string;
43
+ relation: MemoryRelation;
44
+ }
36
45
  export type SyncResult = {
37
46
  added: number;
38
47
  updated: number;
@@ -66,10 +75,79 @@ export declare class MemoryStore {
66
75
  action: 'stored' | 'updated';
67
76
  id: string;
68
77
  similar: SimilarMemory[];
78
+ closed: ClosedFact[];
69
79
  }>;
70
- search(query: string, namespace?: string, limit?: number): Promise<Array<Omit<CachedMemory, 'embedding' | 'folderId' | 'updatedAt'> & {
80
+ /**
81
+ * Ends the facts a new memory replaces. Each superseded relation gets a
82
+ * `to` date (the new fact's start); a memory whose relations have all
83
+ * ended gets `valid_to` and `superseded_by`. Nothing is deleted, so "who
84
+ * led Atlas before Sam?" stays answerable.
85
+ */
86
+ private closeSuperseded;
87
+ /** The graph over what is cached now. Cheap: rebuilt from the notes on
88
+ * demand, never stored. */
89
+ graph(namespace?: string): MemoryGraph;
90
+ /** New names spelled the way existing memories already spell them, and
91
+ * predicates in snake_case, so one entity does not split in two. */
92
+ private canonicalize;
93
+ /**
94
+ * Hybrid recall: similarity, then the graph (memories about an entity the
95
+ * query names, and one relation out from it) and recency. Current facts
96
+ * only, unless `includeHistory`, or as they stood on `asOf`.
97
+ */
98
+ search(query: string, namespace?: string, limit?: number, opts?: {
99
+ asOf?: string;
100
+ includeHistory?: boolean;
101
+ }): Promise<Array<Omit<CachedMemory, 'embedding' | 'folderId' | 'updatedAt'> & {
71
102
  similarity: number;
72
103
  }>>;
104
+ /** Everything known about one entity: its current and past relations and
105
+ * the facts behind them. Null when no memory names it. */
106
+ recallEntity(name: string, namespace?: string): Promise<{
107
+ name: string;
108
+ current: CachedMemory[];
109
+ past: CachedMemory[];
110
+ relations: {
111
+ memoryId: string;
112
+ s: string;
113
+ p: string;
114
+ o: string;
115
+ from?: string;
116
+ to?: string;
117
+ }[];
118
+ } | null>;
119
+ /** Adds entities and relations to an existing memory: how a memory saved
120
+ * before the graph existed gets linked. Also ends what it replaces. */
121
+ link(id: string, add: {
122
+ about?: string[];
123
+ relations?: MemoryRelation[];
124
+ }): Promise<ClosedFact[]>;
125
+ /** One entity spelled two ways becomes one: every memory naming `from`
126
+ * is rewritten to name `into`. Returns how many memories changed. */
127
+ mergeEntities(from: string, into: string): Promise<number>;
128
+ /**
129
+ * What to load at the start of a session: the entities this namespace
130
+ * talks about most, and its most recent current facts, within `limit`
131
+ * facts. Global memories are included alongside a project's.
132
+ */
133
+ context(namespace?: string, limit?: number): Promise<{
134
+ entities: {
135
+ name: string;
136
+ facts: number;
137
+ }[];
138
+ facts: CachedMemory[];
139
+ }>;
140
+ /** Findings for a tidy pass: unlinked memories, likely duplicate names and
141
+ * single-valued facts holding two values at once. */
142
+ health(namespace?: string): Promise<{
143
+ unlinked: string[];
144
+ possibleDuplicates: [string, string][];
145
+ contradictions: {
146
+ subject: string;
147
+ predicate: string;
148
+ objects: string[];
149
+ }[];
150
+ }>;
73
151
  /** Requires a prior sync() in the same unlock to have populated the cache. */
74
152
  list(namespace?: string): CachedMemory[];
75
153
  update(id: string, changes: {
@@ -6,6 +6,8 @@ const virlow_memory_1 = require("@batalabs/virlow-memory");
6
6
  const api_js_1 = require("./api.js");
7
7
  const memory_note_js_1 = require("./memory-note.js");
8
8
  const memory_tree_js_1 = require("./memory-tree.js");
9
+ const memory_graph_js_1 = require("./memory-graph.js");
10
+ const memory_secrets_js_1 = require("./memory-secrets.js");
9
11
  /**
10
12
  * Page size for the sync walk over each memory folder.
11
13
  *
@@ -20,6 +22,25 @@ const memory_tree_js_1 = require("./memory-tree.js");
20
22
  * lowering it only costs round trips.
21
23
  */
22
24
  exports.SYNC_PAGE_LIMIT = 50;
25
+ /** Today as YYYY-MM-DD, the default start of a fact learned now. */
26
+ function today() {
27
+ return new Date().toISOString().slice(0, 10);
28
+ }
29
+ /** Merge two relation lists, one entry per subject, predicate and object. */
30
+ function unionRelations(a = [], b = []) {
31
+ const byKey = new Map();
32
+ for (const r of [...a, ...b]) {
33
+ byKey.set(`${(0, memory_graph_js_1.entityKey)(r.s)}|${(0, memory_graph_js_1.normalizePredicate)(r.p)}|${(0, memory_graph_js_1.entityKey)(r.o)}`, r);
34
+ }
35
+ return [...byKey.values()];
36
+ }
37
+ function unionNames(a = [], b = []) {
38
+ const byKey = new Map();
39
+ for (const n of [...a, ...b])
40
+ if (!byKey.has((0, memory_graph_js_1.entityKey)(n)))
41
+ byKey.set((0, memory_graph_js_1.entityKey)(n), n);
42
+ return [...byKey.values()];
43
+ }
23
44
  const round3 = (n) => Math.round(n * 1000) / 1000;
24
45
  class MemoryStore {
25
46
  api;
@@ -156,17 +177,32 @@ class MemoryStore {
156
177
  }
157
178
  add(label, text, namespace, meta = {}) {
158
179
  return this.enqueue(async () => {
180
+ (0, memory_secrets_js_1.assertNoSecret)(label, text, ...(meta.relations ?? []).map((r) => `${r.s} ${r.p} ${r.o}`));
159
181
  await this.sync();
160
182
  const key = this.requireSession();
161
183
  const startEpoch = this.epoch;
184
+ meta = this.canonicalize(meta);
162
185
  const embedding = await this.embedder.embed(embeddingInput(label, text));
163
186
  const candidates = this.candidatesFor(namespace);
164
- const hit = (0, virlow_memory_1.findDuplicate)(embedding, candidates);
187
+ // A near-identical sentence that changes a single-valued fact ("Sam
188
+ // leads Atlas" after "Dana leads Atlas") is not a restatement: merging
189
+ // would overwrite the old fact and lose its history. It is stored as a
190
+ // new memory, which then ends the old one.
191
+ const found = (0, virlow_memory_1.findDuplicate)(embedding, candidates);
192
+ const contradicts = found !== null &&
193
+ found !== undefined &&
194
+ (meta.relations ?? []).some((r) => (0, memory_graph_js_1.supersededBy)((0, memory_graph_js_1.buildGraph)([this.cache.get(found.id)]), r, Date.now()).length > 0);
195
+ const hit = contradicts ? null : found;
165
196
  if (hit) {
166
197
  // Same memory said again: fold it into the existing note rather than
167
- // accumulating near-identical copies. Its metadata carries forward.
198
+ // accumulating near-identical copies. Its metadata carries forward,
199
+ // and its entities and relations gain whatever the new one names.
168
200
  const existing = this.cache.get(hit.id);
169
201
  const merged = (0, memory_note_js_1.mergeMeta)(existing.meta, meta);
202
+ if (meta.about)
203
+ merged.about = unionNames(existing.meta.about, meta.about);
204
+ if (meta.relations)
205
+ merged.relations = unionRelations(existing.meta.relations, meta.relations);
170
206
  const row = await this.writeNote(key, hit.id, label, text, merged);
171
207
  if (this.epoch === startEpoch) {
172
208
  this.cache.set(hit.id, {
@@ -180,7 +216,7 @@ class MemoryStore {
180
216
  this.embeddings?.set(hit.id, row.updatedAt, embedding);
181
217
  await this.saveEmbeddings(key, startEpoch);
182
218
  }
183
- return { action: 'updated', id: hit.id, similar: [] };
219
+ return { action: 'updated', id: hit.id, similar: [], closed: [] };
184
220
  }
185
221
  const tree = await this.requireTree();
186
222
  const folderId = namespace !== undefined
@@ -191,6 +227,8 @@ class MemoryStore {
191
227
  source: meta.source ?? 'virlow-mcp',
192
228
  created: meta.created ?? new Date().toISOString(),
193
229
  };
230
+ // The graph as it stood before this memory, to find what it replaces.
231
+ const before = this.graph();
194
232
  const row = await this.writeNote(key, null, label, text, stamped, folderId);
195
233
  if (this.epoch === startEpoch) {
196
234
  this.cache.set(row.id, {
@@ -216,26 +254,221 @@ class MemoryStore {
216
254
  similarity: round3(similarity),
217
255
  };
218
256
  });
219
- return { action: 'stored', id: row.id, similar };
257
+ const closed = await this.closeSuperseded(key, startEpoch, before, row.id, stamped);
258
+ return { action: 'stored', id: row.id, similar, closed };
220
259
  });
221
260
  }
222
- async search(query, namespace, limit = 10) {
261
+ /**
262
+ * Ends the facts a new memory replaces. Each superseded relation gets a
263
+ * `to` date (the new fact's start); a memory whose relations have all
264
+ * ended gets `valid_to` and `superseded_by`. Nothing is deleted, so "who
265
+ * led Atlas before Sam?" stays answerable.
266
+ */
267
+ async closeSuperseded(key, startEpoch, graph, newId, meta) {
268
+ const closed = [];
269
+ const byMemory = new Map();
270
+ for (const relation of meta.relations ?? []) {
271
+ const start = relation.from ?? meta.validFrom ?? today();
272
+ const at = Date.parse(start.length === 10 ? `${start}T00:00:00Z` : start);
273
+ for (const edge of (0, memory_graph_js_1.supersededBy)(graph, relation, Number.isNaN(at) ? Date.now() : at)) {
274
+ if (edge.memoryId === newId)
275
+ continue;
276
+ const list = byMemory.get(edge.memoryId) ?? [];
277
+ list.push({ s: edge.s, p: edge.p, o: edge.o, ...(edge.from && { from: edge.from }), to: start });
278
+ byMemory.set(edge.memoryId, list);
279
+ }
280
+ }
281
+ for (const [memoryId, ended] of byMemory) {
282
+ const memory = this.cache.get(memoryId);
283
+ if (!memory)
284
+ continue;
285
+ const relations = (memory.meta.relations ?? []).map((r) => {
286
+ const hit = ended.find((e) => (0, memory_graph_js_1.entityKey)(e.s) === (0, memory_graph_js_1.entityKey)(r.s) &&
287
+ (0, memory_graph_js_1.normalizePredicate)(e.p) === (0, memory_graph_js_1.normalizePredicate)(r.p) &&
288
+ (0, memory_graph_js_1.entityKey)(e.o) === (0, memory_graph_js_1.entityKey)(r.o));
289
+ return hit ? { ...r, to: hit.to } : r;
290
+ });
291
+ const next = { ...memory.meta, relations };
292
+ if (relations.every((r) => r.to)) {
293
+ next.validTo = ended[0].to;
294
+ next.supersededBy = newId;
295
+ }
296
+ const row = await this.writeNote(key, memoryId, memory.label, memory.text, next);
297
+ if (this.epoch === startEpoch) {
298
+ this.cache.set(memoryId, { ...memory, meta: next, updatedAt: row.updatedAt });
299
+ this.embeddings?.set(memoryId, row.updatedAt, memory.embedding);
300
+ }
301
+ for (const relation of ended)
302
+ closed.push({ id: memoryId, label: memory.label, relation });
303
+ }
304
+ if (closed.length > 0)
305
+ await this.saveEmbeddings(key, startEpoch);
306
+ return closed;
307
+ }
308
+ /** The graph over what is cached now. Cheap: rebuilt from the notes on
309
+ * demand, never stored. */
310
+ graph(namespace) {
311
+ return (0, memory_graph_js_1.buildGraph)([...this.cache.values()].filter((m) => namespace === undefined || m.namespace === namespace || m.namespace === undefined));
312
+ }
313
+ /** New names spelled the way existing memories already spell them, and
314
+ * predicates in snake_case, so one entity does not split in two. */
315
+ canonicalize(meta) {
316
+ if (!meta.about && !meta.relations)
317
+ return meta;
318
+ const graph = this.graph();
319
+ const name = (n) => graph.entities.get((0, memory_graph_js_1.entityKey)(n))?.name ?? n.trim();
320
+ const next = { ...meta };
321
+ if (meta.about)
322
+ next.about = unionNames([], meta.about.map(name));
323
+ if (meta.relations) {
324
+ next.relations = meta.relations.map((r) => ({
325
+ ...r,
326
+ s: name(r.s),
327
+ p: (0, memory_graph_js_1.normalizePredicate)(r.p),
328
+ o: graph.entities.has((0, memory_graph_js_1.entityKey)(r.o)) ? name(r.o) : r.o.trim(),
329
+ }));
330
+ // Everything a relation names is also what the memory is about.
331
+ next.about = unionNames(next.about, next.relations.flatMap((r) => [r.s, ...(graph.entities.has((0, memory_graph_js_1.entityKey)(r.o)) || /^[A-Z0-9]/.test(r.o) ? [r.o] : [])]));
332
+ }
333
+ return next;
334
+ }
335
+ /**
336
+ * Hybrid recall: similarity, then the graph (memories about an entity the
337
+ * query names, and one relation out from it) and recency. Current facts
338
+ * only, unless `includeHistory`, or as they stood on `asOf`.
339
+ */
340
+ async search(query, namespace, limit = 10, opts = {}) {
223
341
  await this.sync();
224
342
  this.requireSession();
225
343
  const embedding = await this.embedder.embed(query);
226
- const ranked = (0, virlow_memory_1.rankBySimilarity)(embedding, this.candidatesFor(namespace), limit);
227
- return ranked.map(({ id, similarity }) => {
228
- const memory = this.cache.get(id);
229
- return {
230
- id,
231
- label: memory.label,
232
- text: memory.text,
233
- meta: memory.meta,
234
- ...namespaceField(memory.namespace),
235
- similarity: round3(similarity),
344
+ const candidates = this.candidatesFor(namespace);
345
+ const scored = (0, virlow_memory_1.rankBySimilarity)(embedding, candidates, candidates.length).map(({ id, similarity }) => ({ memory: this.cache.get(id), similarity }));
346
+ const at = opts.asOf ? Date.parse(opts.asOf.length === 10 ? `${opts.asOf}T23:59:59Z` : opts.asOf) : undefined;
347
+ const ranked = (0, memory_graph_js_1.rankMemories)(this.graph(namespace), query, scored, {
348
+ ...(at !== undefined && !Number.isNaN(at) && { at }),
349
+ includeHistory: opts.includeHistory === true,
350
+ limit,
351
+ });
352
+ return ranked.map(({ memory, similarity }) => ({
353
+ id: memory.id,
354
+ label: memory.label,
355
+ text: memory.text,
356
+ meta: memory.meta,
357
+ ...namespaceField(memory.namespace),
358
+ similarity: round3(similarity),
359
+ }));
360
+ }
361
+ /** Everything known about one entity: its current and past relations and
362
+ * the facts behind them. Null when no memory names it. */
363
+ async recallEntity(name, namespace) {
364
+ await this.sync();
365
+ this.requireSession();
366
+ const graph = this.graph(namespace);
367
+ const entity = graph.entities.get((0, memory_graph_js_1.entityKey)(name));
368
+ if (!entity)
369
+ return null;
370
+ const now = Date.now();
371
+ const memories = [...entity.memoryIds]
372
+ .map((id) => this.cache.get(id))
373
+ .filter(Boolean)
374
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
375
+ return {
376
+ name: entity.name,
377
+ current: memories.filter((m) => (0, memory_graph_js_1.memoryHeldAt)(m, now)),
378
+ past: memories.filter((m) => !(0, memory_graph_js_1.memoryHeldAt)(m, now)),
379
+ relations: entity.edges.map((e) => ({ ...e })),
380
+ };
381
+ }
382
+ /** Adds entities and relations to an existing memory: how a memory saved
383
+ * before the graph existed gets linked. Also ends what it replaces. */
384
+ link(id, add) {
385
+ return this.enqueue(async () => {
386
+ await this.sync();
387
+ const key = this.requireSession();
388
+ const startEpoch = this.epoch;
389
+ const existing = this.cache.get(id);
390
+ if (!existing)
391
+ throw new Error(`Memory ${id} not found`);
392
+ (0, memory_secrets_js_1.assertNoSecret)(...(add.relations ?? []).map((r) => `${r.s} ${r.p} ${r.o}`));
393
+ const before = this.graph();
394
+ const extra = this.canonicalize({
395
+ ...(add.about && { about: add.about }),
396
+ ...(add.relations && { relations: add.relations }),
397
+ });
398
+ const meta = {
399
+ ...existing.meta,
400
+ about: unionNames(existing.meta.about, extra.about),
401
+ relations: unionRelations(existing.meta.relations, extra.relations),
236
402
  };
403
+ const row = await this.writeNote(key, id, existing.label, existing.text, meta);
404
+ if (this.epoch === startEpoch) {
405
+ this.cache.set(id, { ...existing, meta, updatedAt: row.updatedAt });
406
+ this.embeddings?.set(id, row.updatedAt, existing.embedding);
407
+ await this.saveEmbeddings(key, startEpoch);
408
+ }
409
+ return this.closeSuperseded(key, startEpoch, before, id, { relations: extra.relations ?? [] });
410
+ });
411
+ }
412
+ /** One entity spelled two ways becomes one: every memory naming `from`
413
+ * is rewritten to name `into`. Returns how many memories changed. */
414
+ mergeEntities(from, into) {
415
+ return this.enqueue(async () => {
416
+ await this.sync();
417
+ const key = this.requireSession();
418
+ const startEpoch = this.epoch;
419
+ const fromKey = (0, memory_graph_js_1.entityKey)(from);
420
+ const swap = (n) => ((0, memory_graph_js_1.entityKey)(n) === fromKey ? into.trim() : n);
421
+ let changed = 0;
422
+ for (const memory of [...this.cache.values()]) {
423
+ const names = [...(memory.meta.about ?? []), ...(memory.meta.relations ?? []).flatMap((r) => [r.s, r.o])];
424
+ if (!names.some((n) => (0, memory_graph_js_1.entityKey)(n) === fromKey))
425
+ continue;
426
+ const meta = { ...memory.meta };
427
+ if (memory.meta.about)
428
+ meta.about = unionNames([], memory.meta.about.map(swap));
429
+ if (memory.meta.relations) {
430
+ meta.relations = unionRelations([], memory.meta.relations.map((r) => ({ ...r, s: swap(r.s), o: swap(r.o) })));
431
+ }
432
+ const row = await this.writeNote(key, memory.id, memory.label, memory.text, meta);
433
+ if (this.epoch === startEpoch) {
434
+ this.cache.set(memory.id, { ...memory, meta, updatedAt: row.updatedAt });
435
+ this.embeddings?.set(memory.id, row.updatedAt, memory.embedding);
436
+ }
437
+ changed++;
438
+ }
439
+ if (changed > 0)
440
+ await this.saveEmbeddings(key, startEpoch);
441
+ return changed;
237
442
  });
238
443
  }
444
+ /**
445
+ * What to load at the start of a session: the entities this namespace
446
+ * talks about most, and its most recent current facts, within `limit`
447
+ * facts. Global memories are included alongside a project's.
448
+ */
449
+ async context(namespace, limit = 12) {
450
+ await this.sync();
451
+ this.requireSession();
452
+ const now = Date.now();
453
+ const graph = this.graph(namespace);
454
+ const entities = [...graph.entities.values()]
455
+ .sort((a, b) => b.memoryIds.size - a.memoryIds.size)
456
+ .slice(0, 8)
457
+ .map((e) => ({ name: e.name, facts: e.memoryIds.size }));
458
+ const facts = this.list(namespace)
459
+ .filter((m) => (0, memory_graph_js_1.memoryHeldAt)(m, now))
460
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))
461
+ .slice(0, limit);
462
+ return { entities, facts };
463
+ }
464
+ /** Findings for a tidy pass: unlinked memories, likely duplicate names and
465
+ * single-valued facts holding two values at once. */
466
+ async health(namespace) {
467
+ await this.sync();
468
+ this.requireSession();
469
+ const memories = this.list(namespace);
470
+ return (0, memory_graph_js_1.graphHealth)(this.graph(namespace), memories);
471
+ }
239
472
  /** Requires a prior sync() in the same unlock to have populated the cache. */
240
473
  list(namespace) {
241
474
  this.requireSession();
@@ -255,6 +488,7 @@ class MemoryStore {
255
488
  throw new Error(`Memory ${id} not found`);
256
489
  const label = changes.label ?? existing.label;
257
490
  const text = changes.text ?? existing.text;
491
+ (0, memory_secrets_js_1.assertNoSecret)(label, text);
258
492
  const meta = (0, memory_note_js_1.mergeMeta)(existing.meta, changes.meta ?? {});
259
493
  const row = await this.writeNote(key, id, label, text, meta);
260
494
  const embedding = await this.embedder.embed(embeddingInput(label, text));
@@ -0,0 +1,105 @@
1
+ import type { MemoryMeta, MemoryRelation } from './memory-note.js';
2
+ /**
3
+ * The memory graph, built from memories rather than stored beside them.
4
+ *
5
+ * A memory's frontmatter names the entities it is about (`about`) and the
6
+ * relations it asserts (`relations`). This module turns a set of memories
7
+ * into entities and edges, answers "is this still true" for a date, and
8
+ * ranks recall with the graph and recency as well as similarity. Pure: no
9
+ * network, no crypto, so every rule here is tested directly.
10
+ *
11
+ * Entities are names. Two mentions are the same entity when their names
12
+ * match ignoring case, spacing and a leading "the"; anything looser (Dana
13
+ * and Dana Whitfield) is a merge the AI or the person asks for, never a
14
+ * guess. See docs/memory-graph.md.
15
+ */
16
+ export interface GraphMemory {
17
+ id: string;
18
+ label: string;
19
+ text: string;
20
+ meta: MemoryMeta;
21
+ namespace?: string;
22
+ updatedAt: string;
23
+ }
24
+ export interface Edge extends MemoryRelation {
25
+ /** The memory that asserted it. */
26
+ memoryId: string;
27
+ }
28
+ export interface Entity {
29
+ key: string;
30
+ /** The spelling most memories use. */
31
+ name: string;
32
+ memoryIds: Set<string>;
33
+ edges: Edge[];
34
+ }
35
+ export interface MemoryGraph {
36
+ entities: Map<string, Entity>;
37
+ edges: Edge[];
38
+ }
39
+ /** The matching key for a name: case, spacing and a leading article do not
40
+ * make a different entity. */
41
+ export declare function entityKey(name: string): string;
42
+ /**
43
+ * Predicates where one side holds one value at a time, so a new fact ends
44
+ * the old one. Which side matters:
45
+ * - one object per subject: a person works at one place, reports to one
46
+ * manager (Dana works_at Acme, then Dana works_at Initech);
47
+ * - one subject per object: a project has one lead (Dana leads Atlas, then
48
+ * Sam leads Atlas).
49
+ * Anything else accumulates (prefers, uses, knows).
50
+ */
51
+ export declare const ONE_OBJECT_PER_SUBJECT: Set<string>;
52
+ export declare const ONE_SUBJECT_PER_OBJECT: Set<string>;
53
+ /** Either kind: a predicate that holds one value at a time. */
54
+ export declare const SINGLE_VALUED: Set<string>;
55
+ /** snake_case, so "Works at" and "works_at" are one predicate. */
56
+ export declare function normalizePredicate(p: string): string;
57
+ /** Whether a span held at `at`: started on or before it, ended after it. */
58
+ export declare function heldAt(from: string | undefined, to: string | undefined, at: number): boolean;
59
+ /**
60
+ * Whether a memory's fact held at `at`. It starts at `valid_from`, else the
61
+ * earliest date its relations give (a fact about "since January" learned
62
+ * today started in January), else when it was learned.
63
+ */
64
+ export declare function memoryHeldAt(memory: GraphMemory, at: number): boolean;
65
+ export declare function buildGraph(memories: Iterable<GraphMemory>): MemoryGraph;
66
+ /** Entities named in a piece of text (a prompt, a query), longest names
67
+ * first so "Dana Whitfield" wins over "Dana". Whole words only. */
68
+ export declare function entitiesMentioned(graph: MemoryGraph, text: string): Entity[];
69
+ /** Entities one relation away from any of `from`. */
70
+ export declare function neighbours(graph: MemoryGraph, from: Entity[]): Entity[];
71
+ export interface RankInput {
72
+ memory: GraphMemory;
73
+ similarity: number;
74
+ }
75
+ /** How much the graph and recency move a similarity score. Small on
76
+ * purpose: similarity still decides, the graph breaks ties and pulls in
77
+ * facts that share an entity with the query but none of its words. */
78
+ export declare const DIRECT_BOOST = 0.15;
79
+ export declare const NEIGHBOUR_BOOST = 0.07;
80
+ export declare function rankMemories(graph: MemoryGraph, query: string, scored: RankInput[], opts: {
81
+ at?: number;
82
+ includeHistory?: boolean;
83
+ limit: number;
84
+ }): Array<RankInput & {
85
+ score: number;
86
+ }>;
87
+ /**
88
+ * Current edges a new relation replaces: same subject and single-valued
89
+ * predicate, a different object, still holding when the new one starts.
90
+ */
91
+ export declare function supersededBy(graph: MemoryGraph, relation: MemoryRelation, at: number): Edge[];
92
+ /**
93
+ * Housekeeping findings for a tidy pass: memories linked to nothing, names
94
+ * that look like one entity spelled two ways, and single-valued predicates
95
+ * holding two values at once.
96
+ */
97
+ export declare function graphHealth(graph: MemoryGraph, memories: GraphMemory[], at?: number): {
98
+ unlinked: string[];
99
+ possibleDuplicates: [string, string][];
100
+ contradictions: {
101
+ subject: string;
102
+ predicate: string;
103
+ objects: string[];
104
+ }[];
105
+ };