mark-improving-agent 2.3.2 → 2.3.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/VERSION CHANGED
@@ -1 +1 @@
1
- 2.3.2
1
+ 2.3.3
@@ -10,3 +10,4 @@ export * from './adaptive-rag.js';
10
10
  export { createContextFragmentationEngine } from './context-fragmentation.js';
11
11
  export { createHybridSearchEngine, createBM25Index, bm25Score, normalizeBM25Scores, DEFAULT_HYBRID_CONFIG } from './hybrid-search.js';
12
12
  export { createPatternRecognizer } from './pattern-recognizer.js';
13
+ export { createMemoryObserver } from './observer.js';
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Memory Observer - Silent Background Memory Writer
3
+ *
4
+ * Implements the Mnemostroma-inspired Observer pattern: the AI agent NEVER writes
5
+ * memory directly. Instead, this Observer sidecar silently watches all I/O,
6
+ * extracts entities, embeds, scores, and indexes content automatically.
7
+ *
8
+ * Key principles:
9
+ * - AI never writes memory — Observer does it silently
10
+ * - Dual async pipeline: Observer (write) + Content Branch (versioned artifacts)
11
+ * - Memory Hulling: separates conversational noise from extractable kernels
12
+ * - 20ms hot buffer retrieval, ~50ms full extraction latency
13
+ *
14
+ * Based on: GG-QandV/mnemostroma (https://github.com/GG-QandV/mnemostroma)
15
+ *
16
+ * @module core/memory
17
+ * @fileoverview Observer pattern for silent memory writing
18
+ */
19
+ import { randomUUID } from 'crypto';
20
+ import { createLogger } from '../../utils/logger.js';
21
+ import { createEmbedder, cosineSimilarity } from './embedder.js';
22
+ const logger = createLogger('[MemoryObserver]');
23
+ const DEFAULT_OBSERVER_CONFIG = {
24
+ importanceThreshold: 0.3,
25
+ hotBufferSize: 50,
26
+ autoExtract: true,
27
+ debounceMs: 100,
28
+ maxShellAge: 5 * 60 * 1000, // 5 minutes
29
+ embedder: createEmbedder({ dimensions: 128 }),
30
+ };
31
+ /**
32
+ * Noise patterns that indicate conversational shell (not worth storing)
33
+ */
34
+ const SHELL_PATTERNS = [
35
+ /^(hi|hello|hey|what's up|howdy)\b/i,
36
+ /^(thanks?|thank you|thx|ty|much appreciated)\b/i,
37
+ /^(okay|ok|okk|kk|sure|yes|no|nah|yeah|yep|nope)\b/i,
38
+ /^(lol|lmao|rofl|haha|heh)\b/i,
39
+ /^(bye|goodbye|see you|cya|good night|night)\b/i,
40
+ /^(sorry|apologies|my bad|whoops)\b/i,
41
+ /^(please|pls|plz|could you|would you)\b/i,
42
+ /^(interesting|cool|nice|awesome|great|good)\b/i,
43
+ /^(i see|i understand|i get it|got it|understood)\b/i,
44
+ /^[\s.,!?;:]*$/,
45
+ /^(oh|ah|uh|um|hmm|well)\b/i,
46
+ /^(let me know|feel free|take care|talk later)\b/i,
47
+ ];
48
+ /**
49
+ * Anchor patterns that indicate important kernels (decisions, deadlines, facts)
50
+ */
51
+ const ANCHOR_PATTERNS = [
52
+ /\b(decided|decision|agreed|chosen|selected|picked|settled)\b/i,
53
+ /\b(must|have to|need to|required|mandatory|essential)\b/i,
54
+ /\b(deadline|due|by|before|after|until|timing)\b/i,
55
+ /\b(never|always|don't|do not|must not|forbidden)\b/i,
56
+ /\b(important|critical|key|vital|priority)\b/i,
57
+ /\b(remember|forget|keep in mind|note that)\b/i,
58
+ /\b(because|since|reason|why|therefore|thus)\b/i,
59
+ /\b(fact|true|real|actually|indeed|in fact)\b/i,
60
+ /\b(but|however|although|except|except for)\b/i,
61
+ /\b(name|date|location|price|cost|amount)\b/i,
62
+ /\b(user|client|customer|they|their)\b.*\b(want|need|prefer|like)\b/i,
63
+ ];
64
+ /**
65
+ * Kernel type classifiers
66
+ */
67
+ function classifyKernel(content, context) {
68
+ const lower = content.toLowerCase();
69
+ const fullText = [content, ...context].join(' ').toLowerCase();
70
+ if (/deadline|due|by |before |after |timing/i.test(lower))
71
+ return 'constraint';
72
+ if (/decided|agreed|chosen|picked|selected/i.test(lower))
73
+ return 'decision';
74
+ if (/must|have to|need to|required|mandatory/i.test(lower))
75
+ return 'rule';
76
+ if (/fact|true|actual|indeed|in fact|real/i.test(fullText))
77
+ return 'fact';
78
+ if (/remember|forget|note|keep in mind/i.test(lower))
79
+ return 'context';
80
+ if (/name|date|location|price|amount/i.test(lower))
81
+ return 'entity';
82
+ return 'context';
83
+ }
84
+ /**
85
+ * Score importance based on content features
86
+ */
87
+ function scoreImportance(content, isAnchor) {
88
+ let score = 0.5;
89
+ if (isAnchor)
90
+ score += 0.2;
91
+ const words = content.split(/\s+/).length;
92
+ if (words >= 5 && words <= 50)
93
+ score += 0.15;
94
+ else if (words > 100)
95
+ score -= 0.1;
96
+ if (/\d+/.test(content))
97
+ score += 0.1;
98
+ if (/[A-Z][a-z]+\s+[A-Z][a-z]+/.test(content))
99
+ score += 0.1;
100
+ if (/\?$/.test(content))
101
+ score += 0.05;
102
+ if (/!$/.test(content))
103
+ score += 0.05;
104
+ return Math.max(0, Math.min(1, score));
105
+ }
106
+ /**
107
+ * Check if content is shell (noise)
108
+ */
109
+ function isShell(content) {
110
+ const trimmed = content.trim();
111
+ if (trimmed.length < 3)
112
+ return true;
113
+ for (const pattern of SHELL_PATTERNS) {
114
+ if (pattern.test(trimmed))
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ /**
120
+ * Extract anchors from content
121
+ */
122
+ function extractAnchors(content) {
123
+ const anchors = [];
124
+ for (const pattern of ANCHOR_PATTERNS) {
125
+ const match = content.match(pattern);
126
+ if (match) {
127
+ anchors.push(match[0]);
128
+ }
129
+ }
130
+ return anchors;
131
+ }
132
+ function buildMemoryObserver(config = {}) {
133
+ const cfg = { ...DEFAULT_OBSERVER_CONFIG, ...config };
134
+ const hotBuffer = [];
135
+ const anchorIndex = new Map();
136
+ const tagIndex = new Map();
137
+ let kernelsExtracted = 0;
138
+ let shellDiscarded = 0;
139
+ let lastFlush = null;
140
+ let sessionId = randomUUID();
141
+ let previousSessionId = null;
142
+ /**
143
+ * Hull content — separate kernels from shell
144
+ */
145
+ function hull(content, context = []) {
146
+ const kernels = [];
147
+ const shell = [];
148
+ const sentences = content.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 2);
149
+ for (const sentence of sentences) {
150
+ if (isShell(sentence)) {
151
+ shell.push(sentence);
152
+ shellDiscarded++;
153
+ continue;
154
+ }
155
+ const anchors = extractAnchors(sentence);
156
+ const isAnchor = anchors.length > 0;
157
+ const importance = scoreImportance(sentence, isAnchor);
158
+ if (importance >= cfg.importanceThreshold) {
159
+ const type = classifyKernel(sentence, context);
160
+ const kernel = {
161
+ type,
162
+ content: sentence,
163
+ importance,
164
+ tags: [type],
165
+ anchors,
166
+ sourceExcerpt: sentence.slice(0, 200),
167
+ };
168
+ if (isAnchor) {
169
+ kernel.tags.push('anchor');
170
+ }
171
+ kernels.push(kernel);
172
+ kernelsExtracted++;
173
+ for (const anchor of anchors) {
174
+ anchorIndex.set(anchor, {
175
+ content: sentence,
176
+ type,
177
+ timestamp: Date.now(),
178
+ });
179
+ }
180
+ for (const tag of kernel.tags) {
181
+ if (!tagIndex.has(tag)) {
182
+ tagIndex.set(tag, []);
183
+ }
184
+ tagIndex.get(tag).push(kernel);
185
+ }
186
+ }
187
+ else {
188
+ shell.push(sentence);
189
+ shellDiscarded++;
190
+ }
191
+ }
192
+ return { kernels, shell };
193
+ }
194
+ function maintainHotBuffer() {
195
+ while (hotBuffer.length > cfg.hotBufferSize) {
196
+ let minIdx = 0;
197
+ let minImportance = Infinity;
198
+ for (let i = 0; i < hotBuffer.length; i++) {
199
+ if (hotBuffer[i].importance < minImportance) {
200
+ minImportance = hotBuffer[i].importance;
201
+ minIdx = i;
202
+ }
203
+ }
204
+ hotBuffer.splice(minIdx, 1);
205
+ }
206
+ }
207
+ async function semanticSearch(query, limit = 5) {
208
+ try {
209
+ const queryEmbedding = cfg.embedder.embed(query);
210
+ const results = [];
211
+ for (const kernel of hotBuffer) {
212
+ const kernelEmbedding = cfg.embedder.embed(kernel.content);
213
+ const similarity = cosineSimilarity(queryEmbedding, kernelEmbedding);
214
+ if (similarity > 0.3) {
215
+ results.push({ content: kernel.content, score: similarity });
216
+ }
217
+ }
218
+ results.sort((a, b) => b.score - a.score);
219
+ return results.slice(0, limit);
220
+ }
221
+ catch (err) {
222
+ logger.warn('Semantic search failed, falling back to keyword', { error: err });
223
+ const queryLower = query.toLowerCase();
224
+ return hotBuffer
225
+ .filter(k => k.content.toLowerCase().includes(queryLower))
226
+ .slice(0, limit)
227
+ .map(k => ({ content: k.content, score: k.importance }));
228
+ }
229
+ }
230
+ return {
231
+ observe(content, stream = 'semantic', context) {
232
+ const start = Date.now();
233
+ const { kernels, shell } = hull(content, context);
234
+ hotBuffer.push(...kernels);
235
+ maintainHotBuffer();
236
+ const processingMs = Date.now() - start;
237
+ logger.debug(`Observed [${stream}]: ${kernels.length} kernels extracted, ${shell.length} shell discarded (${processingMs}ms)`);
238
+ return {
239
+ id: randomUUID(),
240
+ stream,
241
+ kernels,
242
+ shell,
243
+ timestamp: Date.now(),
244
+ processingMs,
245
+ };
246
+ },
247
+ async flush(tier = 'learned') {
248
+ if (hotBuffer.length === 0)
249
+ return [];
250
+ const entries = [];
251
+ for (const kernel of hotBuffer) {
252
+ const entry = {
253
+ id: randomUUID(),
254
+ tier,
255
+ content: kernel.content,
256
+ importance: kernel.importance,
257
+ tags: kernel.tags,
258
+ timestamp: Date.now(),
259
+ accessCount: 0,
260
+ lastAccessed: Date.now(),
261
+ source: 'self',
262
+ };
263
+ entries.push(entry);
264
+ }
265
+ lastFlush = Date.now();
266
+ logger.info(`Flushed ${entries.length} kernels to ${tier} tier`);
267
+ return entries;
268
+ },
269
+ async ctx_semantic(query, limit = 5) {
270
+ return semanticSearch(query, limit);
271
+ },
272
+ ctx_anchors(limit = 20) {
273
+ const results = Array.from(anchorIndex.entries())
274
+ .map(([, data]) => ({
275
+ content: data.content,
276
+ type: data.type,
277
+ timestamp: data.timestamp,
278
+ }))
279
+ .sort((a, b) => b.timestamp - a.timestamp);
280
+ return results.slice(0, limit);
281
+ },
282
+ async ctx_search(tags, limit = 10) {
283
+ const matchingKernels = [];
284
+ for (const tag of tags) {
285
+ const kernels = tagIndex.get(tag) || [];
286
+ matchingKernels.push(...kernels);
287
+ }
288
+ const seen = new Set();
289
+ const unique = matchingKernels.filter(k => {
290
+ if (seen.has(k.content))
291
+ return false;
292
+ seen.add(k.content);
293
+ return true;
294
+ });
295
+ return unique.slice(0, limit).map(k => ({
296
+ id: randomUUID(),
297
+ tier: 'learned',
298
+ content: k.content,
299
+ importance: k.importance,
300
+ tags: k.tags,
301
+ timestamp: Date.now(),
302
+ accessCount: 0,
303
+ lastAccessed: Date.now(),
304
+ }));
305
+ },
306
+ ctx_bridge(prevSessionId) {
307
+ const decisions = Array.from(anchorIndex.entries())
308
+ .filter(([, data]) => data.type === 'decision')
309
+ .map(([, data]) => data.content);
310
+ const constraints = Array.from(anchorIndex.entries())
311
+ .filter(([, data]) => data.type === 'constraint' || data.type === 'rule')
312
+ .map(([, data]) => data.content);
313
+ const importantFacts = Array.from(anchorIndex.entries())
314
+ .filter(([, data]) => data.type === 'fact')
315
+ .map(([, data]) => ({
316
+ fact: data.content,
317
+ certainty: 0.8,
318
+ }));
319
+ const contextSummary = hotBuffer.length > 0
320
+ ? hotBuffer.slice(0, 5).map(k => k.content).join(' ')
321
+ : '';
322
+ const packet = {
323
+ sessionId,
324
+ previousSessionId: prevSessionId || previousSessionId,
325
+ context: contextSummary,
326
+ decisions,
327
+ constraints,
328
+ pendingTasks: [],
329
+ importantFacts,
330
+ timestamp: Date.now(),
331
+ agentVersion: '2.3.3',
332
+ };
333
+ previousSessionId = sessionId;
334
+ sessionId = randomUUID();
335
+ return packet;
336
+ },
337
+ getStats() {
338
+ return {
339
+ hotBufferSize: hotBuffer.length,
340
+ kernelsExtracted,
341
+ shellDiscarded,
342
+ lastFlush,
343
+ };
344
+ },
345
+ };
346
+ }
347
+ export function createMemoryObserver(config) {
348
+ logger.info('Creating Memory Observer (Mnemostroma-inspired)');
349
+ return buildMemoryObserver(config);
350
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '2.3.2';
1
+ export const VERSION = '2.3.3';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mark-improving-agent",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "Self-evolving AI agent with permanent memory, identity continuity, and self-evolution — for AI agents that need to remember, learn, and evolve across sessions",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",