@icex-labs/openclaw-memory-engine 3.3.2 → 3.4.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 +165 -0
- package/package.json +1 -1
- package/setup.sh +34 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate-legacy.mjs — Import existing file-based memory into archival.jsonl
|
|
4
|
+
*
|
|
5
|
+
* Scans workspace for: MEMORY.md, memory/*.md, memory/weekly/*.md, memory/topics/*.md
|
|
6
|
+
* Extracts facts, deduplicates, and appends to memory/archival.jsonl.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node migrate-legacy.mjs [workspace_path]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, appendFileSync, existsSync, readdirSync } from "node:fs";
|
|
12
|
+
import { join, basename } from "node:path";
|
|
13
|
+
|
|
14
|
+
const WS = process.argv[2] || process.env.OPENCLAW_WORKSPACE || join(process.env.HOME || "/tmp", ".openclaw", "workspace");
|
|
15
|
+
const ARCHIVAL = join(WS, "memory", "archival.jsonl");
|
|
16
|
+
|
|
17
|
+
console.log(`🧠 Legacy memory migration`);
|
|
18
|
+
console.log(` Workspace: ${WS}`);
|
|
19
|
+
console.log(` Archival: ${ARCHIVAL}`);
|
|
20
|
+
console.log(``);
|
|
21
|
+
|
|
22
|
+
// Load existing archival for dedup
|
|
23
|
+
const existingContent = new Set();
|
|
24
|
+
if (existsSync(ARCHIVAL)) {
|
|
25
|
+
for (const line of readFileSync(ARCHIVAL, "utf-8").trim().split("\n").filter(Boolean)) {
|
|
26
|
+
try { existingContent.add(JSON.parse(line).content?.toLowerCase()); } catch {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
console.log(`Existing archival: ${existingContent.size} records`);
|
|
30
|
+
|
|
31
|
+
// Generic entity inference (no personal data)
|
|
32
|
+
const ENTITY_PATTERNS = [
|
|
33
|
+
[/\b(IBKR|Interactive Brokers|NAV|portfolio|投资|HELOC|mortgage|finance)/i, "finance"],
|
|
34
|
+
[/\b(immigration|PR|IRCC|CBSA|visa|律师|lawyer|petition)/i, "immigration"],
|
|
35
|
+
[/\b(quant|trading|backtest|signal|portfolio|Sharpe)/i, "trading"],
|
|
36
|
+
[/\b(doctor|医生|hospital|health|medication|药|体检|clinic)/i, "health"],
|
|
37
|
+
[/\b(car|vehicle|SUV|sedan|truck)\b/i, "vehicles"],
|
|
38
|
+
[/\b(k3d|ArgoCD|Helm|kubectl|GitOps|cluster|deploy|CI|CD)/i, "infrastructure"],
|
|
39
|
+
[/\b(OpenClaw|gateway|plugin|session|agent|memory|compaction)/i, "openclaw"],
|
|
40
|
+
[/\b(Discord|Telegram|Slack|bot|channel)/i, "messaging"],
|
|
41
|
+
[/\b(school|university|college|学校|education)/i, "education"],
|
|
42
|
+
[/\b(house|home|property|rent|房)/i, "property"],
|
|
43
|
+
[/\b(lawyer|legal|court|lawsuit|案|诉)/i, "legal"],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
function inferEntity(text) {
|
|
47
|
+
for (const [pat, name] of ENTITY_PATTERNS) {
|
|
48
|
+
if (pat.test(text)) return name;
|
|
49
|
+
}
|
|
50
|
+
return "general";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function extractFacts(text) {
|
|
54
|
+
const facts = [];
|
|
55
|
+
for (const line of text.split(/\n/).map((l) => l.trim()).filter(Boolean)) {
|
|
56
|
+
if (line.startsWith("#") || line.length < 15) continue;
|
|
57
|
+
if (/^(##|===|---|\*\*\*|```|>|\|)/.test(line)) continue;
|
|
58
|
+
const sentences = line.split(/(?<=[。.!!??;;])\s*/).filter(Boolean);
|
|
59
|
+
for (const s of sentences) {
|
|
60
|
+
const clean = s.replace(/^[-*•]\s*/, "").replace(/^\d+\.\s*/, "").trim();
|
|
61
|
+
if (clean.length >= 15 && clean.length <= 500) facts.push(clean);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return facts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Collect all legacy files
|
|
68
|
+
const files = [];
|
|
69
|
+
|
|
70
|
+
// MEMORY.md
|
|
71
|
+
const memoryMd = join(WS, "MEMORY.md");
|
|
72
|
+
if (existsSync(memoryMd)) files.push({ path: memoryMd, tag: "long-term" });
|
|
73
|
+
|
|
74
|
+
// memory/*.md (daily logs)
|
|
75
|
+
const memDir = join(WS, "memory");
|
|
76
|
+
if (existsSync(memDir)) {
|
|
77
|
+
for (const f of readdirSync(memDir).filter((f) => /\.md$/.test(f) && f !== ".abstract")) {
|
|
78
|
+
files.push({ path: join(memDir, f), tag: "daily" });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// memory/weekly/*.md
|
|
83
|
+
const weeklyDir = join(WS, "memory", "weekly");
|
|
84
|
+
if (existsSync(weeklyDir)) {
|
|
85
|
+
for (const f of readdirSync(weeklyDir).filter((f) => f.endsWith(".md"))) {
|
|
86
|
+
files.push({ path: join(weeklyDir, f), tag: "weekly" });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// memory/topics/*.md
|
|
91
|
+
const topicDir = join(WS, "memory", "topics");
|
|
92
|
+
if (existsSync(topicDir)) {
|
|
93
|
+
for (const f of readdirSync(topicDir).filter((f) => f.endsWith(".md"))) {
|
|
94
|
+
files.push({ path: join(topicDir, f), tag: "topic" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
console.log("\nNo legacy memory files found. Nothing to migrate.");
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log(`Found ${files.length} files to scan\n`);
|
|
104
|
+
|
|
105
|
+
let inserted = 0;
|
|
106
|
+
let skipped = 0;
|
|
107
|
+
|
|
108
|
+
for (const { path, tag } of files) {
|
|
109
|
+
const content = readFileSync(path, "utf-8");
|
|
110
|
+
const facts = extractFacts(content);
|
|
111
|
+
let fileInserted = 0;
|
|
112
|
+
|
|
113
|
+
for (const fact of facts) {
|
|
114
|
+
const factLower = fact.toLowerCase();
|
|
115
|
+
|
|
116
|
+
// Exact dedup
|
|
117
|
+
if (existingContent.has(factLower)) {
|
|
118
|
+
skipped++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Keyword overlap dedup (>75% overlap = skip)
|
|
123
|
+
let isDupe = false;
|
|
124
|
+
const factWords = new Set(factLower.split(/\s+/).filter((w) => w.length > 2));
|
|
125
|
+
if (factWords.size > 0) {
|
|
126
|
+
for (const ex of existingContent) {
|
|
127
|
+
const exWords = new Set(ex.split(/\s+/).filter((w) => w.length > 2));
|
|
128
|
+
let overlap = 0;
|
|
129
|
+
for (const w of factWords) {
|
|
130
|
+
if (exWords.has(w)) overlap++;
|
|
131
|
+
}
|
|
132
|
+
if (overlap / factWords.size > 0.75) {
|
|
133
|
+
isDupe = true;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (isDupe) {
|
|
139
|
+
skipped++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const record = {
|
|
144
|
+
id: `arch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
145
|
+
ts: new Date().toISOString(),
|
|
146
|
+
last_accessed: null,
|
|
147
|
+
access_count: 0,
|
|
148
|
+
importance: 5,
|
|
149
|
+
content: fact,
|
|
150
|
+
entity: inferEntity(fact),
|
|
151
|
+
tags: [tag],
|
|
152
|
+
source: "migration",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
appendFileSync(ARCHIVAL, JSON.stringify(record) + "\n", "utf-8");
|
|
156
|
+
existingContent.add(factLower);
|
|
157
|
+
inserted++;
|
|
158
|
+
fileInserted++;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (fileInserted > 0) console.log(` ${basename(path)}: +${fileInserted} facts`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`\n✅ Migration complete: ${inserted} facts imported, ${skipped} skipped (duplicates)`);
|
|
165
|
+
console.log(`Total archival: ${existingContent.size} records`);
|
package/package.json
CHANGED
package/setup.sh
CHANGED
|
@@ -106,6 +106,40 @@ else
|
|
|
106
106
|
echo "⏭️ archival.jsonl already exists ($lines records)"
|
|
107
107
|
fi
|
|
108
108
|
|
|
109
|
+
# --- 3b. Migrate legacy memory files into archival ---
|
|
110
|
+
if command -v node &>/dev/null && [ -f "$PLUGIN_DIR/extras/migrate-legacy.mjs" ]; then
|
|
111
|
+
# Check if there are legacy files to migrate
|
|
112
|
+
legacy_count=0
|
|
113
|
+
[ -f "$WORKSPACE/MEMORY.md" ] && legacy_count=$((legacy_count + 1))
|
|
114
|
+
legacy_count=$((legacy_count + $(ls "$MEMORY_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ')))
|
|
115
|
+
legacy_count=$((legacy_count + $(ls "$MEMORY_DIR"/weekly/*.md 2>/dev/null | wc -l | tr -d ' ')))
|
|
116
|
+
legacy_count=$((legacy_count + $(ls "$MEMORY_DIR"/topics/*.md 2>/dev/null | wc -l | tr -d ' ')))
|
|
117
|
+
|
|
118
|
+
archival_count=$(wc -l < "$MEMORY_DIR/archival.jsonl" 2>/dev/null | tr -d ' ' || echo "0")
|
|
119
|
+
|
|
120
|
+
if [ "$legacy_count" -gt 0 ] && [ "$archival_count" -lt 10 ]; then
|
|
121
|
+
echo ""
|
|
122
|
+
echo "📦 Found $legacy_count legacy memory files (MEMORY.md, daily logs, weekly summaries, topics)."
|
|
123
|
+
if $NON_INTERACTIVE; then
|
|
124
|
+
echo " Migrating automatically..."
|
|
125
|
+
node "$PLUGIN_DIR/extras/migrate-legacy.mjs" "$WORKSPACE" 2>&1 | tail -3
|
|
126
|
+
else
|
|
127
|
+
printf " Migrate into archival memory? [Y/n]: "
|
|
128
|
+
read -r migrate_answer
|
|
129
|
+
if [ "${migrate_answer:-Y}" != "n" ] && [ "${migrate_answer:-Y}" != "N" ]; then
|
|
130
|
+
node "$PLUGIN_DIR/extras/migrate-legacy.mjs" "$WORKSPACE" 2>&1 | tail -5
|
|
131
|
+
else
|
|
132
|
+
echo "⏭️ Skipping migration. Run manually later: node $PLUGIN_DIR/extras/migrate-legacy.mjs $WORKSPACE"
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
echo ""
|
|
136
|
+
else
|
|
137
|
+
if [ "$archival_count" -gt 10 ]; then
|
|
138
|
+
echo "⏭️ Archival already has $archival_count records, skipping migration"
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
109
143
|
# --- 4. Install memory-maintenance.sh ---
|
|
110
144
|
SCRIPTS_DIR="$WORKSPACE/scripts"
|
|
111
145
|
mkdir -p "$SCRIPTS_DIR"
|