@memtensor/memos-local-openclaw-plugin 0.1.2 → 0.1.4

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.
Files changed (77) hide show
  1. package/.env.example +13 -5
  2. package/README.md +180 -68
  3. package/dist/capture/index.d.ts +5 -7
  4. package/dist/capture/index.d.ts.map +1 -1
  5. package/dist/capture/index.js +72 -43
  6. package/dist/capture/index.js.map +1 -1
  7. package/dist/ingest/providers/anthropic.d.ts +2 -0
  8. package/dist/ingest/providers/anthropic.d.ts.map +1 -1
  9. package/dist/ingest/providers/anthropic.js +110 -1
  10. package/dist/ingest/providers/anthropic.js.map +1 -1
  11. package/dist/ingest/providers/bedrock.d.ts +2 -5
  12. package/dist/ingest/providers/bedrock.d.ts.map +1 -1
  13. package/dist/ingest/providers/bedrock.js +110 -6
  14. package/dist/ingest/providers/bedrock.js.map +1 -1
  15. package/dist/ingest/providers/gemini.d.ts +2 -0
  16. package/dist/ingest/providers/gemini.d.ts.map +1 -1
  17. package/dist/ingest/providers/gemini.js +106 -1
  18. package/dist/ingest/providers/gemini.js.map +1 -1
  19. package/dist/ingest/providers/index.d.ts +9 -0
  20. package/dist/ingest/providers/index.d.ts.map +1 -1
  21. package/dist/ingest/providers/index.js +66 -4
  22. package/dist/ingest/providers/index.js.map +1 -1
  23. package/dist/ingest/providers/openai.d.ts +2 -0
  24. package/dist/ingest/providers/openai.d.ts.map +1 -1
  25. package/dist/ingest/providers/openai.js +112 -1
  26. package/dist/ingest/providers/openai.js.map +1 -1
  27. package/dist/ingest/task-processor.d.ts +63 -0
  28. package/dist/ingest/task-processor.d.ts.map +1 -0
  29. package/dist/ingest/task-processor.js +339 -0
  30. package/dist/ingest/task-processor.js.map +1 -0
  31. package/dist/ingest/worker.d.ts +1 -1
  32. package/dist/ingest/worker.d.ts.map +1 -1
  33. package/dist/ingest/worker.js +18 -13
  34. package/dist/ingest/worker.js.map +1 -1
  35. package/dist/recall/engine.d.ts +1 -0
  36. package/dist/recall/engine.d.ts.map +1 -1
  37. package/dist/recall/engine.js +21 -11
  38. package/dist/recall/engine.js.map +1 -1
  39. package/dist/recall/mmr.d.ts.map +1 -1
  40. package/dist/recall/mmr.js +3 -1
  41. package/dist/recall/mmr.js.map +1 -1
  42. package/dist/storage/sqlite.d.ts +67 -1
  43. package/dist/storage/sqlite.d.ts.map +1 -1
  44. package/dist/storage/sqlite.js +251 -5
  45. package/dist/storage/sqlite.js.map +1 -1
  46. package/dist/types.d.ts +15 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js +2 -0
  49. package/dist/types.js.map +1 -1
  50. package/dist/viewer/html.d.ts +1 -1
  51. package/dist/viewer/html.d.ts.map +1 -1
  52. package/dist/viewer/html.js +955 -115
  53. package/dist/viewer/html.js.map +1 -1
  54. package/dist/viewer/server.d.ts +3 -0
  55. package/dist/viewer/server.d.ts.map +1 -1
  56. package/dist/viewer/server.js +59 -1
  57. package/dist/viewer/server.js.map +1 -1
  58. package/index.ts +221 -45
  59. package/openclaw.plugin.json +20 -45
  60. package/package.json +3 -4
  61. package/skill/SKILL.md +59 -0
  62. package/src/capture/index.ts +85 -45
  63. package/src/ingest/providers/anthropic.ts +128 -1
  64. package/src/ingest/providers/bedrock.ts +130 -6
  65. package/src/ingest/providers/gemini.ts +128 -1
  66. package/src/ingest/providers/index.ts +74 -8
  67. package/src/ingest/providers/openai.ts +130 -1
  68. package/src/ingest/task-processor.ts +380 -0
  69. package/src/ingest/worker.ts +21 -15
  70. package/src/recall/engine.ts +22 -12
  71. package/src/recall/mmr.ts +3 -1
  72. package/src/storage/sqlite.ts +298 -5
  73. package/src/types.ts +19 -0
  74. package/src/viewer/html.ts +955 -115
  75. package/src/viewer/server.ts +63 -1
  76. package/SKILL.md +0 -43
  77. package/www/index.html +0 -606
package/.env.example CHANGED
@@ -1,11 +1,19 @@
1
- # Embedding API
2
- # Use any OpenAI-compatible embedding service, or leave blank for local offline model
1
+ # ─── Embedding API ───
2
+ # Use any OpenAI-compatible embedding service, or leave blank for local offline model (Xenova/all-MiniLM-L6-v2)
3
+ EMBEDDING_PROVIDER=openai_compatible
3
4
  EMBEDDING_API_KEY=your-embedding-api-key
4
- EMBEDDING_ENDPOINT=https://api.openai.com/v1
5
- EMBEDDING_MODEL=text-embedding-3-small
5
+ EMBEDDING_ENDPOINT=https://your-embedding-api.com/v1
6
+ EMBEDDING_MODEL=bge-m3
6
7
 
7
- # Summarizer API (OpenAI-compatible)
8
+ # ─── Summarizer API ───
9
+ # OpenAI-compatible LLM for one-sentence chunk summaries
8
10
  # Leave blank to use rule-based fallback (no LLM needed)
11
+ SUMMARIZER_PROVIDER=openai_compatible
9
12
  SUMMARIZER_API_KEY=your-summarizer-api-key
10
13
  SUMMARIZER_ENDPOINT=https://api.openai.com/v1
11
14
  SUMMARIZER_MODEL=gpt-4o-mini
15
+ SUMMARIZER_TEMPERATURE=0
16
+
17
+ # ─── Memory Viewer ───
18
+ # Port for the web-based Memory Viewer (default: 18799)
19
+ # VIEWER_PORT=18799
package/README.md CHANGED
@@ -6,12 +6,17 @@ Persistent local conversation memory for [OpenClaw](https://github.com/nicepkg/o
6
6
 
7
7
  ## Features
8
8
 
9
- - **Auto-capture** — Stores user, assistant, and tool messages after each agent turn
10
- - **Semantic chunking** — Preserves complete code blocks, function bodies, and paragraph boundaries
9
+ - **Auto-capture** — Stores user, assistant, and tool messages after each agent turn via `agent_end` event
10
+ - **Content deduplication** — SHA-256 content hashing prevents duplicate chunks across repeated ingestion
11
+ - **Task-level organization** — Conversations auto-grouped into tasks by topic shift (LLM-judged) or 2-hour idle timeout; each completed task gets a detailed LLM summary
12
+ - **Task status** — `active` (in progress), `completed` (with LLM summary), `skipped` (too brief, excluded from search)
11
13
  - **Hybrid retrieval** — FTS5 keyword + vector semantic dual-channel search with RRF fusion
12
- - **LLM summarization** — One-sentence summary per chunk for fast browsing
13
- - **Multi-provider** — OpenAI, Anthropic, Gemini, Cohere, Voyage, Mistral, Bedrock, or local offline
14
- - **Web viewer** — Built-in dashboard at `http://127.0.0.1:18799` with CRUD, search, filters, pagination
14
+ - **LLM summarization** — Detailed task summaries preserving code, commands, URLs, file paths; chunk-level short summaries for search indexing
15
+ - **Multi-provider embedding** — OpenAI-compatible, Gemini, Cohere, Voyage, Mistral, or local offline (Xenova/all-MiniLM-L6-v2)
16
+ - **Multi-provider summarizer** — OpenAI-compatible, Anthropic, Gemini, AWS Bedrock
17
+ - **Web viewer** — Built-in dashboard at `http://127.0.0.1:18799` with CRUD, search, analytics, task browser, chat-bubble chunk view
18
+ - **Skill-based retrieval** — Agent learns when and how to use memory tools via a bundled Skill (no system prompt injection)
19
+ - **i18n** — Memory Viewer supports Chinese / English with one-click toggle
15
20
  - **Privacy first** — All data in local SQLite, no cloud uploads, password-protected viewer
16
21
 
17
22
  ## Quick Start
@@ -24,9 +29,9 @@ Persistent local conversation memory for [OpenClaw](https://github.com/nicepkg/o
24
29
  openclaw plugins install @memtensor/memos-local-openclaw-plugin
25
30
  ```
26
31
 
27
- The plugin is installed under `~/.openclaw/extensions/` and registered as `memos-local`. No clone or build required.
32
+ The plugin is installed under `~/.openclaw/extensions/memos-local` and registered as `memos-local`.
28
33
 
29
- > **Important:** Installing the plugin does **not** start the Memory Viewer. The viewer HTTP service is started only when the **OpenClaw gateway** is running. After install, you must **configure** `openclaw.json` (step 2) and **start or restart the gateway** (step 3); then the viewer will be available at `http://127.0.0.1:18799`.
34
+ > **Important:** The Memory Viewer starts only when the **OpenClaw gateway** is running. After install, **configure** `openclaw.json` (step 2) and **start the gateway** (step 3); the viewer will then be available at `http://127.0.0.1:18799`.
30
35
 
31
36
  **From source (development):**
32
37
 
@@ -43,6 +48,14 @@ Add the plugin config to `~/.openclaw/openclaw.json`:
43
48
 
44
49
  ```jsonc
45
50
  {
51
+ "agents": {
52
+ "defaults": {
53
+ // IMPORTANT: Disable OpenClaw's built-in memory to avoid conflicts
54
+ "memorySearch": {
55
+ "enabled": false
56
+ }
57
+ }
58
+ },
46
59
  "plugins": {
47
60
  "slots": {
48
61
  "memory": "memos-local"
@@ -71,6 +84,8 @@ Add the plugin config to `~/.openclaw/openclaw.json`:
71
84
  }
72
85
  ```
73
86
 
87
+ > **Critical:** You must set `agents.defaults.memorySearch.enabled` to `false`. Otherwise OpenClaw's built-in memory search runs alongside this plugin, causing duplicate retrieval and wasted tokens.
88
+
74
89
  #### Embedding Provider Options
75
90
 
76
91
  | Provider | `provider` value | Example `model` | Notes |
@@ -107,8 +122,6 @@ Use `${ENV_VAR}` placeholders in config to avoid hardcoding keys:
107
122
 
108
123
  ### 3. Start or Restart the Gateway
109
124
 
110
- The Memory Viewer and all plugin features only run when the OpenClaw gateway is running. After installing and configuring the plugin, start (or restart) the gateway:
111
-
112
125
  ```bash
113
126
  openclaw gateway stop # if already running
114
127
  openclaw gateway install # ensure LaunchAgent is installed (macOS)
@@ -120,7 +133,6 @@ Once the gateway is up, the plugin loads and starts the Memory Viewer at `http:/
120
133
  ### 4. Verify Installation
121
134
 
122
135
  ```bash
123
- # Check the gateway log
124
136
  tail -20 ~/.openclaw/logs/gateway.log
125
137
  ```
126
138
 
@@ -145,73 +157,113 @@ memos-local: started (embedding: openai_compatible)
145
157
  **Step C** — In a new conversation, ask the agent to recall what you discussed:
146
158
 
147
159
  ```
148
- You: Do you remember what we talked about?
149
- Agent: (calls memory_search) Yes, we discussed...
150
- ```
151
-
152
- **Step D** — Check the gateway log for ingest activity:
153
-
154
- ```bash
155
- grep -E "Stored chunk|Chunked turn|Dedup" ~/.openclaw/logs/gateway.log | tail -10
160
+ You: 你还记得我之前让你帮我处理过什么事情吗?
161
+ Agent: (calls memory_search) 是的,我们之前讨论过...
156
162
  ```
157
163
 
158
- You should see lines like:
164
+ ## Retrieval Strategy
159
165
 
160
- ```
161
- Chunked turn=1772459198930-839rr3 into 3 chunks
162
- Stored chunk=667f289e kind=paragraph len=392 hasVec=true
163
- Stored chunk=107d7f32 kind=tool_result role=tool len=210 hasVec=true
164
- ```
166
+ The plugin uses a **Skill-based** approach to teach the agent when and how to use memory tools. A `SKILL.md` file is automatically installed into the workspace skills directory (`~/.openclaw/workspace/skills/memos-local/`).
165
167
 
166
- ### 6. Run the Smoke Test (Optional)
168
+ The Skill instructs the agent to follow a **layered retrieval strategy**:
167
169
 
168
- If you have the source (e.g. cloned the repo or develop locally):
169
-
170
- ```bash
171
- cd MemOS/apps/memos-local-openclaw # or the path where the plugin source is
172
- cp .env.example .env
173
- # Edit .env with your actual API keys
174
- npx tsx scripts/smoke-test.ts
175
- ```
170
+ 1. **`memory_search`** Lightweight search returning hit summaries + IDs. Always the first step.
171
+ 2. **Sufficiency check** — If summaries are enough, answer directly without drilling down.
172
+ 3. **`memory_get`** — Read the full original text of a specific hit when the summary is truncated or insufficient.
173
+ 4. **`task_summary`** — Get the complete task-level summary when the answer may be spread across multiple turns of the same task.
174
+ 5. **`memory_timeline`** — Expand surrounding conversation context when cause-and-effect or chronological details are needed.
175
+ 6. **Ask user** Only after exhausting search budget, ask the user a minimal follow-up question.
176
176
 
177
- The smoke test writes test conversations, searches for them, verifies timeline and get operations, and checks anti-writeback protection. If you installed only from npm, you can skip this step.
177
+ The Skill triggers when:
178
+ - The current context is insufficient to answer definitively
179
+ - The agent is about to ask the user for information (search memory first)
180
+ - The user references past conversations
181
+ - User-specific information is needed (identity, preferences, project config, etc.)
178
182
 
179
183
  ## Agent Tools
180
184
 
181
- The plugin registers 4 tools that your agent can use:
185
+ The plugin registers 5 tools:
182
186
 
183
- | Tool | Purpose |
184
- |---|---|
185
- | `memory_search` | Search memories by natural language query |
186
- | `memory_timeline` | Get surrounding context for a search hit |
187
- | `memory_get` | Get full original text of a memory chunk |
188
- | `memory_viewer` | Get the URL of the web dashboard |
187
+ | Tool | Purpose | When to Use |
188
+ |---|---|---|
189
+ | `memory_search` | Search memories by natural language query | When current context lacks needed facts; before asking the user for information |
190
+ | `memory_get` | Get full original text of a memory chunk | When exact details (commands, code, errors) are needed from a specific hit |
191
+ | `task_summary` | Get detailed summary of a completed task | When the answer may be spread across multiple turns of the same task |
192
+ | `memory_timeline` | Get surrounding context for a search hit | When a search hit needs cause-and-effect or chronological context |
193
+ | `memory_viewer` | Get the URL of the web dashboard | When user asks where to view/manage memories |
189
194
 
190
- The agent uses these automatically via the SKILL.md prompt guide.
195
+ ### Search Parameters
196
+
197
+ | Parameter | Default | Range | Description |
198
+ |---|---|---|---|
199
+ | `query` | — | — | Natural language search query |
200
+ | `maxResults` | 6 | 1–20 | Maximum number of results |
201
+ | `minScore` | 0.45 | 0.35–1.0 | Minimum relevance score |
202
+ | `role` | — | `user` / `assistant` / `tool` | Filter by message role |
191
203
 
192
204
  ## Memory Viewer
193
205
 
194
- Open `http://127.0.0.1:18799` in your browser:
206
+ Open `http://127.0.0.1:18799` in your browser after starting the gateway.
207
+
208
+ **Features:**
209
+ - Browse all stored memories with timeline view
210
+ - Semantic search powered by your embedding model
211
+ - Create, edit, and delete memories (CRUD)
212
+ - Task browser with status filters (active / completed / skipped)
213
+ - Chat-bubble style chunk view in task details
214
+ - Filter by session, role (user/assistant/tool), time range
215
+ - Analytics dashboard — daily write/read activity charts, memory breakdown
216
+ - Light / Dark theme toggle
217
+ - Chinese / English language toggle
218
+ - Pagination (30 per page)
195
219
 
196
- **Viewer won't open or page not loading?**
220
+ **Viewer won't open?**
197
221
 
198
222
  - The viewer is started by the plugin when the **gateway** starts. It does **not** run at install time.
199
- - Ensure the gateway is running: `openclaw gateway start` (or restart with `openclaw gateway stop` then `openclaw gateway start`).
200
- - Ensure the plugin is enabled in `~/.openclaw/openclaw.json`: `plugins.slots.memory` = `"memos-local"` and `plugins.entries.memos-local.enabled` = `true`.
201
- - Check the gateway log: `tail -30 ~/.openclaw/logs/gateway.log` — you should see `MemOS Memory Viewer` and `→ http://127.0.0.1:18799`. If the viewer fails to bind (e.g. port in use), the log will show a warning.
202
-
203
- - First visit: set a password (min 4 chars)
204
- - Browse, search, create, edit, delete memories
205
- - Filter by role (user/assistant/tool), type, time range (down to seconds)
206
- - Pagination (30 per page)
223
+ - Ensure the gateway is running: `openclaw gateway start`
224
+ - Ensure the plugin is enabled in `~/.openclaw/openclaw.json`
225
+ - Check the log: `tail -30 ~/.openclaw/logs/gateway.log` — look for `MemOS Memory Viewer`
207
226
 
208
- **Forgot password?** Click "Forgot password?" on the login page and use the reset token from the gateway log:
227
+ **Forgot password?** Click "Forgot password?" on the login page and use the reset token:
209
228
 
210
229
  ```bash
211
- # 必须用 "password reset token:" 才能匹配到带 token 的那一行(不要用 "reset token")
212
- grep "password reset token:" /tmp/openclaw/openclaw-*.log ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1
230
+ grep "password reset token:" ~/.openclaw/logs/gateway.log 2>/dev/null | tail -1
231
+ ```
232
+
233
+ Copy the 32-character hex string after `password reset token:`.
234
+
235
+ ## How It Works
236
+
237
+ **Write path** (automatic on every agent turn via `agent_end` event):
238
+
239
+ ```
240
+ Conversation → Capture (filter roles, strip system prompts)
241
+ → Content hash dedup check → Store chunk (SQLite + FTS5 + Embed)
242
+ → Associate to Task → Task boundary detected?
243
+ → Yes: Finalize previous task
244
+ → Chunks ≥ 4 & turns ≥ 2: LLM summarize → status = "completed"
245
+ → Otherwise: status = "skipped" (excluded from search)
246
+ → No: Continue accumulating in current task
247
+ ```
248
+
249
+ Key details:
250
+ - System messages are skipped; tool results from the plugin's own tools are not re-stored
251
+ - Evidence wrapper blocks (`[STORED_MEMORY]...[/STORED_MEMORY]`) are stripped to prevent feedback loops
252
+ - Content hash (SHA-256, first 16 hex chars) prevents duplicate chunk ingestion within the same session+role
253
+ - Task boundaries are detected by LLM topic judgment or 2-hour idle timeout
254
+
255
+ **Read path** (when agent calls `memory_search`):
256
+
257
+ ```
258
+ Query → FTS5 + Vector dual recall → RRF Fusion → MMR Rerank
259
+ → Recency Decay → Score Filter → Top-K Results
213
260
  ```
214
- 输出可能是纯文本一行,或一段 JSON;从中复制 `password reset token:` 后面的 32 位 hex 即可。
261
+
262
+ Key details:
263
+ - **RRF (Reciprocal Rank Fusion):** Merges FTS5 and vector search rankings into a unified score
264
+ - **MMR (Maximal Marginal Relevance):** Re-ranks to balance relevance with diversity, preventing redundant results
265
+ - **Recency Decay:** Recent memories get a boost (half-life: 14 days by default)
266
+ - **Score Filter:** Relative threshold based on the top hit's score, preventing over-filtering with RRF's compressed score range
215
267
 
216
268
  ## Advanced Configuration
217
269
 
@@ -237,34 +289,94 @@ All optional — shown with defaults:
237
289
  }
238
290
  ```
239
291
 
240
- ## How It Works
292
+ ## Reinstall / Upgrade
241
293
 
242
- **Write path** (automatic on every agent turn):
294
+ If you see **"plugin already exists"** or **"plugin not found"**:
243
295
 
296
+ **Option A — Clean reinstall via OpenClaw CLI:**
297
+ ```bash
298
+ rm -rf ~/.openclaw/extensions/memos-local
299
+ openclaw plugins install @memtensor/memos-local-openclaw-plugin
300
+ cd ~/.openclaw/extensions/memos-local && npm install --omit=dev
301
+ openclaw gateway stop && openclaw gateway start
244
302
  ```
245
- Conversation → Capture (filter roles) → Semantic Chunk → LLM Summarize
246
- Embed Dedup Check → Store (SQLite + FTS5 + Vector)
303
+
304
+ **Option B Manual install (when config already references memos-local):**
305
+ ```bash
306
+ rm -rf ~/.openclaw/extensions/memos-local
307
+ cd /tmp
308
+ npm pack @memtensor/memos-local-openclaw-plugin
309
+ tar -xzf memtensor-memos-local-openclaw-plugin-*.tgz
310
+ mv package ~/.openclaw/extensions/memos-local
311
+ cd ~/.openclaw/extensions/memos-local && npm install --omit=dev
312
+ openclaw gateway stop && openclaw gateway start
247
313
  ```
248
314
 
249
- **Read path** (when agent calls `memory_search`):
315
+ **Plugin shows as "error" in `openclaw plugins list`?** (e.g. `Cannot find module '@sinclair/typebox'`)
250
316
 
251
- ```
252
- Query FTS5 + Vector dual recall → RRF Fusion → MMR Rerank
253
- → Recency Decay → Score Filter → Top-K Results
317
+ ```bash
318
+ cd ~/.openclaw/extensions/memos-local && npm install --omit=dev
254
319
  ```
255
320
 
256
- See the [full documentation](www/docs/) in the repo for detailed architecture and algorithm explanations.
321
+ Then restart the gateway.
257
322
 
258
- ## Data Location
323
+ ## Troubleshooting
259
324
 
260
- Whether you install from npm or from source, the plugin stores data under your OpenClaw state directory:
325
+ ### 常见问题排查
326
+
327
+ 1. **确认报错内容** — 记下完整报错,例如:`plugin not found`、`Cannot find module 'xxx'`、`Invalid config` 等。
328
+
329
+ 2. **看插件状态**
330
+ ```bash
331
+ openclaw plugins list
332
+ ```
333
+ - Status 为 **error** → 记下错误信息
334
+ - 列表中没有 MemOS Local → 未安装或未放到 `~/.openclaw/extensions/memos-local`
335
+
336
+ 3. **看网关日志**
337
+ ```bash
338
+ tail -50 ~/.openclaw/logs/gateway.log
339
+ ```
340
+ 搜索 `memos-local`、`failed to load`、`Error`、`Cannot find module`。
341
+
342
+ 4. **检查环境**
343
+ - Node 版本:`node -v`(需要 **>= 18**)
344
+ - 插件目录存在:`ls ~/.openclaw/extensions/memos-local/package.json`
345
+ - 依赖已安装:`ls ~/.openclaw/extensions/memos-local/node_modules/@sinclair/typebox`
346
+ 若不存在:`cd ~/.openclaw/extensions/memos-local && npm install --omit=dev`
347
+
348
+ 5. **检查配置** — 打开 `~/.openclaw/openclaw.json`,确认:
349
+ - `agents.defaults.memorySearch.enabled` = `false`(关闭内置记忆)
350
+ - `plugins.slots.memory` = `"memos-local"`
351
+ - `plugins.entries.memos-local.enabled` = `true`
352
+
353
+ 6. **记忆和 OpenClaw 内置记忆冲突** — 如果看到 agent 同时调用了内置的 memory 搜索和插件的 `memory_search`,说明 `agents.defaults.memorySearch.enabled` 没有设为 `false`。
354
+
355
+ ## Data Location
261
356
 
262
357
  | File | Path |
263
358
  |---|---|
264
359
  | Database | `~/.openclaw/memos-local/memos.db` |
265
360
  | Viewer auth | `~/.openclaw/memos-local/viewer-auth.json` |
266
361
  | Gateway log | `~/.openclaw/logs/gateway.log` |
267
- | Plugin code (npm install) | `~/.openclaw/extensions/` (managed by OpenClaw) |
362
+ | Plugin code | `~/.openclaw/extensions/memos-local/` |
363
+ | Skill | `~/.openclaw/workspace/skills/memos-local/SKILL.md` |
364
+
365
+ ## Testing
366
+
367
+ Run the test suite:
368
+
369
+ ```bash
370
+ cd MemOS/apps/memos-local-openclaw
371
+ npm test
372
+ ```
373
+
374
+ Test coverage includes:
375
+ - **Policy tests** — Verifies retrieval strategy, search filtering, evidence extraction, instruction stripping
376
+ - **Recall tests** — RRF fusion, recency decay correctness
377
+ - **Capture tests** — Message filtering, evidence block stripping, self-tool exclusion
378
+ - **Storage tests** — SQLite CRUD, FTS5, vector storage, content hash dedup
379
+ - **Task processor tests** — Task boundary detection, skip logic, summary generation
268
380
 
269
381
  ## License
270
382
 
@@ -1,16 +1,14 @@
1
1
  import type { ConversationMessage, Logger } from "../types";
2
2
  /**
3
- * Filter and extract writable messages from a conversation turn.
3
+ * Extract writable messages from a conversation turn.
4
4
  *
5
- * - Keep user, assistant, and tool messages
6
- * - Skip system prompts
7
- * - Skip tool results from our own memory tools (prevents memory loop)
8
- * - Truncate long tool results to avoid storage bloat
9
- * - Strip injected evidence blocks wrapped in [STORED_MEMORY]...[/STORED_MEMORY]
5
+ * Stores the user's actual text strips only OpenClaw's injected metadata
6
+ * prefixes (Sender info, conversation context, etc.) which are not user content.
7
+ * Only skips: system prompts and our own memory tool results (prevents loop).
10
8
  */
11
9
  export declare function captureMessages(messages: Array<{
12
10
  role: string;
13
11
  content: string;
14
12
  toolName?: string;
15
- }>, sessionKey: string, turnId: string, evidenceTag: string, log: Logger): ConversationMessage[];
13
+ }>, sessionKey: string, turnId: string, _evidenceTag: string, log: Logger): ConversationMessage[];
16
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capture/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAQ,MAAM,EAAE,MAAM,UAAU,CAAC;AAYlE;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACrE,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,GACV,mBAAmB,EAAE,CA+CvB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capture/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAQ,MAAM,EAAE,MAAM,UAAU,CAAC;AA0BlE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACrE,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,MAAM,GACV,mBAAmB,EAAE,CAgCvB"}
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.captureMessages = captureMessages;
4
- const types_1 = require("../types");
5
4
  const SKIP_ROLES = new Set(["system"]);
6
5
  const SELF_TOOLS = new Set([
7
6
  "memory_search",
@@ -9,16 +8,25 @@ const SELF_TOOLS = new Set([
9
8
  "memory_get",
10
9
  "memory_viewer",
11
10
  ]);
11
+ // OpenClaw inbound metadata sentinels — these are AI-facing prefixes,
12
+ // not user content. Must be stripped before storing as memory.
13
+ const INBOUND_META_SENTINELS = [
14
+ "Conversation info (untrusted metadata):",
15
+ "Sender (untrusted metadata):",
16
+ "Thread starter (untrusted, for context):",
17
+ "Replied message (untrusted, for context):",
18
+ "Forwarded message context (untrusted metadata):",
19
+ "Chat history since last reply (untrusted, for context):",
20
+ ];
21
+ const SENTINEL_FAST_RE = new RegExp(INBOUND_META_SENTINELS.map(s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"));
12
22
  /**
13
- * Filter and extract writable messages from a conversation turn.
23
+ * Extract writable messages from a conversation turn.
14
24
  *
15
- * - Keep user, assistant, and tool messages
16
- * - Skip system prompts
17
- * - Skip tool results from our own memory tools (prevents memory loop)
18
- * - Truncate long tool results to avoid storage bloat
19
- * - Strip injected evidence blocks wrapped in [STORED_MEMORY]...[/STORED_MEMORY]
25
+ * Stores the user's actual text strips only OpenClaw's injected metadata
26
+ * prefixes (Sender info, conversation context, etc.) which are not user content.
27
+ * Only skips: system prompts and our own memory tool results (prevents loop).
20
28
  */
21
- function captureMessages(messages, sessionKey, turnId, evidenceTag, log) {
29
+ function captureMessages(messages, sessionKey, turnId, _evidenceTag, log) {
22
30
  const now = Date.now();
23
31
  const result = [];
24
32
  for (const msg of messages) {
@@ -27,54 +35,75 @@ function captureMessages(messages, sessionKey, turnId, evidenceTag, log) {
27
35
  continue;
28
36
  if (!msg.content || msg.content.trim().length === 0)
29
37
  continue;
30
- if (role === "tool") {
31
- if (msg.toolName && SELF_TOOLS.has(msg.toolName)) {
32
- log.debug(`Skipping self-tool result: ${msg.toolName}`);
33
- continue;
34
- }
35
- let content = msg.content.trim();
36
- const maxChars = types_1.DEFAULTS.toolResultMaxChars;
37
- if (content.length > maxChars) {
38
- content = content.slice(0, maxChars) + `\n\n[truncated — original ${content.length} chars]`;
39
- }
40
- const toolLabel = msg.toolName ? `[tool:${msg.toolName}] ` : "[tool] ";
41
- result.push({
42
- role: "tool",
43
- content: toolLabel + content,
44
- timestamp: now,
45
- turnId,
46
- sessionKey,
47
- toolName: msg.toolName,
48
- });
38
+ if (role === "tool" && msg.toolName && SELF_TOOLS.has(msg.toolName)) {
39
+ log.debug(`Skipping self-tool result: ${msg.toolName}`);
49
40
  continue;
50
41
  }
51
- const cleaned = stripEvidenceBlocks(msg.content, evidenceTag);
52
- if (cleaned.trim().length === 0)
42
+ let content = msg.content;
43
+ if (role === "user") {
44
+ content = stripInboundMetadata(content);
45
+ }
46
+ if (!content.trim())
53
47
  continue;
54
48
  result.push({
55
49
  role,
56
- content: cleaned,
50
+ content,
57
51
  timestamp: now,
58
52
  turnId,
59
53
  sessionKey,
54
+ toolName: role === "tool" ? msg.toolName : undefined,
60
55
  });
61
56
  }
62
57
  log.debug(`Captured ${result.length}/${messages.length} messages for session=${sessionKey} turn=${turnId}`);
63
58
  return result;
64
59
  }
65
- function stripEvidenceBlocks(text, tag) {
66
- const openTag = `[${tag}]`;
67
- const closeTag = `[/${tag}]`;
68
- let result = text;
69
- let safety = 0;
70
- while (result.includes(openTag) && result.includes(closeTag) && safety < 50) {
71
- const start = result.indexOf(openTag);
72
- const end = result.indexOf(closeTag, start);
73
- if (end === -1)
74
- break;
75
- result = result.slice(0, start) + result.slice(end + closeTag.length);
76
- safety++;
60
+ /**
61
+ * Strip OpenClaw-injected inbound metadata blocks from user messages.
62
+ *
63
+ * These blocks have the shape:
64
+ * Sender (untrusted metadata):
65
+ * ```json
66
+ * { "label": "...", "id": "..." }
67
+ * ```
68
+ *
69
+ * Also strips the envelope timestamp prefix like "[Tue 2026-03-03 21:58 GMT+8] "
70
+ */
71
+ function stripInboundMetadata(text) {
72
+ if (!SENTINEL_FAST_RE.test(text))
73
+ return text;
74
+ const lines = text.split("\n");
75
+ const result = [];
76
+ let inMetaBlock = false;
77
+ let inFencedJson = false;
78
+ for (let i = 0; i < lines.length; i++) {
79
+ const line = lines[i];
80
+ const trimmed = line.trim();
81
+ if (!inMetaBlock && INBOUND_META_SENTINELS.some(s => s === trimmed)) {
82
+ if (lines[i + 1]?.trim() === "```json") {
83
+ inMetaBlock = true;
84
+ inFencedJson = false;
85
+ continue;
86
+ }
87
+ // Sentinel without fenced JSON — skip this line only
88
+ continue;
89
+ }
90
+ if (inMetaBlock) {
91
+ if (!inFencedJson && trimmed === "```json") {
92
+ inFencedJson = true;
93
+ continue;
94
+ }
95
+ if (inFencedJson && trimmed === "```") {
96
+ inMetaBlock = false;
97
+ inFencedJson = false;
98
+ continue;
99
+ }
100
+ continue;
101
+ }
102
+ result.push(line);
77
103
  }
78
- return result;
104
+ let cleaned = result.join("\n").trim();
105
+ // Strip envelope timestamp prefix: "[Tue 2026-03-03 21:58 GMT+8] actual message"
106
+ cleaned = cleaned.replace(/^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?\s+[A-Z]{3}[+-]\d{1,2}\]\s*/, "");
107
+ return cleaned;
79
108
  }
80
109
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capture/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capture/index.ts"],"names":[],"mappings":"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"}
@@ -1,3 +1,5 @@
1
1
  import type { SummarizerConfig, Logger } from "../../types";
2
+ export declare function summarizeTaskAnthropic(text: string, cfg: SummarizerConfig, log: Logger): Promise<string>;
3
+ export declare function judgeNewTopicAnthropic(currentContext: string, newMessage: string, cfg: SummarizerConfig, log: Logger): Promise<boolean>;
2
4
  export declare function summarizeAnthropic(text: string, cfg: SummarizerConfig, log: Logger): Promise<string>;
3
5
  //# sourceMappingURL=anthropic.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/ingest/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAI5D,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAgCjB"}
1
+ {"version":3,"file":"anthropic.d.ts","sourceRoot":"","sources":["../../../src/ingest/providers/anthropic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAyC5D,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CA8BjB;AAeD,wBAAsB,sBAAsB,CAC1C,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,OAAO,CAAC,CAkClB;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,gBAAgB,EACrB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,CAgCjB"}