@icex-labs/openclaw-memory-engine 3.5.4 ā 4.1.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.
- package/extras/migrate-legacy.mjs +16 -0
- package/index.js +60 -2
- package/lib/auto-capture.js +89 -0
- package/lib/quality.js +268 -0
- package/package.json +1 -1
|
@@ -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 (
|
|
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,8 +29,10 @@ 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";
|
|
35
|
+
import { captureMessage } from "./lib/auto-capture.js";
|
|
34
36
|
|
|
35
37
|
import { readFileSync } from "node:fs";
|
|
36
38
|
|
|
@@ -130,6 +132,35 @@ export default definePluginEntry({
|
|
|
130
132
|
} catch { /* ignore startup errors */ }
|
|
131
133
|
}, 10000); // delay 10s after gateway start
|
|
132
134
|
|
|
135
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
136
|
+
// Auto-capture hooks: passively store facts from conversations
|
|
137
|
+
// No reliance on agent calling tools ā memory happens automatically
|
|
138
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
139
|
+
|
|
140
|
+
api.registerHook("message:received", (event) => {
|
|
141
|
+
const ctx = event.context;
|
|
142
|
+
if (!ctx?.content) return;
|
|
143
|
+
|
|
144
|
+
// Resolve workspace from session key
|
|
145
|
+
const agentId = extractAgentId(event.sessionKey);
|
|
146
|
+
const wsDir = resolveWorkspace({ agentId });
|
|
147
|
+
|
|
148
|
+
captureMessage(wsDir, ctx.content, "user-message");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
api.registerHook("message:sent", (event) => {
|
|
152
|
+
const ctx = event.context;
|
|
153
|
+
if (!ctx?.content || !ctx?.success) return;
|
|
154
|
+
|
|
155
|
+
// Only capture agent replies, not system messages
|
|
156
|
+
if (ctx.content.length < 50) return;
|
|
157
|
+
|
|
158
|
+
const agentId = extractAgentId(event.sessionKey);
|
|
159
|
+
const wsDir = resolveWorkspace({ agentId });
|
|
160
|
+
|
|
161
|
+
captureMessage(wsDir, ctx.content, "agent-reply");
|
|
162
|
+
});
|
|
163
|
+
|
|
133
164
|
// āāā core_memory_read āāā
|
|
134
165
|
api.registerTool(withAgent((agentId) => ({
|
|
135
166
|
name: "core_memory_read",
|
|
@@ -657,5 +688,32 @@ export default definePluginEntry({
|
|
|
657
688
|
}
|
|
658
689
|
},
|
|
659
690
|
})));
|
|
691
|
+
|
|
692
|
+
// āāā memory_quality āāā
|
|
693
|
+
api.registerTool(withAgent((agentId) => ({
|
|
694
|
+
name: "memory_quality",
|
|
695
|
+
description:
|
|
696
|
+
"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.",
|
|
697
|
+
parameters: {
|
|
698
|
+
type: "object",
|
|
699
|
+
properties: {
|
|
700
|
+
skip_graph: { type: "boolean", description: "Skip graph triple extraction (default: false)" },
|
|
701
|
+
skip_episodes: { type: "boolean", description: "Skip episode generation (default: false)" },
|
|
702
|
+
},
|
|
703
|
+
additionalProperties: false,
|
|
704
|
+
},
|
|
705
|
+
async execute(_id, params) {
|
|
706
|
+
const wsp = ws(agentId, params);
|
|
707
|
+
try {
|
|
708
|
+
const result = runQualityPass(wsp, {
|
|
709
|
+
skipGraph: params.skip_graph || false,
|
|
710
|
+
skipEpisodes: params.skip_episodes || false,
|
|
711
|
+
});
|
|
712
|
+
return text(formatQualityReport(result));
|
|
713
|
+
} catch (e) {
|
|
714
|
+
return text(`ERROR: Quality pass failed: ${e.message}`);
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
})));
|
|
660
718
|
},
|
|
661
719
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-capture: hook into message:received and message:sent events
|
|
3
|
+
* to automatically extract and store facts in archival memory.
|
|
4
|
+
*
|
|
5
|
+
* No reliance on the agent calling tools ā memory happens passively.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { appendRecord } from "./archival.js";
|
|
9
|
+
import { indexEmbedding } from "./embedding.js";
|
|
10
|
+
import { extractTriples, addTriple } from "./graph.js";
|
|
11
|
+
import { resolveWorkspace } from "./paths.js";
|
|
12
|
+
|
|
13
|
+
// Minimum message length to consider for fact extraction
|
|
14
|
+
const MIN_LENGTH = 20;
|
|
15
|
+
|
|
16
|
+
// Skip patterns ā don't store these as facts
|
|
17
|
+
const SKIP_PATTERNS = [
|
|
18
|
+
/^(hi|hello|hey|ok|thanks|good morning|good night|ę©|ęå®|ä½ å„½|åÆ|儽ē|谢谢)/i,
|
|
19
|
+
/^HEARTBEAT_OK$/,
|
|
20
|
+
/^\//, // slash commands
|
|
21
|
+
/^(yes|no|yeah|nah|sure|maybe)$/i,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// High-value content patterns ā always store these
|
|
25
|
+
const HIGH_VALUE_PATTERNS = [
|
|
26
|
+
/\b(decided|decision|plan|scheduled|booked|bought|sold|paid|ē¾|ä¹°|å|é¢ēŗ¦|å³å®)\b/i,
|
|
27
|
+
/\b(doctor|lawyer|immigration|IRCC|IBKR|account|password|address|phone|email)\b/i,
|
|
28
|
+
/\b(remember|don't forget|ęé|č®°ä½|å«åæ)\b/i,
|
|
29
|
+
/\$\d{2,}/, // dollar amounts
|
|
30
|
+
/\b\d{4}-\d{2}-\d{2}\b/, // dates
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Entity inference (same as quality.js but lightweight)
|
|
34
|
+
const ENTITY_PATTERNS = [
|
|
35
|
+
[/\b(IBKR|invest|portfolio|HELOC|mortgage|bank|\$\d{3,})/i, "finance"],
|
|
36
|
+
[/\b(immigration|PR|IRCC|CBSA|visa|lawyer|å¾åø)/i, "immigration"],
|
|
37
|
+
[/\b(doctor|å»ē|hospital|health|medication|čÆ)/i, "health"],
|
|
38
|
+
[/\b(car|vehicle|Escalade|GX550|ES350|Tesla|tire|车)/i, "vehicles"],
|
|
39
|
+
[/\b(school|homework|exam|swimming|lesson|å¦ę ”|课)/i, "education"],
|
|
40
|
+
[/\b(deploy|k3d|ArgoCD|kubectl|CI|cluster)/i, "infrastructure"],
|
|
41
|
+
[/\b(quant|trading|backtest|signal|strategy)/i, "quant"],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function inferEntity(text) {
|
|
45
|
+
for (const [pat, name] of ENTITY_PATTERNS) {
|
|
46
|
+
if (pat.test(text)) return name;
|
|
47
|
+
}
|
|
48
|
+
return "conversation";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function shouldCapture(content) {
|
|
52
|
+
if (!content || content.length < MIN_LENGTH) return false;
|
|
53
|
+
if (SKIP_PATTERNS.some((p) => p.test(content.trim()))) return false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isHighValue(content) {
|
|
58
|
+
return HIGH_VALUE_PATTERNS.some((p) => p.test(content));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Process an incoming or outgoing message and auto-store if valuable.
|
|
63
|
+
*/
|
|
64
|
+
export function captureMessage(ws, content, source = "auto-capture") {
|
|
65
|
+
if (!shouldCapture(content)) return null;
|
|
66
|
+
|
|
67
|
+
const importance = isHighValue(content) ? 7 : 4;
|
|
68
|
+
const entity = inferEntity(content);
|
|
69
|
+
|
|
70
|
+
// Trim very long messages to first 500 chars
|
|
71
|
+
const trimmed = content.length > 500 ? content.slice(0, 497) + "..." : content;
|
|
72
|
+
|
|
73
|
+
const record = appendRecord(ws, {
|
|
74
|
+
content: trimmed,
|
|
75
|
+
entity,
|
|
76
|
+
tags: [source],
|
|
77
|
+
importance,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Background: index embedding + extract graph triples
|
|
81
|
+
indexEmbedding(ws, record).catch(() => {});
|
|
82
|
+
|
|
83
|
+
const triples = extractTriples(trimmed);
|
|
84
|
+
for (const t of triples) {
|
|
85
|
+
addTriple(ws, t.s, t.r, t.o, record.id);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return record;
|
|
89
|
+
}
|
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
|
+
"version": "4.1.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",
|