openclaw-hrr-memory 1.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.
- package/README.md +123 -0
- package/index.js +408 -0
- package/openclaw.plugin.json +26 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# openclaw-hrr-memory
|
|
2
|
+
|
|
3
|
+
Structured fact recall for [OpenClaw](https://openclaw.ai) agents using [Holographic Reduced Representations](https://github.com/Joncik91/hrr-memory).
|
|
4
|
+
|
|
5
|
+
RAG handles 80% of memory queries. The other 20% — exact fact recall like "What is Alice's timezone?" — is where it struggles. This plugin fills that gap with instant <2ms structured lookups, zero dependencies, no embeddings API.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
openclaw plugins install openclaw-hrr-memory
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## What It Does
|
|
14
|
+
|
|
15
|
+
Parses your agent's MEMORY.md into `(subject, relation, object)` triples and stores them in an HRR index. Agents query facts instantly instead of searching through document chunks.
|
|
16
|
+
|
|
17
|
+
| Question type | Tool | Speed |
|
|
18
|
+
|---------------|------|-------|
|
|
19
|
+
| "What is Jounes's timezone?" | `fact_lookup` | <2ms |
|
|
20
|
+
| "Where does Alice work?" | `fact_ask` | <2ms |
|
|
21
|
+
| "What did we discuss about deployment?" | `memory_search` | ~200ms |
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | Description |
|
|
26
|
+
|------|-------------|
|
|
27
|
+
| `fact_lookup` | Structured subject+relation query. Use first for factual questions. |
|
|
28
|
+
| `fact_ask` | Natural language with stop word handling. |
|
|
29
|
+
| `fact_forget` | Remove outdated facts. |
|
|
30
|
+
| `fact_rebuild` | Force reindex from MEMORY.md. |
|
|
31
|
+
|
|
32
|
+
The plugin tells the agent to try `fact_lookup` before `memory_search` for direct questions via system prompt injection.
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
In your OpenClaw config (`~/.openclaw/openclaw.json`):
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"plugins": {
|
|
41
|
+
"entries": {
|
|
42
|
+
"hrr-memory": {
|
|
43
|
+
"config": {
|
|
44
|
+
"memoryFiles": ["~/.openclaw/workspace/MEMORY.md"],
|
|
45
|
+
"watchInterval": 30000
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Option | Default | Description |
|
|
54
|
+
|--------|---------|-------------|
|
|
55
|
+
| `memoryFiles` | Workspace MEMORY.md | Paths to MEMORY.md files to index |
|
|
56
|
+
| `watchInterval` | `30000` | File watcher interval (ms). `0` to disable. |
|
|
57
|
+
| `enableObservations` | `false` | Enable belief change tracking (requires hrr-memory-obs) |
|
|
58
|
+
|
|
59
|
+
## Observation Layer (Optional)
|
|
60
|
+
|
|
61
|
+
Track how facts change over time with [hrr-memory-obs](https://github.com/Joncik91/hrr-memory-obs):
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
cd ~/.openclaw/extensions/openclaw-hrr-memory
|
|
65
|
+
npm install hrr-memory-obs
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then enable in config:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"plugins": {
|
|
73
|
+
"entries": {
|
|
74
|
+
"hrr-memory": {
|
|
75
|
+
"config": {
|
|
76
|
+
"enableObservations": true
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This adds four tools:
|
|
85
|
+
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `fact_history` | Temporal changelog for a subject |
|
|
89
|
+
| `fact_observations` | Synthesized beliefs about knowledge changes |
|
|
90
|
+
| `fact_flags` | Unflushed conflict flags |
|
|
91
|
+
| `fact_observe_write` | Store observation about belief changes |
|
|
92
|
+
|
|
93
|
+
Every MEMORY.md edit is diffed against the previous state. Changed facts are recorded in a timeline, and conflicting values (e.g., timezone changed from UTC to CET) are automatically flagged.
|
|
94
|
+
|
|
95
|
+
## How MEMORY.md Is Parsed
|
|
96
|
+
|
|
97
|
+
The parser extracts triples from markdown key-value patterns:
|
|
98
|
+
|
|
99
|
+
```markdown
|
|
100
|
+
## server
|
|
101
|
+
- **port**: 8080
|
|
102
|
+
- **timezone**: CET
|
|
103
|
+
|
|
104
|
+
## jounes
|
|
105
|
+
- **role**: developer
|
|
106
|
+
- **prefers**: concise answers, dark mode
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Becomes:
|
|
110
|
+
- `(server, port, 8080)` — `fact_lookup subject="server" relation="port"` → `8080`
|
|
111
|
+
- `(jounes, role, developer)` — `fact_ask "What is Jounes's role?"` → `developer`
|
|
112
|
+
|
|
113
|
+
The `##` heading becomes the subject. Key-value lines become relations and objects.
|
|
114
|
+
|
|
115
|
+
## Links
|
|
116
|
+
|
|
117
|
+
- [hrr-memory](https://github.com/Joncik91/hrr-memory) — standalone HRR library
|
|
118
|
+
- [hrr-memory-obs](https://github.com/Joncik91/hrr-memory-obs) — observation layer
|
|
119
|
+
- [Architecture](https://github.com/Joncik91/hrr-memory/blob/main/docs/architecture.md) — how HRR works
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclaw-hrr-memory — Structured fact recall for OpenClaw agents.
|
|
3
|
+
*
|
|
4
|
+
* Registers tools: fact_lookup, fact_ask, fact_forget, fact_rebuild
|
|
5
|
+
* Optional observation layer: fact_history, fact_observations, fact_flags, fact_observe_write
|
|
6
|
+
*
|
|
7
|
+
* RAG handles 80% of memory queries. The other 20% — exact fact recall — is
|
|
8
|
+
* where it struggles and HRR excels. Use both.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
12
|
+
import { readFileSync, existsSync, watchFile } from "fs";
|
|
13
|
+
import { join, resolve } from "path";
|
|
14
|
+
import { HRRMemory } from "hrr-memory";
|
|
15
|
+
|
|
16
|
+
// Optional observation layer — gracefully degrade if not installed
|
|
17
|
+
let ObservationMemoryClass = null;
|
|
18
|
+
try {
|
|
19
|
+
const obs = await import("hrr-memory-obs");
|
|
20
|
+
ObservationMemoryClass = obs.ObservationMemory;
|
|
21
|
+
} catch {
|
|
22
|
+
// hrr-memory-obs not installed — core tools still work
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── MEMORY.md Parser ──────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
function parseMemoryToTriples(content) {
|
|
28
|
+
const triples = [];
|
|
29
|
+
let section = "general";
|
|
30
|
+
|
|
31
|
+
for (const line of content.split("\n")) {
|
|
32
|
+
const t = line.trim();
|
|
33
|
+
if (t.startsWith("## ")) {
|
|
34
|
+
section = t.slice(3).trim().toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (t.startsWith("# ") || t.startsWith("{") || t.startsWith('"') || t.startsWith("```")) continue;
|
|
38
|
+
if (/session.?key|session.?id|sender|message_id|timestamp/i.test(t)) continue;
|
|
39
|
+
|
|
40
|
+
// Match "- **key**: value" or "- key: value" patterns
|
|
41
|
+
const kvMatch = t.match(/^[-*]\s*(?:\*\*)?([^:*]+?)(?:\*\*)?\s*:\s*(.+)$/);
|
|
42
|
+
if (kvMatch) {
|
|
43
|
+
const key = kvMatch[1].trim().toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
|
44
|
+
const value = kvMatch[2].trim();
|
|
45
|
+
if (key && value && value.length < 80 && value.length > 1) {
|
|
46
|
+
triples.push({
|
|
47
|
+
subject: section,
|
|
48
|
+
relation: key,
|
|
49
|
+
object: value.toLowerCase().replace(/[^a-z0-9_./:-]+/g, "_").replace(/^_|_$/g, ""),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Heuristic extraction for loose prose
|
|
56
|
+
if (section && t.length > 10 && t.length < 200 && !t.startsWith("-")) {
|
|
57
|
+
const inM = t.match(/(\w+)\s+in\s+(\w+)/i);
|
|
58
|
+
if (inM && inM[1].length > 2 && inM[2].length > 2)
|
|
59
|
+
triples.push({ subject: section, relation: "location", object: inM[2].toLowerCase() });
|
|
60
|
+
const atM = t.match(/(?:at|for)\s+(\w+)/i);
|
|
61
|
+
if (atM && atM[1].length > 2)
|
|
62
|
+
triples.push({ subject: section, relation: "organization", object: atM[1].toLowerCase() });
|
|
63
|
+
const prefM = t.match(/[Pp]refers?\s+(.+?)(?:\.|$)/);
|
|
64
|
+
if (prefM) {
|
|
65
|
+
for (const p of prefM[1].split(",").map((x) => x.trim().toLowerCase())) {
|
|
66
|
+
if (p.length > 2 && p.length < 40)
|
|
67
|
+
triples.push({ subject: section, relation: "prefers", object: p.replace(/[^a-z0-9_]+/g, "_") });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return triples;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function tripleKey(t) {
|
|
78
|
+
return `${t.subject}\0${t.relation}\0${t.object}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveMemoryFiles(api) {
|
|
82
|
+
const config = api.pluginConfig || {};
|
|
83
|
+
if (config.memoryFiles && config.memoryFiles.length > 0) {
|
|
84
|
+
return config.memoryFiles.map((f) => resolve(f));
|
|
85
|
+
}
|
|
86
|
+
// Default: workspace MEMORY.md files
|
|
87
|
+
const workspaceDir = api.runtime?.agent?.workspaceDir;
|
|
88
|
+
if (workspaceDir) {
|
|
89
|
+
return [join(workspaceDir, "MEMORY.md")];
|
|
90
|
+
}
|
|
91
|
+
const home = process.env.HOME || "/root";
|
|
92
|
+
return [join(home, ".openclaw/workspace/MEMORY.md")];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Plugin Entry ──────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
export default definePluginEntry({
|
|
98
|
+
id: "hrr-memory",
|
|
99
|
+
name: "HRR Fact Memory",
|
|
100
|
+
description: "Structured fact recall using Holographic Reduced Representations",
|
|
101
|
+
|
|
102
|
+
register(api) {
|
|
103
|
+
const config = api.pluginConfig || {};
|
|
104
|
+
const watchInterval = config.watchInterval ?? 30000;
|
|
105
|
+
const enableObs = config.enableObservations ?? false;
|
|
106
|
+
|
|
107
|
+
const MEMORY_FILES = resolveMemoryFiles(api);
|
|
108
|
+
const stateDir = api.runtime?.state?.dir
|
|
109
|
+
? resolve(api.runtime.state.dir)
|
|
110
|
+
: resolve(process.env.HOME || "/root", ".openclaw/memory");
|
|
111
|
+
const INDEX_PATH = join(stateDir, "hrr-index.json");
|
|
112
|
+
const OBS_PATH = join(stateDir, "observations.json");
|
|
113
|
+
|
|
114
|
+
const ObservationMemory = enableObs && ObservationMemoryClass ? ObservationMemoryClass : null;
|
|
115
|
+
if (enableObs && !ObservationMemory) {
|
|
116
|
+
api.logger.warn("hrr-memory-obs not installed. Install with: npm install hrr-memory-obs");
|
|
117
|
+
} else if (ObservationMemory) {
|
|
118
|
+
api.logger.info("Observation layer enabled");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let mem = null;
|
|
122
|
+
let lastBuild = 0;
|
|
123
|
+
|
|
124
|
+
function initMem() {
|
|
125
|
+
if (ObservationMemory && existsSync(INDEX_PATH) && existsSync(OBS_PATH)) {
|
|
126
|
+
return ObservationMemory.load(INDEX_PATH, OBS_PATH);
|
|
127
|
+
}
|
|
128
|
+
if (existsSync(INDEX_PATH)) {
|
|
129
|
+
const hrr = HRRMemory.load(INDEX_PATH);
|
|
130
|
+
return ObservationMemory ? new ObservationMemory(hrr) : hrr;
|
|
131
|
+
}
|
|
132
|
+
const hrr = new HRRMemory();
|
|
133
|
+
return ObservationMemory ? new ObservationMemory(hrr) : hrr;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function collectTriples() {
|
|
137
|
+
const triples = [];
|
|
138
|
+
for (const fp of MEMORY_FILES) {
|
|
139
|
+
if (!existsSync(fp)) continue;
|
|
140
|
+
triples.push(...parseMemoryToTriples(readFileSync(fp, "utf8")));
|
|
141
|
+
}
|
|
142
|
+
return triples;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function rebuildIndex() {
|
|
146
|
+
const triples = collectTriples();
|
|
147
|
+
const currentKeys = new Set(triples.map(tripleKey));
|
|
148
|
+
|
|
149
|
+
// Diff against previous state for observation tracking
|
|
150
|
+
const oldTriples = mem && typeof mem.search === "function" ? mem.search(null, null) : [];
|
|
151
|
+
const oldKeys = new Set(oldTriples.map(tripleKey));
|
|
152
|
+
const added = triples.filter((t) => !oldKeys.has(tripleKey(t)));
|
|
153
|
+
const removed = oldTriples.filter((t) => !currentKeys.has(tripleKey(t)));
|
|
154
|
+
|
|
155
|
+
// Rebuild HRR from scratch
|
|
156
|
+
const newHrr = new HRRMemory();
|
|
157
|
+
for (const { subject, relation, object } of triples) {
|
|
158
|
+
newHrr.store(subject, relation, object);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Preserve observations if available
|
|
162
|
+
if (ObservationMemory) {
|
|
163
|
+
const obsData = mem && typeof mem.toJSON === "function" ? mem.toJSON() : null;
|
|
164
|
+
mem = obsData ? ObservationMemory.fromJSON(newHrr, obsData) : new ObservationMemory(newHrr);
|
|
165
|
+
|
|
166
|
+
// Feed changes into timeline
|
|
167
|
+
const ts = Date.now();
|
|
168
|
+
for (const t of removed) {
|
|
169
|
+
mem._timeline.append({ ts, subject: t.subject, relation: t.relation, object: t.object, op: "forget" });
|
|
170
|
+
mem._meta.totalForgets++;
|
|
171
|
+
}
|
|
172
|
+
for (const { subject, relation, object } of added) {
|
|
173
|
+
const flag = mem._conflict.check(subject, relation, object, ts);
|
|
174
|
+
const entry = { ts, subject, relation, object, op: "store" };
|
|
175
|
+
if (flag) {
|
|
176
|
+
entry.conflict = { oldObject: flag.oldObject, similarity: flag.similarity };
|
|
177
|
+
mem._flags.push(flag);
|
|
178
|
+
}
|
|
179
|
+
mem._timeline.append(entry);
|
|
180
|
+
mem._conflict.track(subject, relation);
|
|
181
|
+
mem._meta.totalStores++;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
mem.save(INDEX_PATH, OBS_PATH);
|
|
185
|
+
} else {
|
|
186
|
+
mem = newHrr;
|
|
187
|
+
mem.save(INDEX_PATH);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lastBuild = Date.now();
|
|
191
|
+
return triples.length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
mem = initMem();
|
|
195
|
+
rebuildIndex();
|
|
196
|
+
|
|
197
|
+
// File watcher
|
|
198
|
+
if (watchInterval > 0) {
|
|
199
|
+
for (const f of MEMORY_FILES) {
|
|
200
|
+
if (existsSync(f)) {
|
|
201
|
+
watchFile(f, { interval: watchInterval }, () => {
|
|
202
|
+
if (Date.now() - lastBuild > 10000) rebuildIndex();
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Core Tools ──────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
api.registerTool({
|
|
211
|
+
name: "fact_lookup",
|
|
212
|
+
description:
|
|
213
|
+
"Look up structured facts from memory. Use FIRST for specific factual questions like 'What is X's Y?' Returns instant results (<2ms). For fuzzy/semantic search, use memory_search instead.",
|
|
214
|
+
parameters: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
subject: { type: "string", description: "Entity to query (e.g., 'jounes', 'server', 'research')" },
|
|
218
|
+
relation: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "Attribute to look up (e.g., 'timezone', 'port'). Omit to list all facts about subject.",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
required: ["subject"],
|
|
224
|
+
},
|
|
225
|
+
async execute(_id, params) {
|
|
226
|
+
if (Date.now() - lastBuild > 300000) rebuildIndex();
|
|
227
|
+
const subject = (params.subject || "").toLowerCase().trim().replace(/\s+/g, "_");
|
|
228
|
+
const relation = params.relation ? params.relation.toLowerCase().trim().replace(/\s+/g, "_") : null;
|
|
229
|
+
|
|
230
|
+
if (relation) {
|
|
231
|
+
const result = mem.query(subject, relation);
|
|
232
|
+
const related = mem.querySubject(subject).slice(0, 8);
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify(
|
|
238
|
+
{ query: { subject, relation }, result: result.confident ? result.match : null, confidence: result.score, related_facts: related },
|
|
239
|
+
null,
|
|
240
|
+
2
|
|
241
|
+
),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const facts = mem.querySubject(subject);
|
|
247
|
+
return { content: [{ type: "text", text: JSON.stringify({ query: { subject }, facts }, null, 2) }] };
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
api.registerTool({
|
|
252
|
+
name: "fact_ask",
|
|
253
|
+
description:
|
|
254
|
+
'Ask a natural language question against structured memory. Handles stop words, possessives, hyphens. Example: "What is Jounes\'s timezone?" Use when you don\'t know the exact subject/relation.',
|
|
255
|
+
parameters: {
|
|
256
|
+
type: "object",
|
|
257
|
+
properties: {
|
|
258
|
+
question: { type: "string", description: 'Natural language question (e.g., "What is alice\'s timezone?")' },
|
|
259
|
+
},
|
|
260
|
+
required: ["question"],
|
|
261
|
+
},
|
|
262
|
+
async execute(_id, params) {
|
|
263
|
+
if (Date.now() - lastBuild > 300000) rebuildIndex();
|
|
264
|
+
const result = mem.ask(params.question || "");
|
|
265
|
+
return { content: [{ type: "text", text: JSON.stringify({ question: params.question, ...result }, null, 2) }] };
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
api.registerTool({
|
|
270
|
+
name: "fact_forget",
|
|
271
|
+
description: "Remove a specific fact from memory. Use when a fact is wrong or outdated.",
|
|
272
|
+
parameters: {
|
|
273
|
+
type: "object",
|
|
274
|
+
properties: {
|
|
275
|
+
subject: { type: "string", description: "Entity" },
|
|
276
|
+
relation: { type: "string", description: "Attribute" },
|
|
277
|
+
object: { type: "string", description: "Value to remove" },
|
|
278
|
+
},
|
|
279
|
+
required: ["subject", "relation", "object"],
|
|
280
|
+
},
|
|
281
|
+
async execute(_id, params) {
|
|
282
|
+
const removed = typeof mem.forget === "function"
|
|
283
|
+
? await mem.forget(params.subject, params.relation, params.object)
|
|
284
|
+
: mem.forget(params.subject, params.relation, params.object);
|
|
285
|
+
if (removed) {
|
|
286
|
+
if (ObservationMemory) mem.save(INDEX_PATH, OBS_PATH);
|
|
287
|
+
else mem.save(INDEX_PATH);
|
|
288
|
+
}
|
|
289
|
+
return { content: [{ type: "text", text: JSON.stringify({ removed, subject: params.subject, relation: params.relation, object: params.object }) }] };
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
api.registerTool(
|
|
294
|
+
{
|
|
295
|
+
name: "fact_rebuild",
|
|
296
|
+
description: "Force rebuild the fact index from MEMORY.md files.",
|
|
297
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
298
|
+
async execute() {
|
|
299
|
+
const count = rebuildIndex();
|
|
300
|
+
return { content: [{ type: "text", text: `Index rebuilt: ${count} facts.\n${JSON.stringify(mem.stats(), null, 2)}` }] };
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{ optional: true }
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// ── Observation Tools (only with hrr-memory-obs) ────────
|
|
307
|
+
|
|
308
|
+
if (ObservationMemory) {
|
|
309
|
+
api.registerTool({
|
|
310
|
+
name: "fact_history",
|
|
311
|
+
description: "View temporal history of stored/forgotten facts for a subject.",
|
|
312
|
+
parameters: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
subject: { type: "string", description: "Entity to query" },
|
|
316
|
+
relation: { type: "string", description: "Optional: filter to a specific relation" },
|
|
317
|
+
},
|
|
318
|
+
required: ["subject"],
|
|
319
|
+
},
|
|
320
|
+
async execute(_id, params) {
|
|
321
|
+
const entries = mem.history(
|
|
322
|
+
(params.subject || "").toLowerCase().trim().replace(/\s+/g, "_"),
|
|
323
|
+
params.relation ? params.relation.toLowerCase().trim().replace(/\s+/g, "_") : undefined
|
|
324
|
+
);
|
|
325
|
+
return { content: [{ type: "text", text: JSON.stringify({ entries, count: entries.length }, null, 2) }] };
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
api.registerTool({
|
|
330
|
+
name: "fact_observations",
|
|
331
|
+
description: "Read synthesized beliefs about how knowledge has evolved over time.",
|
|
332
|
+
parameters: {
|
|
333
|
+
type: "object",
|
|
334
|
+
properties: {
|
|
335
|
+
subject: { type: "string", description: "Optional: filter to a specific subject" },
|
|
336
|
+
},
|
|
337
|
+
required: [],
|
|
338
|
+
},
|
|
339
|
+
async execute(_id, params) {
|
|
340
|
+
const subject = params.subject ? params.subject.toLowerCase().trim().replace(/\s+/g, "_") : undefined;
|
|
341
|
+
const observations = mem.observations(subject);
|
|
342
|
+
return { content: [{ type: "text", text: JSON.stringify({ observations, count: observations.length }, null, 2) }] };
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
api.registerTool({
|
|
347
|
+
name: "fact_flags",
|
|
348
|
+
description: "Read unflushed conflict flags — belief changes waiting to be consolidated.",
|
|
349
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
350
|
+
async execute() {
|
|
351
|
+
return { content: [{ type: "text", text: JSON.stringify({ flags: mem.flags(), count: mem.flags().length }, null, 2) }] };
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
api.registerTool({
|
|
356
|
+
name: "fact_observe_write",
|
|
357
|
+
description: "Store a synthesized observation about belief changes.",
|
|
358
|
+
parameters: {
|
|
359
|
+
type: "object",
|
|
360
|
+
properties: {
|
|
361
|
+
subject: { type: "string", description: "Primary subject" },
|
|
362
|
+
observation: { type: "string", description: "1-2 sentence synthesis of the change" },
|
|
363
|
+
evidence: {
|
|
364
|
+
type: "array",
|
|
365
|
+
items: {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: {
|
|
368
|
+
ts: { type: "number" },
|
|
369
|
+
triple: { type: "array", items: { type: "string" } },
|
|
370
|
+
},
|
|
371
|
+
required: ["ts", "triple"],
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
confidence: { type: "string", enum: ["high", "medium", "low"] },
|
|
375
|
+
},
|
|
376
|
+
required: ["subject", "observation", "evidence", "confidence"],
|
|
377
|
+
},
|
|
378
|
+
async execute(_id, params) {
|
|
379
|
+
const obs = mem.addObservation({
|
|
380
|
+
subject: params.subject,
|
|
381
|
+
observation: params.observation,
|
|
382
|
+
evidence: params.evidence,
|
|
383
|
+
confidence: params.confidence,
|
|
384
|
+
});
|
|
385
|
+
mem.clearFlags(obs.subject);
|
|
386
|
+
mem.save(INDEX_PATH, OBS_PATH);
|
|
387
|
+
return { content: [{ type: "text", text: JSON.stringify({ id: obs.id, stored: true, subject: obs.subject }) }] };
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── System Prompt ───────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
api.on(
|
|
395
|
+
"before_prompt_build",
|
|
396
|
+
() => ({
|
|
397
|
+
appendSystemContext: [
|
|
398
|
+
"MEMORY TOOL PRIORITY:",
|
|
399
|
+
"1. fact_lookup / fact_ask — USE FIRST for factual questions (who, what, where, which, when). Instant structured recall (<2ms).",
|
|
400
|
+
"2. memory_search — USE SECOND for fuzzy, contextual, or open-ended queries.",
|
|
401
|
+
"",
|
|
402
|
+
"Available fact tools: fact_lookup, fact_ask, fact_forget" + (ObservationMemory ? ", fact_history, fact_observations" : ""),
|
|
403
|
+
].join("\n"),
|
|
404
|
+
}),
|
|
405
|
+
{ priority: 5 }
|
|
406
|
+
);
|
|
407
|
+
},
|
|
408
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "hrr-memory",
|
|
3
|
+
"name": "HRR Fact Memory",
|
|
4
|
+
"description": "Structured fact recall using Holographic Reduced Representations. Instant <2ms lookups for factual questions (who, what, where, which). Complements RAG-based memory_search.",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"memoryFiles": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": { "type": "string" },
|
|
12
|
+
"description": "Paths to MEMORY.md files to index. Defaults to workspace MEMORY.md."
|
|
13
|
+
},
|
|
14
|
+
"watchInterval": {
|
|
15
|
+
"type": "number",
|
|
16
|
+
"default": 30000,
|
|
17
|
+
"description": "File watcher interval in ms. Set to 0 to disable."
|
|
18
|
+
},
|
|
19
|
+
"enableObservations": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"default": false,
|
|
22
|
+
"description": "Enable the observation layer (requires hrr-memory-obs). Tracks belief changes over time."
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-hrr-memory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Structured fact recall for OpenClaw agents using Holographic Reduced Representations. Complements RAG with instant <2ms factual lookups.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Jounes <jounes@reefsown.space>",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/Joncik91/openclaw-hrr-memory"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"openclaw",
|
|
15
|
+
"openclaw-plugin",
|
|
16
|
+
"memory",
|
|
17
|
+
"hrr",
|
|
18
|
+
"holographic-reduced-representations",
|
|
19
|
+
"agent-memory",
|
|
20
|
+
"fact-recall",
|
|
21
|
+
"structured-memory"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"hrr-memory": "^0.2.0"
|
|
25
|
+
},
|
|
26
|
+
"optionalDependencies": {
|
|
27
|
+
"hrr-memory-obs": "^0.1.2"
|
|
28
|
+
},
|
|
29
|
+
"openclaw": {
|
|
30
|
+
"extensions": ["./index.js"]
|
|
31
|
+
}
|
|
32
|
+
}
|