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 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
+ }