mark-improving-agent 2.3.1 → 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 +1 -1
- package/dist/core/memory/index.js +2 -0
- package/dist/core/memory/observer.js +350 -0
- package/dist/core/memory/pattern-recognizer.js +358 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.3.
|
|
1
|
+
2.3.3
|
|
@@ -9,3 +9,5 @@ export * from './hopfield-network.js';
|
|
|
9
9
|
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
|
+
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
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory Pattern Recognizer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes recurring patterns in memory storage and retrieval to enable
|
|
5
|
+
* personalized memory prioritization. Detects temporal patterns, interaction
|
|
6
|
+
* styles, and topic frequencies to optimize memory consolidation.
|
|
7
|
+
*
|
|
8
|
+
* Based on: Adaptive Memory Systems research - patterns in when/how users store memories
|
|
9
|
+
*
|
|
10
|
+
* @module core/memory
|
|
11
|
+
* @fileoverview Pattern recognition for memory optimization
|
|
12
|
+
*/
|
|
13
|
+
import { createLogger } from '../../utils/logger.js';
|
|
14
|
+
const logger = createLogger('[PatternRecognizer]');
|
|
15
|
+
const DEFAULT_CONFIG = {
|
|
16
|
+
minMemories: 10,
|
|
17
|
+
temporalWindowDays: 30,
|
|
18
|
+
enableAnomalyDetection: true,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Creates a Memory Pattern Recognizer
|
|
22
|
+
*
|
|
23
|
+
* Analyzes memory entries to detect patterns in:
|
|
24
|
+
* - Temporal distribution (when memories are stored)
|
|
25
|
+
* - Topic frequency and coherence
|
|
26
|
+
* - Tag co-occurrence
|
|
27
|
+
* - User interaction styles
|
|
28
|
+
*
|
|
29
|
+
* @param entries - Memory entries to analyze
|
|
30
|
+
* @param config - Configuration options
|
|
31
|
+
* @returns Pattern analysis results
|
|
32
|
+
*/
|
|
33
|
+
export function createPatternRecognizer(entries, config = {}) {
|
|
34
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
35
|
+
return new PatternRecognizerImpl(entries, cfg);
|
|
36
|
+
}
|
|
37
|
+
class PatternRecognizerImpl {
|
|
38
|
+
entries;
|
|
39
|
+
config;
|
|
40
|
+
tagCooccurrence = new Map();
|
|
41
|
+
analysisCache = null;
|
|
42
|
+
constructor(entries, config) {
|
|
43
|
+
this.entries = entries;
|
|
44
|
+
this.config = config;
|
|
45
|
+
this.buildTagCooccurrence();
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build tag co-occurrence matrix for pattern detection
|
|
49
|
+
*/
|
|
50
|
+
buildTagCooccurrence() {
|
|
51
|
+
this.tagCooccurrence.clear();
|
|
52
|
+
for (const entry of this.entries) {
|
|
53
|
+
for (const tag of entry.tags) {
|
|
54
|
+
if (!this.tagCooccurrence.has(tag)) {
|
|
55
|
+
this.tagCooccurrence.set(tag, {
|
|
56
|
+
tag,
|
|
57
|
+
cooccursWith: new Map(),
|
|
58
|
+
totalCount: 0,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
const tagData = this.tagCooccurrence.get(tag);
|
|
62
|
+
tagData.totalCount++;
|
|
63
|
+
// Count co-occurrences with other tags
|
|
64
|
+
for (const otherTag of entry.tags) {
|
|
65
|
+
if (otherTag !== tag) {
|
|
66
|
+
const current = tagData.cooccursWith.get(otherTag) || 0;
|
|
67
|
+
tagData.cooccursWith.set(otherTag, current + 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Perform full pattern analysis
|
|
75
|
+
*/
|
|
76
|
+
analyze() {
|
|
77
|
+
if (this.entries.length < this.config.minMemories) {
|
|
78
|
+
logger.info(`Not enough memories for pattern analysis (${this.entries.length} < ${this.config.minMemories})`);
|
|
79
|
+
return this.createEmptyAnalysis();
|
|
80
|
+
}
|
|
81
|
+
logger.info(`Analyzing patterns across ${this.entries.length} memories`);
|
|
82
|
+
const temporalPatterns = this.getTemporalPatterns();
|
|
83
|
+
const topicPatterns = this.getTopicPatterns();
|
|
84
|
+
const interactionStyle = this.getInteractionStyle();
|
|
85
|
+
const optimalRecallTimes = this.getOptimalRecallTimes();
|
|
86
|
+
const predictedHighValueTags = this.getPredictedHighValueTags();
|
|
87
|
+
const anomalyScore = this.getAnomalyScore();
|
|
88
|
+
const analysis = {
|
|
89
|
+
temporalPatterns,
|
|
90
|
+
topicPatterns,
|
|
91
|
+
interactionStyle,
|
|
92
|
+
optimalRecallTimes,
|
|
93
|
+
predictedHighValueTags,
|
|
94
|
+
anomalyScore,
|
|
95
|
+
analyzedAt: Date.now(),
|
|
96
|
+
};
|
|
97
|
+
this.analysisCache = analysis;
|
|
98
|
+
return analysis;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Create empty analysis when insufficient data
|
|
102
|
+
*/
|
|
103
|
+
createEmptyAnalysis() {
|
|
104
|
+
return {
|
|
105
|
+
temporalPatterns: [],
|
|
106
|
+
topicPatterns: [],
|
|
107
|
+
interactionStyle: {
|
|
108
|
+
style: 'mixed',
|
|
109
|
+
confidence: 0,
|
|
110
|
+
indicators: [],
|
|
111
|
+
dominantTags: [],
|
|
112
|
+
},
|
|
113
|
+
optimalRecallTimes: [],
|
|
114
|
+
predictedHighValueTags: [],
|
|
115
|
+
anomalyScore: 0,
|
|
116
|
+
analyzedAt: Date.now(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Get temporal patterns (when user stores memories)
|
|
121
|
+
*/
|
|
122
|
+
getTemporalPatterns() {
|
|
123
|
+
const patterns = [];
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const cutoff = now - (this.config.temporalWindowDays * 24 * 60 * 60 * 1000);
|
|
126
|
+
// Filter to time window
|
|
127
|
+
const recentEntries = this.entries.filter(e => e.timestamp >= cutoff);
|
|
128
|
+
// Hour patterns
|
|
129
|
+
const hourMap = new Map();
|
|
130
|
+
for (const entry of recentEntries) {
|
|
131
|
+
const d = new Date(entry.timestamp);
|
|
132
|
+
const hour = d.getHours();
|
|
133
|
+
const existing = hourMap.get(hour) || { count: 0, importance: 0, tags: [] };
|
|
134
|
+
existing.count++;
|
|
135
|
+
existing.importance += entry.importance;
|
|
136
|
+
existing.tags.push(...entry.tags.slice(0, 2));
|
|
137
|
+
hourMap.set(hour, existing);
|
|
138
|
+
}
|
|
139
|
+
for (const [hour, data] of hourMap) {
|
|
140
|
+
patterns.push({
|
|
141
|
+
window: 'hour',
|
|
142
|
+
value: hour,
|
|
143
|
+
count: data.count,
|
|
144
|
+
avgImportance: data.importance / data.count,
|
|
145
|
+
topTags: this.getTopTags(data.tags, 3),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// Day patterns (0=Sun, 6=Sat)
|
|
149
|
+
const dayMap = new Map();
|
|
150
|
+
for (const entry of recentEntries) {
|
|
151
|
+
const d = new Date(entry.timestamp);
|
|
152
|
+
const day = d.getDay();
|
|
153
|
+
const existing = dayMap.get(day) || { count: 0, importance: 0, tags: [] };
|
|
154
|
+
existing.count++;
|
|
155
|
+
existing.importance += entry.importance;
|
|
156
|
+
existing.tags.push(...entry.tags.slice(0, 2));
|
|
157
|
+
dayMap.set(day, existing);
|
|
158
|
+
}
|
|
159
|
+
for (const [day, data] of dayMap) {
|
|
160
|
+
patterns.push({
|
|
161
|
+
window: 'day',
|
|
162
|
+
value: day,
|
|
163
|
+
count: data.count,
|
|
164
|
+
avgImportance: data.importance / data.count,
|
|
165
|
+
topTags: this.getTopTags(data.tags, 3),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return patterns;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Get top tags from a list
|
|
172
|
+
*/
|
|
173
|
+
getTopTags(tags, limit) {
|
|
174
|
+
const counts = new Map();
|
|
175
|
+
for (const tag of tags) {
|
|
176
|
+
counts.set(tag, (counts.get(tag) || 0) + 1);
|
|
177
|
+
}
|
|
178
|
+
return Array.from(counts.entries())
|
|
179
|
+
.sort((a, b) => b[1] - a[1])
|
|
180
|
+
.slice(0, limit)
|
|
181
|
+
.map(([tag]) => tag);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get topic patterns based on tag frequency and recency
|
|
185
|
+
*/
|
|
186
|
+
getTopicPatterns() {
|
|
187
|
+
const patterns = [];
|
|
188
|
+
for (const [tag, data] of this.tagCooccurrence) {
|
|
189
|
+
const recency = Math.max(...this.entries
|
|
190
|
+
.filter(e => e.tags.includes(tag))
|
|
191
|
+
.map(e => e.timestamp));
|
|
192
|
+
// Calculate coherence: how consistently this tag appears
|
|
193
|
+
const entriesWithTag = this.entries.filter(e => e.tags.includes(tag));
|
|
194
|
+
const coherence = entriesWithTag.length / this.entries.length;
|
|
195
|
+
patterns.push({
|
|
196
|
+
topic: tag,
|
|
197
|
+
frequency: data.totalCount,
|
|
198
|
+
avgImportance: data.totalCount / this.entries.length, // normalized
|
|
199
|
+
recency,
|
|
200
|
+
coherence: Math.min(coherence * 10, 1), // scale to 0-1
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return patterns.sort((a, b) => b.frequency - a.frequency);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Detect user's interaction style based on tag patterns
|
|
207
|
+
*/
|
|
208
|
+
getInteractionStyle() {
|
|
209
|
+
const tagData = Array.from(this.tagCooccurrence.entries())
|
|
210
|
+
.map(([tagName, data]) => ({
|
|
211
|
+
tag: tagName,
|
|
212
|
+
cooccursWith: data.cooccursWith,
|
|
213
|
+
totalCount: data.totalCount
|
|
214
|
+
}))
|
|
215
|
+
.sort((a, b) => b.totalCount - a.totalCount);
|
|
216
|
+
const topTags = tagData.slice(0, 10).map(d => d.tag);
|
|
217
|
+
// Style indicators
|
|
218
|
+
const indicators = [];
|
|
219
|
+
// Analytical: code, technical, system, data, logic
|
|
220
|
+
const analyticalTags = ['code', 'technical', 'system', 'data', 'logic', 'debug', 'architecture'];
|
|
221
|
+
const analyticalCount = topTags.filter(t => analyticalTags.includes(t.toLowerCase())).length;
|
|
222
|
+
if (analyticalCount >= 2)
|
|
223
|
+
indicators.push('anytical patterns detected');
|
|
224
|
+
// Creative: idea, design, creative, story, write, art
|
|
225
|
+
const creativeTags = ['idea', 'design', 'creative', 'story', 'write', 'art', 'content'];
|
|
226
|
+
const creativeCount = topTags.filter(t => creativeTags.some(c => t.toLowerCase().includes(c))).length;
|
|
227
|
+
if (creativeCount >= 2)
|
|
228
|
+
indicators.push('creative patterns detected');
|
|
229
|
+
// Operational: task, project, plan, schedule, workflow
|
|
230
|
+
const operationalTags = ['task', 'project', 'plan', 'schedule', 'workflow', 'routine'];
|
|
231
|
+
const operationalCount = topTags.filter(t => operationalTags.some(o => t.toLowerCase().includes(o))).length;
|
|
232
|
+
if (operationalCount >= 2)
|
|
233
|
+
indicators.push('operational patterns detected');
|
|
234
|
+
// Social: user, people, team, communication, feedback
|
|
235
|
+
const socialTags = ['user', 'people', 'team', 'communication', 'feedback', 'review', 'collaboration'];
|
|
236
|
+
const socialCount = topTags.filter(t => socialTags.some(s => t.toLowerCase().includes(s))).length;
|
|
237
|
+
if (socialCount >= 2)
|
|
238
|
+
indicators.push('social patterns detected');
|
|
239
|
+
// Determine dominant style
|
|
240
|
+
const counts = { analytical: analyticalCount, creative: creativeCount, operational: operationalCount, social: socialCount };
|
|
241
|
+
const maxCount = Math.max(...Object.values(counts));
|
|
242
|
+
const dominantStyles = Object.entries(counts).filter(([, c]) => c === maxCount).map(([s]) => s);
|
|
243
|
+
let style = 'mixed';
|
|
244
|
+
let confidence = 0.5;
|
|
245
|
+
if (dominantStyles.length === 1 && maxCount >= 2) {
|
|
246
|
+
style = dominantStyles[0];
|
|
247
|
+
confidence = Math.min(maxCount / 5, 0.95); // higher confidence with more matches
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
style,
|
|
251
|
+
confidence,
|
|
252
|
+
indicators,
|
|
253
|
+
dominantTags: topTags.slice(0, 5),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Calculate optimal times for memory recall based on temporal patterns
|
|
258
|
+
*/
|
|
259
|
+
getOptimalRecallTimes() {
|
|
260
|
+
const hourCounts = new Map();
|
|
261
|
+
for (const entry of this.entries) {
|
|
262
|
+
const d = new Date(entry.timestamp);
|
|
263
|
+
const hour = d.getHours();
|
|
264
|
+
hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1);
|
|
265
|
+
}
|
|
266
|
+
const maxCount = Math.max(...hourCounts.values());
|
|
267
|
+
const results = [];
|
|
268
|
+
for (let h = 0; h < 24; h++) {
|
|
269
|
+
const count = hourCounts.get(h) || 0;
|
|
270
|
+
results.push({
|
|
271
|
+
hour: h,
|
|
272
|
+
score: count / maxCount,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return results.sort((a, b) => b.score - a.score);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Predict which tags will be high-value based on patterns
|
|
279
|
+
*/
|
|
280
|
+
getPredictedHighValueTags() {
|
|
281
|
+
// Tags that appear frequently with high importance
|
|
282
|
+
const tagScores = new Map();
|
|
283
|
+
for (const entry of this.entries) {
|
|
284
|
+
for (const tag of entry.tags) {
|
|
285
|
+
const currentScore = tagScores.get(tag) || 0;
|
|
286
|
+
// Score = importance * recency boost * frequency
|
|
287
|
+
const recencyBoost = this.getRecencyBoost(entry.timestamp);
|
|
288
|
+
tagScores.set(tag, currentScore + (entry.importance * recencyBoost));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return Array.from(tagScores.entries())
|
|
292
|
+
.sort((a, b) => b[1] - a[1])
|
|
293
|
+
.slice(0, 10)
|
|
294
|
+
.map(([tag]) => tag);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Get recency boost factor (higher for recent entries)
|
|
298
|
+
*/
|
|
299
|
+
getRecencyBoost(timestamp) {
|
|
300
|
+
const now = Date.now();
|
|
301
|
+
const age = now - timestamp;
|
|
302
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
303
|
+
const daysOld = age / dayMs;
|
|
304
|
+
// Exponential decay: more recent = higher boost
|
|
305
|
+
return Math.exp(-daysOld / 7); // 7-day half-life
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Calculate anomaly score for recent memories
|
|
309
|
+
* Compares recent patterns to historical baseline
|
|
310
|
+
*/
|
|
311
|
+
getAnomalyScore() {
|
|
312
|
+
if (!this.config.enableAnomalyDetection || this.entries.length < 20) {
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
317
|
+
const weekMs = 7 * dayMs;
|
|
318
|
+
// Split into recent (last 3 days) and baseline (before that)
|
|
319
|
+
const recentCutoff = now - (3 * dayMs);
|
|
320
|
+
const baselineCutoff = now - weekMs;
|
|
321
|
+
const recentEntries = this.entries.filter(e => e.timestamp >= recentCutoff);
|
|
322
|
+
const baselineEntries = this.entries.filter(e => e.timestamp >= baselineCutoff && e.timestamp < recentCutoff);
|
|
323
|
+
if (recentEntries.length === 0 || baselineEntries.length === 0) {
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
// Compare tag distribution
|
|
327
|
+
const recentTags = this.getTagDistribution(recentEntries);
|
|
328
|
+
const baselineTags = this.getTagDistribution(baselineEntries);
|
|
329
|
+
// Calculate KL-like divergence (simplified)
|
|
330
|
+
let divergence = 0;
|
|
331
|
+
const allTags = new Set([...recentTags.keys(), ...baselineTags.keys()]);
|
|
332
|
+
for (const tag of allTags) {
|
|
333
|
+
const p = recentTags.get(tag) || 0.001;
|
|
334
|
+
const q = baselineTags.get(tag) || 0.001;
|
|
335
|
+
divergence += p * Math.log(p / q);
|
|
336
|
+
}
|
|
337
|
+
// Normalize to 0-1 range (typical divergence is 0-2)
|
|
338
|
+
return Math.min(divergence / 2, 1);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Get tag distribution as proportions
|
|
342
|
+
*/
|
|
343
|
+
getTagDistribution(entries) {
|
|
344
|
+
const counts = new Map();
|
|
345
|
+
let total = 0;
|
|
346
|
+
for (const entry of entries) {
|
|
347
|
+
for (const tag of entry.tags) {
|
|
348
|
+
counts.set(tag, (counts.get(tag) || 0) + 1);
|
|
349
|
+
total++;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const dist = new Map();
|
|
353
|
+
for (const [tag, count] of counts) {
|
|
354
|
+
dist.set(tag, count / total);
|
|
355
|
+
}
|
|
356
|
+
return dist;
|
|
357
|
+
}
|
|
358
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '2.3.
|
|
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.
|
|
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",
|