@icex-labs/openclaw-memory-engine 5.1.1 → 5.2.1

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.
@@ -32,6 +32,14 @@
32
32
  "message": "Run memory_dashboard to regenerate the HTML dashboard. Do NOT output anything to the main chat.",
33
33
  "model": "anthropic/claude-sonnet-4-6",
34
34
  "description": "Daily dashboard refresh at 9:30am"
35
+ },
36
+ {
37
+ "id": "memory-graph-weekly",
38
+ "schedule": "0 5 * * 0",
39
+ "agent": "main",
40
+ "message": "Review the last 50 archival records via archival_search. For any that describe clear relationships between entities (person owns thing, person works at place, person has condition, etc.), add them via graph_add. Only add relationships you are confident about. Do NOT output anything to the main chat. Send summary to notify bot.",
41
+ "model": "anthropic/claude-sonnet-4-6",
42
+ "description": "Weekly: LLM-based graph triple extraction from recent archival"
35
43
  }
36
44
  ]
37
45
  }
package/index.js CHANGED
@@ -323,13 +323,32 @@ export default definePluginEntry({
323
323
  const old = records[idx].content;
324
324
  records[idx].content = params.content;
325
325
  records[idx].updated_at = new Date().toISOString();
326
- if (params.entity !== undefined) records[idx].entity = params.entity;
326
+
327
+ // Re-classify if entity not explicitly provided
328
+ if (params.entity !== undefined) {
329
+ records[idx].entity = params.entity;
330
+ } else {
331
+ const { classify } = await import("./lib/classifier.js");
332
+ const cls = await classify(params.content, wsp);
333
+ if (cls.entity !== "general") records[idx].entity = cls.entity;
334
+ records[idx].importance = cls.importance;
335
+ // Reuse embedding for indexing
336
+ if (cls.embedding) {
337
+ const embCache = loadEmbeddingCache(wsp);
338
+ embCache[params.id] = cls.embedding;
339
+ saveEmbeddingCache(wsp);
340
+ }
341
+ }
327
342
  if (params.tags !== undefined) records[idx].tags = params.tags;
328
343
  rewriteArchival(wsp, records);
329
- const embCache = loadEmbeddingCache(wsp);
330
- delete embCache[params.id];
331
- saveEmbeddingCache(wsp);
332
- indexEmbedding(wsp, records[idx]).catch(() => {});
344
+
345
+ // Re-embed if classify didn't already do it
346
+ if (params.entity !== undefined) {
347
+ const embCache = loadEmbeddingCache(wsp);
348
+ delete embCache[params.id];
349
+ saveEmbeddingCache(wsp);
350
+ indexEmbedding(wsp, records[idx]).catch(() => {});
351
+ }
333
352
  return text(`OK: Updated ${params.id}. Old: "${old.slice(0, 60)}..." → New: "${params.content.slice(0, 60)}..."`);
334
353
  },
335
354
  })));
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { loadArchival, appendRecord } from "./archival.js";
7
- import { loadEmbeddingCache, saveEmbeddingCache } from "./embedding.js";
7
+ import { loadEmbeddingCache, saveEmbeddingCache, cosineSimilarity } from "./embedding.js";
8
8
  import { classify } from "./classifier.js";
9
9
 
10
10
  /** Split text into sentence-level fact candidates. */
@@ -54,6 +54,12 @@ export async function consolidateText(ws, text, defaultEntity = "", defaultTags
54
54
  const inserted = [];
55
55
  const skipped = [];
56
56
 
57
+ // Load embedding cache for semantic dedup
58
+ const embCache = loadEmbeddingCache(ws);
59
+ const recentEmbeddings = existing.slice(-100)
60
+ .map((r) => embCache[r.id])
61
+ .filter(Boolean);
62
+
57
63
  for (const fact of candidates) {
58
64
  const factLower = fact.toLowerCase();
59
65
  if (isDuplicate(factLower, existingTexts)) {
@@ -62,6 +68,17 @@ export async function consolidateText(ws, text, defaultEntity = "", defaultTags
62
68
  }
63
69
 
64
70
  const { entity, importance, embedding } = await classify(fact, ws);
71
+
72
+ // Semantic dedup: skip if cosine similarity > 0.92 with any recent record
73
+ if (embedding && recentEmbeddings.length > 0) {
74
+ const isSemDupe = recentEmbeddings.some(
75
+ (emb) => cosineSimilarity(embedding, emb) > 0.92,
76
+ );
77
+ if (isSemDupe) {
78
+ skipped.push(fact.slice(0, 60));
79
+ continue;
80
+ }
81
+ }
65
82
  const finalEntity = (entity !== "general") ? entity : defaultEntity || "general";
66
83
 
67
84
  const record = appendRecord(ws, {
package/lib/reflection.js CHANGED
@@ -9,6 +9,7 @@
9
9
  import { loadArchival } from "./archival.js";
10
10
  import { loadEpisodes } from "./episodes.js";
11
11
  import { loadGraph } from "./graph.js";
12
+ import { readCore } from "./core.js";
12
13
 
13
14
  /**
14
15
  * Analyze recent memory for patterns and trends.
@@ -132,9 +133,27 @@ export function analyzePatterns(ws, windowDays = 7) {
132
133
  neglected_entities: neglected.slice(0, 10),
133
134
  forgetting_candidates: forgettingCandidates.length,
134
135
  },
136
+ // Core memory focus age check
137
+ stale_focus: checkStaleFocus(ws),
135
138
  };
136
139
  }
137
140
 
141
+ /**
142
+ * Check if current_focus items in core memory are stale (>14 days since last update).
143
+ */
144
+ function checkStaleFocus(ws) {
145
+ try {
146
+ const core = readCore(ws);
147
+ const updatedAt = core._meta?.updated_at;
148
+ if (!updatedAt) return [];
149
+ const daysSinceUpdate = (Date.now() - new Date(updatedAt).getTime()) / 86400000;
150
+ if (daysSinceUpdate > 14 && Array.isArray(core.current_focus) && core.current_focus.length > 0) {
151
+ return core.current_focus;
152
+ }
153
+ } catch { /* ignore */ }
154
+ return [];
155
+ }
156
+
138
157
  /**
139
158
  * Format analysis into a human-readable report for the agent.
140
159
  */
@@ -182,6 +201,14 @@ export function formatReflection(analysis) {
182
201
  lines.push(``, `⚠️ ${analysis.health.forgetting_candidates} facts below forgetting threshold — consider archival_deduplicate or cleanup`);
183
202
  }
184
203
 
204
+ if (analysis.stale_focus && analysis.stale_focus.length > 0) {
205
+ lines.push(``, `⚠️ Core memory current_focus not updated in >14 days. Review these items:`);
206
+ for (const item of analysis.stale_focus) {
207
+ lines.push(` - ${item}`);
208
+ }
209
+ lines.push(` Use core_memory_replace to update or remove completed items.`);
210
+ }
211
+
185
212
  lines.push(``, `Totals: ${analysis.health.total_archival} facts, ${analysis.health.total_episodes} episodes, ${analysis.health.total_graph} graph triples`);
186
213
 
187
214
  return lines.join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icex-labs/openclaw-memory-engine",
3
- "version": "5.1.1",
3
+ "version": "5.2.1",
4
4
  "description": "MemGPT-style hierarchical memory plugin for OpenClaw — core memory block + archival storage with semantic search",
5
5
  "type": "module",
6
6
  "main": "index.js",
package/setup.sh CHANGED
@@ -384,22 +384,53 @@ except:
384
384
 
385
385
  echo " Agents found: $AGENTS"
386
386
 
387
- # Register main agent crons (shared across all agents using default workspace)
388
- register_cron "memory-reflect-daily" "0 9 * * *" "main" \
389
- "Run memory_reflect with window_days=7. If you notice patterns, store via archival_insert with tags=['reflection']. Do NOT output to main chat." \
390
- "Daily reflection: analyze memory patterns"
391
-
392
- register_cron "memory-consolidate-6h" "0 */6 * * *" "main" \
393
- "Read today's daily log. If it has content not in archival, run memory_consolidate. Then archival_stats. Do NOT output to main chat." \
394
- "Auto-consolidate daily logs every 6 hours"
395
-
396
- register_cron "memory-dedup-weekly" "0 4 * * 0" "main" \
397
- "Run archival_deduplicate with apply=true. Then archival_stats. Do NOT output to main chat." \
398
- "Weekly dedup: clean near-duplicate records"
399
-
400
- register_cron "memory-dashboard-daily" "30 9 * * *" "main" \
401
- "Run memory_dashboard to regenerate the HTML dashboard. Do NOT output to main chat." \
402
- "Daily dashboard refresh for main agent" 30000
387
+ # Register crons from JSON definition file (single source of truth)
388
+ CRON_JSON="$PLUGIN_DIR/extras/auto-consolidation-crons.json"
389
+ if [ -f "$CRON_JSON" ] && command -v python3 &>/dev/null; then
390
+ python3 -c "
391
+ import json, subprocess, sys
392
+
393
+ with open('$CRON_JSON') as f:
394
+ crons = json.load(f).get('crons', [])
395
+
396
+ existing = '''$EXISTING_CRONS'''
397
+ tz = '''$TZ_IANA'''
398
+
399
+ for c in crons:
400
+ name = c['id']
401
+ if name in existing:
402
+ print(f'⏭️ Cron \"{name}\" already exists')
403
+ continue
404
+ agent = c.get('agent', 'main')
405
+ cmd = [
406
+ 'openclaw', 'cron', 'add',
407
+ '--name', name,
408
+ '--cron', c['schedule'],
409
+ '--tz', tz,
410
+ '--agent', agent,
411
+ '--session', 'isolated',
412
+ '--model', c.get('model', 'anthropic/claude-sonnet-4-6'),
413
+ '--message', c['message'],
414
+ '--description', c.get('description', ''),
415
+ '--timeout', '60000',
416
+ ]
417
+ result = subprocess.run(cmd, capture_output=True, text=True)
418
+ if result.returncode == 0:
419
+ print(f'✅ Cron \"{name}\" ({agent}) registered')
420
+ else:
421
+ print(f'⚠️ Cron \"{name}\" failed (gateway not running?)')
422
+ " 2>/dev/null
423
+ else
424
+ echo "⚠️ Cron JSON not found or python3 missing — registering defaults manually"
425
+ register_cron "memory-reflect-daily" "0 9 * * *" "main" \
426
+ "Run memory_reflect. Do NOT output to main chat." "Daily reflection"
427
+ register_cron "memory-consolidate-6h" "0 */6 * * *" "main" \
428
+ "Run memory_consolidate on today's daily log. Do NOT output to main chat." "Auto-consolidate"
429
+ register_cron "memory-dedup-weekly" "0 4 * * 0" "main" \
430
+ "Run archival_deduplicate with apply=true. Do NOT output to main chat." "Weekly dedup"
431
+ register_cron "memory-dashboard-daily" "30 9 * * *" "main" \
432
+ "Run memory_dashboard. Do NOT output to main chat." "Dashboard refresh" 30000
433
+ fi
403
434
 
404
435
  # Register per-agent crons for agents with separate workspaces
405
436
  STAGGER=0
@@ -441,7 +472,22 @@ else
441
472
  echo "⚠️ openclaw CLI not found — skipping cron registration"
442
473
  fi
443
474
 
444
- # --- 9. Validate config ---
475
+ # --- 9. Track installed version ---
476
+ INSTALLED_VERSION=$(python3 -c "
477
+ import json
478
+ with open('$PLUGIN_DIR/package.json') as f: print(json.load(f).get('version','unknown'))
479
+ " 2>/dev/null || echo "unknown")
480
+ PREV_VERSION=""
481
+ VERSION_FILE="$MEMORY_DIR/.memory-engine-version"
482
+ [ -f "$VERSION_FILE" ] && PREV_VERSION=$(cat "$VERSION_FILE")
483
+ echo "$INSTALLED_VERSION" > "$VERSION_FILE"
484
+
485
+ if [ -n "$PREV_VERSION" ] && [ "$PREV_VERSION" != "$INSTALLED_VERSION" ]; then
486
+ echo ""
487
+ echo "📦 Upgraded: $PREV_VERSION → $INSTALLED_VERSION"
488
+ fi
489
+
490
+ # --- 10. Validate config ---
445
491
  echo ""
446
492
  if command -v openclaw &>/dev/null; then
447
493
  openclaw config validate 2>&1 && echo "✅ Config valid" || echo "❌ Config validation failed"