@icex-labs/openclaw-memory-engine 4.1.0 → 4.1.2
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/index.js +17 -20
- package/lib/auto-capture.js +35 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -138,28 +138,25 @@ export default definePluginEntry({
|
|
|
138
138
|
// ═══════════════════════════════════════════════════════════════════
|
|
139
139
|
|
|
140
140
|
api.registerHook("message:received", (event) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
});
|
|
141
|
+
try {
|
|
142
|
+
const ctx = event.context;
|
|
143
|
+
if (!ctx?.content) return;
|
|
144
|
+
const agentId = extractAgentId(event.sessionKey);
|
|
145
|
+
const wsDir = resolveWorkspace({ agentId });
|
|
146
|
+
captureMessage(wsDir, ctx.content, "user-message");
|
|
147
|
+
} catch { /* don't break message flow */ }
|
|
148
|
+
}, { name: "memory-engine-capture-received", description: "Auto-capture facts from incoming messages" });
|
|
150
149
|
|
|
151
150
|
api.registerHook("message:sent", (event) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
captureMessage(wsDir, ctx.content, "agent-reply");
|
|
162
|
-
});
|
|
151
|
+
try {
|
|
152
|
+
const ctx = event.context;
|
|
153
|
+
if (!ctx?.content || !ctx?.success) return;
|
|
154
|
+
if (ctx.content.length < 50) return;
|
|
155
|
+
const agentId = extractAgentId(event.sessionKey);
|
|
156
|
+
const wsDir = resolveWorkspace({ agentId });
|
|
157
|
+
captureMessage(wsDir, ctx.content, "agent-reply");
|
|
158
|
+
} catch { /* don't break message flow */ }
|
|
159
|
+
}, { name: "memory-engine-capture-sent", description: "Auto-capture facts from agent replies" });
|
|
163
160
|
|
|
164
161
|
// ─── core_memory_read ───
|
|
165
162
|
api.registerTool(withAgent((agentId) => ({
|
package/lib/auto-capture.js
CHANGED
|
@@ -5,11 +5,15 @@
|
|
|
5
5
|
* No reliance on the agent calling tools — memory happens passively.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { appendRecord } from "./archival.js";
|
|
8
|
+
import { loadArchival, appendRecord } from "./archival.js";
|
|
9
9
|
import { indexEmbedding } from "./embedding.js";
|
|
10
10
|
import { extractTriples, addTriple } from "./graph.js";
|
|
11
11
|
import { resolveWorkspace } from "./paths.js";
|
|
12
12
|
|
|
13
|
+
/** Recent capture cache to prevent duplicates within short windows. */
|
|
14
|
+
const recentCaptures = new Map(); // content_hash → timestamp
|
|
15
|
+
const DEDUP_WINDOW_MS = 60_000; // 60 seconds
|
|
16
|
+
|
|
13
17
|
// Minimum message length to consider for fact extraction
|
|
14
18
|
const MIN_LENGTH = 20;
|
|
15
19
|
|
|
@@ -64,10 +68,39 @@ function isHighValue(content) {
|
|
|
64
68
|
export function captureMessage(ws, content, source = "auto-capture") {
|
|
65
69
|
if (!shouldCapture(content)) return null;
|
|
66
70
|
|
|
71
|
+
// Dedup: skip if same content captured in last 60s
|
|
72
|
+
const contentHash = content.slice(0, 100).toLowerCase().replace(/\s+/g, " ");
|
|
73
|
+
const now = Date.now();
|
|
74
|
+
if (recentCaptures.has(contentHash) && now - recentCaptures.get(contentHash) < DEDUP_WINDOW_MS) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
recentCaptures.set(contentHash, now);
|
|
78
|
+
|
|
79
|
+
// Clean old entries from dedup cache
|
|
80
|
+
if (recentCaptures.size > 200) {
|
|
81
|
+
for (const [key, ts] of recentCaptures) {
|
|
82
|
+
if (now - ts > DEDUP_WINDOW_MS) recentCaptures.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Also check against existing archival (keyword overlap)
|
|
87
|
+
const existing = loadArchival(ws);
|
|
88
|
+
const contentLower = content.toLowerCase();
|
|
89
|
+
const contentWords = new Set(contentLower.split(/\s+/).filter((w) => w.length > 2));
|
|
90
|
+
if (contentWords.size > 0) {
|
|
91
|
+
for (let i = existing.length - 1; i >= Math.max(0, existing.length - 50); i--) {
|
|
92
|
+
const ex = (existing[i].content || "").toLowerCase();
|
|
93
|
+
const exWords = new Set(ex.split(/\s+/).filter((w) => w.length > 2));
|
|
94
|
+
let overlap = 0;
|
|
95
|
+
for (const w of contentWords) { if (exWords.has(w)) overlap++; }
|
|
96
|
+
if (overlap / contentWords.size > 0.7) return null; // too similar to recent record
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
const importance = isHighValue(content) ? 7 : 4;
|
|
68
101
|
const entity = inferEntity(content);
|
|
69
102
|
|
|
70
|
-
// Trim very long messages
|
|
103
|
+
// Trim very long messages
|
|
71
104
|
const trimmed = content.length > 500 ? content.slice(0, 497) + "..." : content;
|
|
72
105
|
|
|
73
106
|
const record = appendRecord(ws, {
|
package/package.json
CHANGED