@icex-labs/openclaw-memory-engine 5.0.3 → 5.1.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.
- package/extras/migrate-legacy.mjs +18 -1
- package/index.js +1 -1
- package/lib/graph.js +48 -41
- package/lib/quality.js +39 -3
- package/package.json +1 -1
|
@@ -140,9 +140,26 @@ for (const { path, tag } of files) {
|
|
|
140
140
|
continue;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
// Infer date from filename (e.g., 2026-03-28.md → 2026-03-28, 2026-W13.md → week date)
|
|
144
|
+
const dateMatch = basename(path).match(/^(\d{4}-\d{2}-\d{2})/);
|
|
145
|
+
const weekMatch = basename(path).match(/^(\d{4})-W(\d{2})/);
|
|
146
|
+
let inferredTs;
|
|
147
|
+
if (dateMatch) {
|
|
148
|
+
inferredTs = dateMatch[1] + "T12:00:00Z";
|
|
149
|
+
} else if (weekMatch) {
|
|
150
|
+
// Approximate: week number × 7 days from Jan 1
|
|
151
|
+
const year = parseInt(weekMatch[1]);
|
|
152
|
+
const week = parseInt(weekMatch[2]);
|
|
153
|
+
const jan1 = new Date(year, 0, 1);
|
|
154
|
+
const weekDate = new Date(jan1.getTime() + (week - 1) * 7 * 86400000);
|
|
155
|
+
inferredTs = weekDate.toISOString();
|
|
156
|
+
} else {
|
|
157
|
+
inferredTs = new Date().toISOString();
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
const record = {
|
|
144
161
|
id: `arch-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
145
|
-
ts:
|
|
162
|
+
ts: inferredTs,
|
|
146
163
|
last_accessed: null,
|
|
147
164
|
access_count: 0,
|
|
148
165
|
importance: 5,
|
package/index.js
CHANGED
|
@@ -702,7 +702,7 @@ export default definePluginEntry({
|
|
|
702
702
|
async execute(_id, params) {
|
|
703
703
|
const wsp = ws(agentId, params);
|
|
704
704
|
try {
|
|
705
|
-
const result = runQualityPass(wsp, {
|
|
705
|
+
const result = await runQualityPass(wsp, {
|
|
706
706
|
skipGraph: params.skip_graph || false,
|
|
707
707
|
skipEpisodes: params.skip_episodes || false,
|
|
708
708
|
});
|
package/lib/graph.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Knowledge Graph: triple store (subject, relation, object).
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Storage: memory/graph.jsonl — one triple per line.
|
|
6
|
-
* Auto-extraction: pattern-based extraction from archival insert content.
|
|
3
|
+
* v5.1: strict extraction — only extract triples from clear, short, structured statements.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
6
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
@@ -12,7 +9,7 @@ import { graphPath } from "./paths.js";
|
|
|
12
9
|
|
|
13
10
|
// ─── In-memory cache ───
|
|
14
11
|
|
|
15
|
-
const cache = new Map();
|
|
12
|
+
const cache = new Map();
|
|
16
13
|
|
|
17
14
|
export function loadGraph(ws) {
|
|
18
15
|
if (cache.has(ws) && cache.get(ws).loaded) return cache.get(ws).triples;
|
|
@@ -31,7 +28,6 @@ export function loadGraph(ws) {
|
|
|
31
28
|
export function addTriple(ws, subject, relation, object, sourceId = null) {
|
|
32
29
|
const triples = loadGraph(ws);
|
|
33
30
|
|
|
34
|
-
// Deduplicate: same subject+relation+object
|
|
35
31
|
const exists = triples.some(
|
|
36
32
|
(t) => t.s === subject && t.r === relation && t.o === object,
|
|
37
33
|
);
|
|
@@ -61,13 +57,6 @@ export function removeTriple(ws, tripleId) {
|
|
|
61
57
|
return true;
|
|
62
58
|
}
|
|
63
59
|
|
|
64
|
-
/**
|
|
65
|
-
* Query the graph from a starting entity, optionally filtering by relation.
|
|
66
|
-
* @param {string} entity - starting node
|
|
67
|
-
* @param {string} [relation] - optional relation filter
|
|
68
|
-
* @param {number} [depth=2] - traversal depth
|
|
69
|
-
* @returns {Array<{ path: string[], triple: object }>}
|
|
70
|
-
*/
|
|
71
60
|
export function queryGraph(ws, entity, relation = null, depth = 2) {
|
|
72
61
|
const triples = loadGraph(ws);
|
|
73
62
|
const entityLower = entity.toLowerCase();
|
|
@@ -82,16 +71,14 @@ export function queryGraph(ws, entity, relation = null, depth = 2) {
|
|
|
82
71
|
|
|
83
72
|
const currentLower = current.toLowerCase();
|
|
84
73
|
for (const t of triples) {
|
|
85
|
-
// Forward: subject matches
|
|
86
74
|
if (t.s.toLowerCase() === currentLower) {
|
|
87
75
|
if (relation && t.r.toLowerCase() !== relation.toLowerCase()) continue;
|
|
88
|
-
results.push({ path: [...path, `--${t.r}-->`], node: t.o, triple: t });
|
|
76
|
+
results.push({ path: [...path, `--${t.r}-->`], node: t.o, triple: { id: t.id, s: t.s, r: t.r, o: t.o } });
|
|
89
77
|
traverse(t.o, currentDepth + 1, [...path, `--${t.r}-->`, t.o]);
|
|
90
78
|
}
|
|
91
|
-
// Reverse: object matches
|
|
92
79
|
if (t.o.toLowerCase() === currentLower) {
|
|
93
80
|
if (relation && t.r.toLowerCase() !== relation.toLowerCase()) continue;
|
|
94
|
-
results.push({ path: [...path, `<--${t.r}--`], node: t.s, triple: t });
|
|
81
|
+
results.push({ path: [...path, `<--${t.r}--`], node: t.s, triple: { id: t.id, s: t.s, r: t.r, o: t.o } });
|
|
95
82
|
traverse(t.s, currentDepth + 1, [...path, `<--${t.r}--`, t.s]);
|
|
96
83
|
}
|
|
97
84
|
}
|
|
@@ -101,45 +88,65 @@ export function queryGraph(ws, entity, relation = null, depth = 2) {
|
|
|
101
88
|
return results;
|
|
102
89
|
}
|
|
103
90
|
|
|
104
|
-
// ─── Auto-extraction patterns ───
|
|
91
|
+
// ─── Auto-extraction: strict patterns only ───
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validate that a string looks like a proper entity name (not a sentence fragment).
|
|
95
|
+
* - Must be short (≤25 chars)
|
|
96
|
+
* - Must not contain markdown, code, or sentence-like patterns
|
|
97
|
+
* - Must start with a capital letter or CJK character
|
|
98
|
+
*/
|
|
99
|
+
function isValidEntity(s) {
|
|
100
|
+
if (!s || s.length > 25 || s.length < 2) return false;
|
|
101
|
+
// Reject markdown, code, URLs, punctuation-heavy strings
|
|
102
|
+
if (/[`*\[\]{}()|→←⚠#>]/.test(s)) return false;
|
|
103
|
+
// Reject if it looks like a sentence (has verb-like patterns or too many words)
|
|
104
|
+
if (s.split(/\s+/).length > 5) return false;
|
|
105
|
+
// Must start with uppercase, CJK, or known pattern
|
|
106
|
+
if (!/^[A-Z\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(s)) return false;
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
105
109
|
|
|
106
110
|
const EXTRACTION_PATTERNS = [
|
|
107
|
-
// "X's doctor is Y"
|
|
108
|
-
{ re:
|
|
109
|
-
// "X
|
|
110
|
-
{ re:
|
|
111
|
-
// "X
|
|
112
|
-
{ re:
|
|
113
|
-
// "X
|
|
114
|
-
{ re:
|
|
115
|
-
// "X
|
|
116
|
-
{ re:
|
|
111
|
+
// "X's doctor is Y"
|
|
112
|
+
{ re: /^([A-Z]\w+)(?:'s)\s+doctor\s+is\s+(.+)$/i, r: "has_doctor" },
|
|
113
|
+
// "X's lawyer is Y"
|
|
114
|
+
{ re: /^([A-Z]\w+)(?:'s)\s+(?:lawyer|attorney)\s+is\s+(.+)$/i, r: "has_lawyer" },
|
|
115
|
+
// "X lives in Y"
|
|
116
|
+
{ re: /^([A-Z]\w+)\s+lives?\s+in\s+([A-Z][\w\s,]+)$/i, r: "lives_in" },
|
|
117
|
+
// "X works at Y"
|
|
118
|
+
{ re: /^([A-Z]\w+)\s+works?\s+at\s+([A-Z][\w\s&.]+)$/i, r: "works_at" },
|
|
119
|
+
// "X owns Y" — only match short clear statements
|
|
120
|
+
{ re: /^([A-Z]\w+)\s+owns?\s+(?:a\s+)?([A-Z][\w\s]+)$/i, r: "owns" },
|
|
121
|
+
// "X drives a Y"
|
|
122
|
+
{ re: /^([A-Z]\w+)\s+drives?\s+(?:a\s+)?([A-Z][\w\s]+)$/i, r: "owns" },
|
|
123
|
+
// "X has chronic Y" / "X has Y disease/condition"
|
|
124
|
+
{ re: /^([A-Z]\w+)\s+has\s+(?:chronic\s+)?(\w[\w\s]*(?:disease|condition|syndrome|urticaria|diabetes|asthma))$/i, r: "has_condition" },
|
|
125
|
+
// "X takes Y" (medication)
|
|
126
|
+
{ re: /^([A-Z]\w+)\s+takes?\s+([A-Z][\w\s]+\d+\s*mg)$/i, r: "treated_by" },
|
|
127
|
+
// "X attends Y"
|
|
128
|
+
{ re: /^([A-Z]\w+)\s+attends?\s+([A-Z][\w\s]+)$/i, r: "attends" },
|
|
117
129
|
// "X's wife/husband is Y"
|
|
118
|
-
{ re:
|
|
130
|
+
{ re: /^([A-Z]\w+)(?:'s)\s+(?:wife|husband)\s+is\s+(.+)$/i, r: "spouse" },
|
|
119
131
|
// "X's son/daughter is Y"
|
|
120
|
-
{ re:
|
|
121
|
-
// "X costs/price Y" / 定价
|
|
122
|
-
{ re: /(.+?)\s*(?:costs?|定价|售价|price[ds]?\s*(?:at)?)\s*\$?([\d,.]+)/i, r: "price" },
|
|
123
|
-
// "X's lawyer is Y"
|
|
124
|
-
{ re: /(.+?)(?:'s|的)\s*(?:lawyer|律师|attorney)\s*(?:is|是|为)\s*(.+)/i, r: "has_lawyer" },
|
|
125
|
-
// "X owns Y" / 拥有
|
|
126
|
-
{ re: /(.+?)\s*(?:owns?|拥有|有一辆|drives?)\s*(.+)/i, r: "owns" },
|
|
127
|
-
// "X studies/attends Y" / 就读
|
|
128
|
-
{ re: /(.+?)\s*(?:attends?|studies? at|就读于?|在(.+?)(?:上学|读书))/i, r: "attends" },
|
|
132
|
+
{ re: /^([A-Z]\w+)(?:'s)\s+(?:son|daughter)\s+is\s+(.+)$/i, r: "has_child" },
|
|
129
133
|
];
|
|
130
134
|
|
|
131
135
|
/**
|
|
132
136
|
* Extract triples from a text string.
|
|
133
|
-
*
|
|
137
|
+
* Strict: only matches clean, short statements with proper entity names.
|
|
134
138
|
*/
|
|
135
139
|
export function extractTriples(text) {
|
|
136
140
|
const results = [];
|
|
141
|
+
// Only try extraction on short, clear text (not paragraphs)
|
|
142
|
+
if (text.length > 150) return results;
|
|
143
|
+
|
|
137
144
|
for (const pat of EXTRACTION_PATTERNS) {
|
|
138
145
|
const m = text.match(pat.re);
|
|
139
146
|
if (m) {
|
|
140
147
|
const s = (m[1] || "").trim();
|
|
141
|
-
const o = (
|
|
142
|
-
if (s && o
|
|
148
|
+
const o = (m[2] || "").trim();
|
|
149
|
+
if (isValidEntity(s) && o.length >= 2 && o.length <= 40) {
|
|
143
150
|
results.push({ s, r: pat.r, o });
|
|
144
151
|
}
|
|
145
152
|
}
|
package/lib/quality.js
CHANGED
|
@@ -67,12 +67,18 @@ export async function runQualityPass(ws, options = {}) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
|
-
* Generate episode summaries from
|
|
70
|
+
* Generate episode summaries from records.
|
|
71
|
+
* Strategy 1: group by date (works when ts reflects original dates)
|
|
72
|
+
* Strategy 2: group by entity/topic (fallback when all ts are same day, e.g., after migration)
|
|
71
73
|
*/
|
|
72
74
|
function generateEpisodesFromRecords(ws, records) {
|
|
73
75
|
const episodes = loadEpisodes(ws);
|
|
76
|
+
const existingTopics = new Set(episodes.flatMap((e) => e.topics || []));
|
|
74
77
|
const existingDates = new Set(episodes.map((e) => e.ts?.slice(0, 10)));
|
|
75
78
|
|
|
79
|
+
let generated = 0;
|
|
80
|
+
|
|
81
|
+
// Strategy 1: by date
|
|
76
82
|
const byDate = {};
|
|
77
83
|
for (const r of records) {
|
|
78
84
|
if (!r.ts) continue;
|
|
@@ -81,14 +87,15 @@ function generateEpisodesFromRecords(ws, records) {
|
|
|
81
87
|
byDate[date].push(r);
|
|
82
88
|
}
|
|
83
89
|
|
|
84
|
-
let generated = 0;
|
|
85
90
|
for (const [date, dayRecords] of Object.entries(byDate)) {
|
|
86
91
|
if (existingDates.has(date) || dayRecords.length < 3) continue;
|
|
92
|
+
// Skip if most records share the same date (migration artifact)
|
|
93
|
+
if (dayRecords.length > records.length * 0.5) continue;
|
|
87
94
|
|
|
88
95
|
const topics = [...new Set(dayRecords.map((r) => r.entity).filter((e) => e && e !== "general"))];
|
|
89
96
|
const topContent = dayRecords
|
|
90
97
|
.sort((a, b) => (b.importance || 5) - (a.importance || 5))
|
|
91
|
-
.slice(0,
|
|
98
|
+
.slice(0, 3)
|
|
92
99
|
.map((r) => r.content.slice(0, 80))
|
|
93
100
|
.join("; ");
|
|
94
101
|
|
|
@@ -103,6 +110,35 @@ function generateEpisodesFromRecords(ws, records) {
|
|
|
103
110
|
generated++;
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
// Strategy 2: by entity (fallback for migration data with same-day ts)
|
|
114
|
+
const byEntity = {};
|
|
115
|
+
for (const r of records) {
|
|
116
|
+
const e = r.entity || "general";
|
|
117
|
+
if (e === "general") continue;
|
|
118
|
+
if (!byEntity[e]) byEntity[e] = [];
|
|
119
|
+
byEntity[e].push(r);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const [entity, recs] of Object.entries(byEntity)) {
|
|
123
|
+
if (recs.length < 5) continue;
|
|
124
|
+
if (existingTopics.has(entity)) continue;
|
|
125
|
+
|
|
126
|
+
const top = recs
|
|
127
|
+
.sort((a, b) => (b.importance || 5) - (a.importance || 5))
|
|
128
|
+
.slice(0, 3)
|
|
129
|
+
.map((r) => r.content.slice(0, 80));
|
|
130
|
+
|
|
131
|
+
saveEpisode(ws, {
|
|
132
|
+
summary: `[${entity}] ${top.join("; ")}`,
|
|
133
|
+
decisions: [],
|
|
134
|
+
mood: "",
|
|
135
|
+
topics: [entity],
|
|
136
|
+
participants: [],
|
|
137
|
+
source: "topic-summary",
|
|
138
|
+
});
|
|
139
|
+
generated++;
|
|
140
|
+
}
|
|
141
|
+
|
|
106
142
|
return generated;
|
|
107
143
|
}
|
|
108
144
|
|
package/package.json
CHANGED