@jeremiaheth/neolata-mem 0.8.0 → 0.8.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/LICENSE CHANGED
@@ -1,21 +1,89 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Jeremiaheth
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ Elastic License 2.0 (ELv2)
2
+
3
+ URL: https://www.elastic.co/licensing/elastic-license
4
+
5
+ ## Acceptance
6
+
7
+ By using the software, you agree to all of the terms and conditions below.
8
+
9
+ ## Copyright License
10
+
11
+ The licensor grants you a non-exclusive, royalty-free, worldwide,
12
+ non-sublicensable, non-transferable license to use, copy, distribute, make
13
+ available, and prepare derivative works of the software, in each case subject
14
+ to the limitations and conditions below.
15
+
16
+ ## Limitations
17
+
18
+ You may not provide the software to third parties as a hosted or managed
19
+ service, where the service provides users with access to any substantial set
20
+ of the features or functionality of the software.
21
+
22
+ You may not move, change, disable, or circumvent the license key functionality
23
+ in the software, and you may not remove or obscure any functionality in the
24
+ software that is protected by the license key.
25
+
26
+ You may not alter, remove, or obscure any licensing, copyright, or other
27
+ notices of the licensor in the software. Any use of the licensor's trademarks
28
+ is subject to applicable law.
29
+
30
+ ## Patents
31
+
32
+ The licensor grants you a license, under any patent claims the licensor can
33
+ license, or becomes able to license, to make, have made, use, sell, offer for
34
+ sale, import and have imported the software, in each case subject to the
35
+ limitations and conditions in this license. This license does not cover any
36
+ patent claims that you cause to be infringed by modifications or additions to
37
+ the software. If you or your company make any written claim that the software
38
+ infringes or contributes to infringement of any patent, your patent license
39
+ for the software granted under these terms ends immediately. If your company
40
+ makes such a claim, your patent license ends immediately for work on behalf
41
+ of your company.
42
+
43
+ ## Notices
44
+
45
+ You must ensure that anyone who gets a copy of any part of the software from
46
+ you also gets a copy of these terms.
47
+
48
+ If you modify the software, you must include in any modified copies of the
49
+ software prominent notices stating that you have modified the software.
50
+
51
+ ## No Other Rights
52
+
53
+ These terms do not imply any licenses other than those expressly granted in
54
+ these terms.
55
+
56
+ ## Termination
57
+
58
+ If you use the software in violation of these terms, such use is not licensed,
59
+ and your licenses will automatically terminate. If the licensor provides you
60
+ with a notice of your violation, and you cease all violation of this license
61
+ no later than 30 days after you receive that notice, your licenses will be
62
+ reinstated retroactively. However, if you violate these terms after such
63
+ reinstatement, any additional violation of these terms will cause your licenses
64
+ to terminate automatically and permanently.
65
+
66
+ ## No Liability
67
+
68
+ *As far as the law allows, the software comes as is, without any warranty or
69
+ condition, and the licensor will not be liable to you for any damages arising
70
+ out of these terms or the use or nature of the software, under any kind of
71
+ legal claim.*
72
+
73
+ ## Definitions
74
+
75
+ The **licensor** is the entity offering these terms, and the **software** is
76
+ the software the licensor makes available under these terms, including any
77
+ portion of it.
78
+
79
+ **You** refers to the individual or entity agreeing to these terms.
80
+
81
+ **Your company** is any legal entity, sole proprietorship, or other kind of
82
+ organization that you work for, plus all organizations that have control over,
83
+ are under the control of, or are under common control with that organization.
84
+ **Control** means ownership of substantially all the assets of an entity, or
85
+ the power to direct the management and policies of an entity.
86
+
87
+ ---
88
+
89
+ Copyright 2025-2026 Jeremiaheth (Jeremiah Ojo)
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Graph-native memory engine for AI agents.** Zettelkasten-inspired linking, biological decay, conflict resolution.
4
4
 
5
- [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![Elastic License 2.0](https://img.shields.io/badge/license-Elastic--2.0-blue.svg)](LICENSE)
6
6
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](https://nodejs.org)
7
7
 
8
8
  ---
@@ -37,6 +37,7 @@ const mem = createMemory({
37
37
  },
38
38
  });
39
39
 
40
+ // Agent IDs like 'kuro' and 'maki' below are just examples — use any string to identify your agents.
40
41
  await mem.store('kuro', 'Found XSS vulnerability in login form', { category: 'finding', importance: 0.9 });
41
42
  await mem.store('kuro', 'OWASP Top 10 audit completed', { category: 'event' });
42
43
 
@@ -68,7 +69,7 @@ embeddings: {
68
69
 
69
70
  ### 🔗 A-MEM Zettelkasten Linking
70
71
 
71
- Every memory automatically links to related memories bidirectionally. When you store "Redis runs on port 6379", it finds existing memories about Redis, ports, or databases and creates links in both directions.
72
+ Every memory automatically links to related memories - bidirectionally. When you store "Redis runs on port 6379", it finds existing memories about Redis, ports, or databases and creates links in both directions.
72
73
 
73
74
  ```javascript
74
75
  await mem.store('a', 'Redis runs on port 6379');
@@ -110,14 +111,14 @@ await mem.evolve('a', 'Server now runs on port 8080');
110
111
  // Old version archived with evolution history
111
112
  ```
112
113
 
113
- **Quarantine lane** low-trust or structurally conflicting memories are quarantined instead of auto-superseding:
114
+ **Quarantine lane** - low-trust or structurally conflicting memories are quarantined instead of auto-superseding:
114
115
 
115
116
  ```javascript
116
117
  // Store with claim metadata and provenance
117
118
  await mem.store('a', 'Server runs on port 443', {
118
119
  claim: { subject: 'server', predicate: 'port', value: '443' },
119
120
  provenance: { source: 'user_explicit', trust: 1.0 },
120
- onConflict: 'quarantine', // default quarantine low-trust conflicts
121
+ onConflict: 'quarantine', // default - quarantine low-trust conflicts
121
122
  });
122
123
 
123
124
  // Review quarantined memories
@@ -207,7 +208,7 @@ const mem = createMemory({
207
208
  dir: './my-data', // Custom directory for JSON storage
208
209
  },
209
210
 
210
- // Embeddings (optional keyword search works without)
211
+ // Embeddings (optional - keyword search works without)
211
212
  embeddings: {
212
213
  type: 'openai', // 'openai' (any compatible API) | 'noop' (keyword only)
213
214
  apiKey: '...',
@@ -216,7 +217,7 @@ const mem = createMemory({
216
217
  extraBody: {}, // Extra params (e.g. { input_type: 'passage' } for NIM)
217
218
  },
218
219
 
219
- // Fact extraction (optional enables ingest())
220
+ // Fact extraction (optional - enables ingest())
220
221
  extraction: {
221
222
  type: 'llm', // 'llm' | 'passthrough'
222
223
  apiKey: '...',
@@ -224,7 +225,7 @@ const mem = createMemory({
224
225
  baseUrl: 'https://api.openai.com/v1',
225
226
  },
226
227
 
227
- // LLM for conflict resolution (optional enables evolve())
228
+ // LLM for conflict resolution (optional - enables evolve())
228
229
  llm: {
229
230
  type: 'openai',
230
231
  apiKey: '...',
@@ -323,7 +324,7 @@ Set `OPENAI_API_KEY` or `NVIDIA_API_KEY` for embedding support. See `npx @jeremi
323
324
 
324
325
  ### `createMemory(opts?) → MemoryGraph`
325
326
 
326
- Factory function. All options are optional zero-config returns a working instance with JSON storage and keyword search.
327
+ Factory function. All options are optional - zero-config returns a working instance with JSON storage and keyword search.
327
328
 
328
329
  ### Core Methods
329
330
 
@@ -419,6 +420,48 @@ Factory function. All options are optional — zero-config returns a working ins
419
420
  | `pendingConflicts()` | List unresolved structural conflicts |
420
421
  | `resolveConflict(conflictId, opts)` | Resolve a pending conflict |
421
422
 
423
+ ### Runtime Helpers
424
+
425
+ Convenience functions for agent workflows — heartbeat auto-store, contextual recall, and pre-compaction dumps.
426
+
427
+ | Function | Description |
428
+ |----------|-------------|
429
+ | `detectKeyMoments(text, opts?)` | Extract decisions, preferences, commitments, and blockers from text |
430
+ | `extractTopicSlug(text, opts?)` | Derive a topic slug from text (with optional synonym mapping) |
431
+ | `heartbeatStore(mem, agent, turns, config?)` | Auto-store key moments from conversation turns on a heartbeat interval |
432
+ | `contextualRecall(mem, agent, seedText, config?)` | Budget-aware recall: merges recent + semantic + high-importance memories by topic |
433
+ | `preCompactionDump(mem, agent, turns, config?)` | Extract and persist takeaways before context window compaction |
434
+
435
+ ```javascript
436
+ import { createMemory, heartbeatStore, contextualRecall, preCompactionDump } from '@jeremiaheth/neolata-mem';
437
+
438
+ const mem = createMemory({ /* ... */ });
439
+
440
+ // Heartbeat: auto-store key moments from recent turns
441
+ const result = await heartbeatStore(mem, 'kuro', conversationTurns, {
442
+ sessionId: 'sess-123',
443
+ topicSlug: 'deployment',
444
+ minNewTurns: 3, // skip if fewer than 3 new turns (default: 3)
445
+ lastStoredIndex: -1, // track position across calls
446
+ });
447
+ // → { stored: 2, ids: [...], lastIndex: 14, moments: [...] }
448
+
449
+ // Contextual recall: topic-aware, budget-capped context retrieval
450
+ const context = await contextualRecall(mem, 'kuro', 'How did we fix the RLS issue?', {
451
+ maxTokens: 2000, // token budget (default: 2000)
452
+ semanticCount: 8, // semantic search results (default: 8)
453
+ importanceThreshold: 0.8,
454
+ });
455
+ // → { topicSlug: 'rls', memories: [...], totalTokens: 1823, excluded: 3 }
456
+
457
+ // Pre-compaction dump: persist session takeaways before context reset
458
+ const dump = await preCompactionDump(mem, 'kuro', conversationTurns, {
459
+ sessionId: 'sess-123',
460
+ maxTakeaways: 10, // max individual moments to store (default: 10)
461
+ });
462
+ // → { takeaways: 4, snapshotId: '...', ids: [...] }
463
+ ```
464
+
422
465
  ### Advanced: Bring Your Own Providers
423
466
 
424
467
  ```javascript
@@ -482,6 +525,7 @@ Decay Cycle:
482
525
  | Conflict resolution | ✅ | ✅ | ❌ | ❌ |
483
526
  | Quarantine lane | ✅ | ❌ | ❌ | ❌ |
484
527
  | Predicate schemas | ✅ | ❌ | ❌ | ❌ |
528
+ | Runtime helpers (heartbeat/recall/dump) | ✅ | ❌ | ❌ | ❌ |
485
529
  | Explainability API | ✅ | ❌ | ❌ | ❌ |
486
530
  | Episodes & compression | ✅ | ❌ | ❌ | ❌ |
487
531
  | Labeled clusters | ✅ | ❌ | ❌ | ❌ |
@@ -496,7 +540,7 @@ neolata-mem includes several hardening measures:
496
540
 
497
541
  - **Input validation**: Agent names (alphanumeric, max 64 chars), memory text (max 10KB), bounded total memory count (default 50K)
498
542
  - **Prompt injection mitigation**: All user content is XML-fenced in LLM prompts with explicit instruction boundaries. LLM output is structurally validated (type checks, index bounds, category whitelists)
499
- - **SSRF protection**: All provider URLs validated via `validateBaseUrl()` blocks cloud metadata endpoints, private IP ranges (configurable), non-HTTP protocols
543
+ - **SSRF protection**: All provider URLs validated via `validateBaseUrl()` - blocks cloud metadata endpoints, private IP ranges (configurable), non-HTTP protocols
500
544
  - **Supabase hardening**: UUID validation on all query params (prevents PostgREST injection), error text sanitized (strips tokens/keys), safe upsert-based save (no data loss on crash), automatic 429 retry with backoff
501
545
  - **Atomic writes**: JSON storage uses write-to-temp + rename to prevent corruption from concurrent access
502
546
  - **Path traversal guards**: Storage directories and write-through paths validated with `resolve()` + prefix checks
@@ -504,12 +548,12 @@ neolata-mem includes several hardening measures:
504
548
  - **Retry bounds**: Embedding and Supabase API retries are capped at 3 with exponential backoff (no infinite loops)
505
549
  - **Error surfacing**: Failed conflict detection returns `{ error }` instead of silently proceeding
506
550
 
507
- **Trust model**: For JSON storage, neolata-mem trusts the filesystem protect your data directory. For Supabase, use Row Level Security (RLS) policies. Embedding vectors can approximate original text via inversion attacks treat them as sensitive.
551
+ **Trust model**: For JSON storage, neolata-mem trusts the filesystem - protect your data directory. For Supabase, use Row Level Security (RLS) policies. Embedding vectors can approximate original text via inversion attacks - treat them as sensitive.
508
552
 
509
553
  ## Documentation
510
554
 
511
- 📖 **[Full User Guide](docs/guide.md)** configuration deep dive, embedding providers, storage backends, recipes, troubleshooting, architecture.
555
+ 📖 **[Full User Guide](docs/guide.md)** - configuration deep dive, embedding providers, storage backends, recipes, troubleshooting, architecture.
512
556
 
513
557
  ## License
514
558
 
515
- MITdo whatever you want.
559
+ [Elastic License 2.0](LICENSE) free to use, modify, and distribute. You just can't offer it as a hosted/managed service.
package/docs/guide.md CHANGED
@@ -25,8 +25,9 @@
25
25
  17. [CLI Reference](#cli-reference)
26
26
  18. [OpenClaw Integration](#openclaw-integration)
27
27
  19. [Recipes](#recipes)
28
- 20. [Troubleshooting](#troubleshooting)
29
- 21. [Architecture](#architecture)
28
+ 20. [Runtime Helpers](#runtime-helpers)
29
+ 21. [Troubleshooting](#troubleshooting)
30
+ 22. [Architecture](#architecture)
30
31
 
31
32
  ---
32
33
 
@@ -260,10 +261,12 @@ First-class Supabase backend with incremental operations and server-side vector
260
261
  storage: {
261
262
  type: 'supabase',
262
263
  url: process.env.SUPABASE_URL,
263
- key: process.env.SUPABASE_SERVICE_KEY,
264
+ key: process.env.SUPABASE_KEY, // Prefer anon key + RLS; service key bypasses row-level security
264
265
  }
265
266
  ```
266
267
 
268
+ > ⚠️ **Security:** Prefer a Supabase anon/public key with Row-Level Security (RLS) policies. A service key grants full database access and should only be used for admin operations, never in client-facing agents.
269
+
267
270
  **Setup:** Run `sql/schema.sql` in your Supabase Dashboard SQL Editor to create the required tables.
268
271
 
269
272
  **Features:**
@@ -329,6 +332,8 @@ This lets you back neolata-mem with PostgreSQL, SQLite, Redis, S3 — anything.
329
332
 
330
333
  ### Storing
331
334
 
335
+ > **Note:** Agent IDs like `'kuro'` and `'maki'` used throughout this guide are just examples — use any string to identify your agents.
336
+
332
337
  ```javascript
333
338
  const result = await mem.store('kuro', 'Found XSS in login form', {
334
339
  category: 'finding', // finding | decision | fact | insight | task | event | preference
@@ -1125,7 +1130,7 @@ const mem = createMemory({
1125
1130
  storage: {
1126
1131
  type: 'supabase',
1127
1132
  url: process.env.SUPABASE_URL,
1128
- key: process.env.SUPABASE_SERVICE_KEY,
1133
+ key: process.env.SUPABASE_KEY, // Prefer anon key + RLS
1129
1134
  },
1130
1135
  embeddings: {
1131
1136
  type: 'openai',
@@ -1148,6 +1153,8 @@ const results = await mem.search('kuro', 'dark mode');
1148
1153
 
1149
1154
  ### Write-through to webhooks
1150
1155
 
1156
+ > ⚠️ **Security:** Webhook URLs are an explicit data exfiltration surface. Each store/decay event sends memory content to the configured endpoint. Only configure URLs you trust and control.
1157
+
1151
1158
  ```javascript
1152
1159
  import { createMemory, webhookWritethrough } from '@jeremiaheth/neolata-mem';
1153
1160
 
@@ -1166,6 +1173,172 @@ detach();
1166
1173
 
1167
1174
  ---
1168
1175
 
1176
+ ## Runtime Helpers
1177
+
1178
+ Standalone functions for common agent workflows. These work with any `MemoryGraph` instance and don't require special configuration.
1179
+
1180
+ ```javascript
1181
+ import {
1182
+ detectKeyMoments, extractTopicSlug,
1183
+ heartbeatStore, contextualRecall, preCompactionDump,
1184
+ } from '@jeremiaheth/neolata-mem';
1185
+ ```
1186
+
1187
+ ### detectKeyMoments(text, opts?)
1188
+
1189
+ Scans text for decisions, preferences, commitments, and blockers using pattern matching.
1190
+
1191
+ ```javascript
1192
+ const moments = detectKeyMoments("Decision: we're going with Supabase. Blocked by RLS permissions.");
1193
+ // [
1194
+ // { type: 'decision', text: "Decision: we're going with Supabase", importance: 0.9 },
1195
+ // { type: 'blocker', text: "Blocked by RLS permissions", importance: 0.85 },
1196
+ // ]
1197
+ ```
1198
+
1199
+ **Moment types and importance:**
1200
+
1201
+ | Type | Importance | Trigger patterns |
1202
+ |------|-----------|-----------------|
1203
+ | `decision` | 0.9 | "Decision:", "We decided", "Going with", "Let's do", "Ship it" |
1204
+ | `commitment` | 0.8 | "I will", "We will", "TODO:", "Action item:" |
1205
+ | `blocker` | 0.85 | "Blocked by", "Blocker:", "Can't proceed", "Waiting on" |
1206
+ | `preference` | 0.7 | "I prefer", "I like", "I want", "Always use" |
1207
+
1208
+ ### extractTopicSlug(text, opts?)
1209
+
1210
+ Derives a topic slug from text by finding the most frequent non-stop-word. Supports synonym mapping.
1211
+
1212
+ ```javascript
1213
+ extractTopicSlug('How did we fix the RLS policy issue?');
1214
+ // → 'rls'
1215
+
1216
+ extractTopicSlug('Update the neolata package', {
1217
+ synonyms: { 'neolata-mem': ['neolata', 'memory', 'mem'] }
1218
+ });
1219
+ // → 'neolata-mem'
1220
+ ```
1221
+
1222
+ ### heartbeatStore(mem, agent, turns, config?)
1223
+
1224
+ Auto-stores key moments from conversation turns. Designed to be called periodically (e.g., on a heartbeat timer). If no key moments are detected, stores a truncated session snapshot instead.
1225
+
1226
+ **Config:**
1227
+
1228
+ | Option | Type | Default | Description |
1229
+ |--------|------|---------|-------------|
1230
+ | `sessionId` | string | — | Tag stored memories with session ID |
1231
+ | `topicSlug` | string | — | Tag with topic |
1232
+ | `projectSlug` | string | — | Tag with project |
1233
+ | `minNewTurns` | number | 3 | Skip if fewer new turns since last call |
1234
+ | `lastStoredIndex` | number | -1 | Index of last processed turn (track across calls) |
1235
+
1236
+ **Returns:** `{ stored, ids, lastIndex, moments, skipped? }`
1237
+
1238
+ ```javascript
1239
+ const turns = [
1240
+ { role: 'user', content: 'Let\'s do the Supabase migration' },
1241
+ { role: 'assistant', content: 'Decision: migrating to Supabase for production storage.' },
1242
+ { role: 'user', content: 'Ship it' },
1243
+ { role: 'assistant', content: 'TODO: run the migration script on OCI.' },
1244
+ ];
1245
+
1246
+ let lastIndex = -1;
1247
+ const result = await heartbeatStore(mem, 'kuro', turns, {
1248
+ sessionId: 'sess-abc',
1249
+ topicSlug: 'supabase',
1250
+ lastStoredIndex: lastIndex,
1251
+ });
1252
+ // → { stored: 3, ids: [...], lastIndex: 3, moments: [...] }
1253
+
1254
+ lastIndex = result.lastIndex; // track for next heartbeat call
1255
+ ```
1256
+
1257
+ ### contextualRecall(mem, agent, seedText, config?)
1258
+
1259
+ Budget-aware context retrieval that merges three recall strategies: recent memories, semantic search results, and high-importance memories filtered by topic. Results are deduplicated, sorted by importance, and capped to a token budget.
1260
+
1261
+ **Config:**
1262
+
1263
+ | Option | Type | Default | Description |
1264
+ |--------|------|---------|-------------|
1265
+ | `maxTokens` | number | 2000 | Token budget for returned memories |
1266
+ | `recentCount` | number | 5 | Number of recent memories to fetch |
1267
+ | `semanticCount` | number | 8 | Number of semantic search results |
1268
+ | `importantCount` | number | 10 | Candidates for importance filtering |
1269
+ | `importanceThreshold` | number | 0.8 | Minimum importance for the "important" lane |
1270
+ | `synonyms` | object | {} | Synonym map for topic extraction |
1271
+
1272
+ **Returns:** `{ topicSlug, memories, totalTokens, excluded }`
1273
+
1274
+ ```javascript
1275
+ const ctx = await contextualRecall(mem, 'kuro', 'What was our RLS fix?', {
1276
+ maxTokens: 1500,
1277
+ importanceThreshold: 0.7,
1278
+ });
1279
+
1280
+ console.log(ctx.topicSlug); // 'rls'
1281
+ console.log(ctx.memories); // [...] deduplicated, importance-sorted, budget-capped
1282
+ console.log(ctx.totalTokens); // 1342
1283
+ console.log(ctx.excluded); // 5 (didn't fit in budget)
1284
+ ```
1285
+
1286
+ ### preCompactionDump(mem, agent, turns, config?)
1287
+
1288
+ Extracts key moments from a full conversation, deduplicates them, persists the top takeaways, and stores a structured session snapshot. Call this before context window compaction to preserve important information.
1289
+
1290
+ **Config:**
1291
+
1292
+ | Option | Type | Default | Description |
1293
+ |--------|------|---------|-------------|
1294
+ | `sessionId` | string | — | Tag stored memories with session ID |
1295
+ | `topicSlug` | string | — | Tag with topic |
1296
+ | `projectSlug` | string | — | Tag with project |
1297
+ | `maxTakeaways` | number | 10 | Maximum individual moments to persist |
1298
+
1299
+ **Returns:** `{ takeaways, snapshotId, ids }`
1300
+
1301
+ The session snapshot is a markdown-formatted summary grouped by type:
1302
+
1303
+ ```
1304
+ ## Session Snapshot
1305
+ **Decisions:** migrating to Supabase for production storage
1306
+ **Open threads:** Blocked by RLS permissions on link inserts
1307
+ **Commitments:** TODO: run the migration script on OCI
1308
+ **Preferences:** I prefer service key over anon key for writes
1309
+ ```
1310
+
1311
+ ```javascript
1312
+ const dump = await preCompactionDump(mem, 'kuro', allTurns, {
1313
+ sessionId: 'sess-abc',
1314
+ maxTakeaways: 10,
1315
+ });
1316
+ // → { takeaways: 4, snapshotId: 'uuid-...', ids: [...] }
1317
+ ```
1318
+
1319
+ ### Workflow: Combining All Three
1320
+
1321
+ A typical agent lifecycle using all three helpers:
1322
+
1323
+ ```javascript
1324
+ // 1. On session start: recall context
1325
+ const ctx = await contextualRecall(mem, agent, userMessage);
1326
+ // → inject ctx.memories into system prompt
1327
+
1328
+ // 2. Periodically during conversation: heartbeat store
1329
+ let lastIdx = -1;
1330
+ setInterval(async () => {
1331
+ const r = await heartbeatStore(mem, agent, turns, { lastStoredIndex: lastIdx });
1332
+ lastIdx = r.lastIndex;
1333
+ }, 60_000);
1334
+
1335
+ // 3. Before compaction: dump takeaways
1336
+ const dump = await preCompactionDump(mem, agent, turns);
1337
+ // → key moments and snapshot persisted to graph
1338
+ ```
1339
+
1340
+ ---
1341
+
1169
1342
  ## Troubleshooting
1170
1343
 
1171
1344
  ### "No results from search"