@icex-labs/openclaw-memory-engine 3.5.4 โ†’ 4.0.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.
@@ -163,3 +163,19 @@ for (const { path, tag } of files) {
163
163
 
164
164
  console.log(`\nโœ… Migration complete: ${inserted} facts imported, ${skipped} skipped (duplicates)`);
165
165
  console.log(`Total archival: ${existingContent.size} records`);
166
+
167
+ // --- Post-migration quality pass ---
168
+ if (inserted > 0) {
169
+ console.log(`\n๐Ÿ“Š Running post-migration quality pass...`);
170
+ try {
171
+ // Dynamic import of quality module (relative to plugin install dir)
172
+ // Since this script is in extras/, quality.js is at ../lib/quality.js
173
+ const qualityPath = join(import.meta.dirname, "..", "lib", "quality.js");
174
+ const { runQualityPass, formatQualityReport } = await import(qualityPath);
175
+ const result = runQualityPass(WS);
176
+ console.log(formatQualityReport(result));
177
+ } catch (e) {
178
+ console.log(`โš ๏ธ Quality pass skipped: ${e.message}`);
179
+ console.log(` Run manually: openclaw agent -m 'memory_quality'`);
180
+ }
181
+ }
package/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * MemGPT-style hierarchical memory plugin for OpenClaw.
5
5
  *
6
- * Tools (19):
6
+ * Tools (20):
7
7
  * Core: core_memory_read, core_memory_replace, core_memory_append
8
8
  * Archival: archival_insert, archival_search, archival_update, archival_delete, archival_stats
9
9
  * Graph: graph_query, graph_add
@@ -11,7 +11,7 @@
11
11
  * Reflection: memory_reflect
12
12
  * Maintenance: archival_deduplicate, memory_consolidate
13
13
  * Backup: memory_export, memory_import
14
- * Admin: memory_migrate, memory_dashboard
14
+ * Admin: memory_migrate, memory_dashboard, memory_quality
15
15
  */
16
16
 
17
17
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
@@ -29,6 +29,7 @@ import { exportMemory, importMemory } from "./lib/backup.js";
29
29
  import { queryGraph, addTriple, extractTriples } from "./lib/graph.js";
30
30
  import { saveEpisode, recallEpisodes, indexEpisodeEmbedding } from "./lib/episodes.js";
31
31
  import { analyzePatterns, formatReflection } from "./lib/reflection.js";
32
+ import { runQualityPass, formatQualityReport } from "./lib/quality.js";
32
33
  import { migrateFromJsonl } from "./lib/store-sqlite.js";
33
34
  import { generateDashboard } from "./lib/dashboard.js";
34
35
 
@@ -657,5 +658,32 @@ export default definePluginEntry({
657
658
  }
658
659
  },
659
660
  })));
661
+
662
+ // โ”€โ”€โ”€ memory_quality โ”€โ”€โ”€
663
+ api.registerTool(withAgent((agentId) => ({
664
+ name: "memory_quality",
665
+ description:
666
+ "Run a data quality pass: re-classify 'general' entities with richer patterns, re-rate flat importance scores using domain rules, extract missing knowledge graph triples, and generate episodes from daily record clusters. Run after migration or periodically to improve memory quality.",
667
+ parameters: {
668
+ type: "object",
669
+ properties: {
670
+ skip_graph: { type: "boolean", description: "Skip graph triple extraction (default: false)" },
671
+ skip_episodes: { type: "boolean", description: "Skip episode generation (default: false)" },
672
+ },
673
+ additionalProperties: false,
674
+ },
675
+ async execute(_id, params) {
676
+ const wsp = ws(agentId, params);
677
+ try {
678
+ const result = runQualityPass(wsp, {
679
+ skipGraph: params.skip_graph || false,
680
+ skipEpisodes: params.skip_episodes || false,
681
+ });
682
+ return text(formatQualityReport(result));
683
+ } catch (e) {
684
+ return text(`ERROR: Quality pass failed: ${e.message}`);
685
+ }
686
+ },
687
+ })));
660
688
  },
661
689
  });
package/lib/quality.js ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Data quality engine: re-classify entities, re-rate importance,
3
+ * extract missing graph triples, generate episodes from summaries.
4
+ */
5
+
6
+ import { loadArchival, rewriteArchival } from "./archival.js";
7
+ import { addTriple, extractTriples, loadGraph } from "./graph.js";
8
+ import { saveEpisode, loadEpisodes } from "./episodes.js";
9
+
10
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
11
+ // Extended entity patterns (much richer than consolidate.js)
12
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
13
+
14
+ const ENTITY_PATTERNS = [
15
+ // People / family
16
+ [/\b(wife|husband|spouse|่€ๅฉ†|่€ๅ…ฌ|ๅคชๅคช|ไธˆๅคซ|ๅฆปๅญ)\b/i, "family"],
17
+ [/\b(son|daughter|child|kid|ๅ„ฟๅญ|ๅฅณๅ„ฟ|ๅญฉๅญ)\b/i, "family"],
18
+ [/\b(mom|dad|mother|father|parent|ๅฆˆ|็ˆธ|็ˆถๆฏ)\b/i, "family"],
19
+
20
+ // Finance
21
+ [/\b(IBKR|Interactive Brokers|broker|brokerage)\b/i, "finance"],
22
+ [/\b(TFSA|RRSP|RESP|401k|IRA|pension)\b/i, "finance"],
23
+ [/\b(invest|portfolio|NAV|stock|ETF|QQQ|VOO|dividend)\b/i, "finance"],
24
+ [/\b(HELOC|mortgage|loan|credit|debt|ๅˆฉ็އ|rate)\b/i, "finance"],
25
+ [/\b(bank|RBC|TD|BMO|Scotiabank|CIBC)\b/i, "finance"],
26
+ [/\b(budget|expense|income|salary|payment|pay|ๆŠฅ็จŽ|tax)\b/i, "finance"],
27
+ [/\b(accountant|bookkeep|ไผš่ฎก)\b/i, "finance"],
28
+ [/\b(\$\d|CAD|USD|ไธ‡|ๅƒ)\b/i, "finance"],
29
+
30
+ // Immigration / legal
31
+ [/\b(immigration|immigrant|PR|permanent resident|็งปๆฐ‘)\b/i, "immigration"],
32
+ [/\b(IRCC|CBSA|ATIP|NSIRA|Mandamus|IMM-)\b/i, "immigration"],
33
+ [/\b(visa|work permit|็ญพ่ฏ|ๅทฅ็ญพ)\b/i, "immigration"],
34
+ [/\b(lawyer|attorney|paralegal|ๅพ‹ๅธˆ|ๆณ•ๅพ‹)\b/i, "legal"],
35
+ [/\b(petition|complaint|CHRC|tribunal|court|ๆกˆ)\b/i, "legal"],
36
+
37
+ // Health
38
+ [/\b(doctor|physician|GP|ๅŒป็”Ÿ|ไธปๆฒป|Dr\.)\b/i, "health"],
39
+ [/\b(hospital|clinic|medical|่ฏŠๆ‰€|ๅŒป้™ข)\b/i, "health"],
40
+ [/\b(medication|medicine|drug|pill|tablet|่ฏ|ๅค„ๆ–น)\b/i, "health"],
41
+ [/\b(cetirizine|urticaria|่จ้บป็–น|allergy|่ฟ‡ๆ•)\b/i, "health"],
42
+ [/\b(health|symptom|diagnosis|ไฝ“ๆฃ€|ๆฃ€ๆŸฅ|screening)\b/i, "health"],
43
+ [/\b(dental|dentist|vision|eye|็‰™|็œผ)\b/i, "health"],
44
+
45
+ // Vehicles
46
+ [/\b(car|vehicle|SUV|sedan|truck|van|minivan|่ฝฆ)\b/i, "vehicles"],
47
+ [/\b(Tesla|Toyota|Lexus|BMW|Mercedes|Cadillac|Honda|Audi)\b/i, "vehicles"],
48
+ [/\b(Escalade|GX550|ES350|Sienna|Model [3SXY])\b/i, "vehicles"],
49
+ [/\b(tire|tyre|PPF|wrap|oil change|maintenance|ไฟๅ…ป|่ฝฎ่ƒŽ)\b/i, "vehicles"],
50
+ [/\b(insurance|ไฟ้™ฉ|Desjardins|policy)\b/i, "vehicles"],
51
+
52
+ // Infrastructure / DevOps
53
+ [/\b(k3d|k3s|k8s|kubernetes|cluster|pod|deploy)\b/i, "infrastructure"],
54
+ [/\b(ArgoCD|Helm|kubectl|GitOps|CI|CD|pipeline)\b/i, "infrastructure"],
55
+ [/\b(Docker|container|image|registry|GHCR)\b/i, "infrastructure"],
56
+ [/\b(U9|prod|production|staging|dev cluster)\b/i, "infrastructure"],
57
+ [/\b(SOPS|secret|encrypt|cert|SSL|TLS)\b/i, "infrastructure"],
58
+
59
+ // OpenClaw / AI
60
+ [/\b(OpenClaw|openclaw|gateway|plugin|hook)\b/i, "openclaw"],
61
+ [/\b(agent|session|compaction|memory|embedding)\b/i, "openclaw"],
62
+ [/\b(LLM|Claude|Anthropic|GPT|OpenAI|AI|token)\b/i, "ai"],
63
+ [/\b(prompt|context window|model|inference)\b/i, "ai"],
64
+
65
+ // Quant / trading
66
+ [/\b(quant|quantitative|backtest|backtesting)\b/i, "quant"],
67
+ [/\b(trading|trade|signal|strategy|turtle|ๆตท้พŸ)\b/i, "quant"],
68
+ [/\b(Sharpe|drawdown|ๅ›žๆ’ค|ๅนดๅŒ–|annualized)\b/i, "quant"],
69
+ [/\b(paper trading|live trading|order|position)\b/i, "quant"],
70
+
71
+ // Messaging
72
+ [/\b(Telegram|Discord|WhatsApp|Slack|bot|channel)\b/i, "messaging"],
73
+
74
+ // Property / home
75
+ [/\b(house|home|condo|apartment|property|ๆˆฟ|็งŸ)\b/i, "property"],
76
+ [/\b(NAS|Synology|backup|Time Machine)\b/i, "property"],
77
+ [/\b(lawn|garden|yard|snow|่‰ๅช|้“ฒ้›ช)\b/i, "property"],
78
+
79
+ // Education / kids
80
+ [/\b(school|class|homework|exam|test|ๅญฆๆ ก|ไฝœไธš)\b/i, "education"],
81
+ [/\b(kindergarten|grade|teacher|่€ๅธˆ)\b/i, "education"],
82
+ [/\b(swimming|skating|skiing|hockey|lesson|่ฏพ)\b/i, "education"],
83
+ [/\b(Science Fair|concert|recital|่กจๆผ”)\b/i, "education"],
84
+
85
+ // Projects / SaaS
86
+ [/\b(icex|SaaS|MVP|startup|product|launch)\b/i, "project"],
87
+ [/\b(ESP32|Arduino|IoT|hardware|sensor)\b/i, "project"],
88
+
89
+ // Shopping / daily
90
+ [/\b(Costco|Amazon|Walmart|shopping|่ดญ็‰ฉ|ไนฐ)\b/i, "shopping"],
91
+ [/\b(flight|airline|Air Canada|travel|trip|ๆœบ็ฅจ|้ฃž)\b/i, "travel"],
92
+ [/\b(restaurant|food|meal|dinner|lunch|ๅƒ|้ฅญ)\b/i, "daily"],
93
+ ];
94
+
95
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
96
+ // Importance rules
97
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
98
+
99
+ const IMPORTANCE_RULES = [
100
+ // High (8-9): critical life matters
101
+ { match: /\b(immigration|PR|IRCC|CBSA|Mandamus|visa|NSIRA|CHRC|petition|lawsuit|court)\b/i, importance: 9 },
102
+ { match: /\b(IBKR|NAV|portfolio|invest|\$\d{4,}|ไธ‡|HELOC|mortgage)\b/i, importance: 8 },
103
+ { match: /\b(doctor|hospital|medication|diagnosis|surgery|health insurance|AHCIP)\b/i, importance: 8 },
104
+ { match: /\b(lawyer|attorney|legal|ๅพ‹ๅธˆ)\b/i, importance: 8 },
105
+ { match: /\b(ๆฐธ่ฟœไธ่ฆ|NEVER|CRITICAL|ไธฅ็ฆ|ๅฟ…้กป|MUST)\b/i, importance: 9 },
106
+ { match: /\b(VIN|policy number|case number|account number|IMM-)\b/i, importance: 8 },
107
+
108
+ // Medium-high (7): important but not critical
109
+ { match: /\b(ArgoCD|GitOps|k3d|U9|prod|deploy|CI)\b/i, importance: 6 },
110
+ { match: /\b(quant|backtest|trading|signal|Sharpe)\b/i, importance: 7 },
111
+ { match: /\b(GX550|Escalade|ES350|car insurance)\b/i, importance: 6 },
112
+ { match: /\b(OpenClaw|gateway|plugin|config)\b/i, importance: 6 },
113
+ { match: /\b(icex|SaaS|MVP|ESP32)\b/i, importance: 6 },
114
+
115
+ // Low (3): ephemeral
116
+ { match: /\b(swimming lesson|concert|recital|playdate)\b/i, importance: 3 },
117
+ { match: /\b(weather|ๅคฉๆฐ”)\b/i, importance: 2 },
118
+ { match: /\b(heartbeat|HEARTBEAT_OK|session start|daily log)\b/i, importance: 2 },
119
+ { match: /\b(good morning|good night|ๆ—ฉไธŠๅฅฝ|ๆ™šๅฎ‰)\b/i, importance: 2 },
120
+ ];
121
+
122
+ function inferImportance(content, currentImportance) {
123
+ // Only re-rate if currently at default (5)
124
+ if (currentImportance !== 5) return currentImportance;
125
+
126
+ for (const rule of IMPORTANCE_RULES) {
127
+ if (rule.match.test(content)) return rule.importance;
128
+ }
129
+ return 5; // keep default if no rule matches
130
+ }
131
+
132
+ function inferEntity(content, currentEntity) {
133
+ // Only re-classify if currently "general" or empty
134
+ if (currentEntity && currentEntity !== "general") return currentEntity;
135
+
136
+ for (const [pattern, entity] of ENTITY_PATTERNS) {
137
+ if (pattern.test(content)) return entity;
138
+ }
139
+ return currentEntity || "general";
140
+ }
141
+
142
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
143
+ // Quality pass
144
+ // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
145
+
146
+ /**
147
+ * Run a full quality pass over archival records.
148
+ * @returns {{ reclassified, rerated, triplesAdded, episodesGenerated }}
149
+ */
150
+ export function runQualityPass(ws, options = {}) {
151
+ const records = loadArchival(ws);
152
+ const existingGraph = loadGraph(ws);
153
+ const existingTripleSet = new Set(
154
+ existingGraph.map((t) => `${t.s}|${t.r}|${t.o}`.toLowerCase()),
155
+ );
156
+
157
+ let reclassified = 0;
158
+ let rerated = 0;
159
+ let triplesAdded = 0;
160
+
161
+ for (const record of records) {
162
+ // 1. Re-classify entity
163
+ const newEntity = inferEntity(record.content, record.entity);
164
+ if (newEntity !== record.entity) {
165
+ record.entity = newEntity;
166
+ reclassified++;
167
+ }
168
+
169
+ // 2. Re-rate importance
170
+ const newImportance = inferImportance(record.content, record.importance ?? 5);
171
+ if (newImportance !== (record.importance ?? 5)) {
172
+ record.importance = newImportance;
173
+ rerated++;
174
+ }
175
+
176
+ // 3. Extract graph triples
177
+ if (!options.skipGraph) {
178
+ const triples = extractTriples(record.content);
179
+ for (const t of triples) {
180
+ const key = `${t.s}|${t.r}|${t.o}`.toLowerCase();
181
+ if (!existingTripleSet.has(key)) {
182
+ const added = addTriple(ws, t.s, t.r, t.o, record.id);
183
+ if (added) {
184
+ existingTripleSet.add(key);
185
+ triplesAdded++;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+
192
+ // Save updated records
193
+ if (reclassified > 0 || rerated > 0) {
194
+ rewriteArchival(ws, records);
195
+ }
196
+
197
+ // 4. Generate episodes from weekly summaries (if episodes are sparse)
198
+ let episodesGenerated = 0;
199
+ if (!options.skipEpisodes) {
200
+ episodesGenerated = generateEpisodesFromRecords(ws, records);
201
+ }
202
+
203
+ return { reclassified, rerated, triplesAdded, episodesGenerated, total: records.length };
204
+ }
205
+
206
+ /**
207
+ * Generate episode summaries from clusters of records on the same day.
208
+ */
209
+ function generateEpisodesFromRecords(ws, records) {
210
+ const episodes = loadEpisodes(ws);
211
+ const existingDates = new Set(episodes.map((e) => e.ts?.slice(0, 10)));
212
+
213
+ // Group records by date
214
+ const byDate = {};
215
+ for (const r of records) {
216
+ if (!r.ts) continue;
217
+ const date = r.ts.slice(0, 10);
218
+ if (!byDate[date]) byDate[date] = [];
219
+ byDate[date].push(r);
220
+ }
221
+
222
+ let generated = 0;
223
+ for (const [date, dayRecords] of Object.entries(byDate)) {
224
+ // Skip if episode already exists for this date, or too few records
225
+ if (existingDates.has(date) || dayRecords.length < 3) continue;
226
+
227
+ // Aggregate topics and entities
228
+ const topics = [...new Set(dayRecords.map((r) => r.entity).filter((e) => e && e !== "general"))];
229
+ const topContent = dayRecords
230
+ .sort((a, b) => (b.importance || 5) - (a.importance || 5))
231
+ .slice(0, 5)
232
+ .map((r) => r.content.slice(0, 80))
233
+ .join("; ");
234
+
235
+ saveEpisode(ws, {
236
+ summary: `${date}: ${topContent}`,
237
+ decisions: [],
238
+ mood: "",
239
+ topics: topics.slice(0, 5),
240
+ participants: [],
241
+ source: "quality-pass",
242
+ });
243
+ generated++;
244
+ }
245
+
246
+ return generated;
247
+ }
248
+
249
+ /**
250
+ * Format quality pass results into a report.
251
+ */
252
+ export function formatQualityReport(result) {
253
+ const lines = [
254
+ `๐Ÿ“Š Memory Quality Pass Complete`,
255
+ ``,
256
+ ` Records scanned: ${result.total}`,
257
+ ` Entities re-classified: ${result.reclassified}`,
258
+ ` Importance re-rated: ${result.rerated}`,
259
+ ` Graph triples extracted: ${result.triplesAdded}`,
260
+ ` Episodes generated: ${result.episodesGenerated}`,
261
+ ];
262
+
263
+ if (result.reclassified === 0 && result.rerated === 0 && result.triplesAdded === 0 && result.episodesGenerated === 0) {
264
+ lines.push(``, ` All data is already high quality. Nothing to fix.`);
265
+ }
266
+
267
+ return lines.join("\n");
268
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icex-labs/openclaw-memory-engine",
3
- "version": "3.5.4",
3
+ "version": "4.0.0",
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",