@basicmemory/openclaw-basic-memory 0.24.0 → 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.
Files changed (87) hide show
  1. package/README.md +24 -12
  2. package/bm-client.ts +302 -112
  3. package/commands/cli.ts +5 -6
  4. package/commands/slash.ts +4 -4
  5. package/context-engine/basic-memory-context-engine.ts +74 -9
  6. package/context-engine/types.ts +21 -0
  7. package/dist/bm-client.d.ts +61 -12
  8. package/dist/bm-client.js +250 -101
  9. package/dist/commands/cli.d.ts +1 -1
  10. package/dist/commands/cli.js +5 -5
  11. package/dist/commands/slash.d.ts +1 -1
  12. package/dist/commands/slash.js +4 -3
  13. package/dist/context-engine/basic-memory-context-engine.d.ts +20 -1
  14. package/dist/context-engine/basic-memory-context-engine.js +50 -2
  15. package/dist/context-engine/types.d.ts +9 -0
  16. package/dist/context-engine/types.js +1 -0
  17. package/dist/index.d.ts +10 -4
  18. package/dist/index.js +79 -69
  19. package/dist/memory/basic-memory-capability.d.ts +19 -0
  20. package/dist/memory/basic-memory-capability.js +190 -0
  21. package/dist/tools/build-context.d.ts +1 -1
  22. package/dist/tools/build-context.js +12 -4
  23. package/dist/tools/delete-note.d.ts +1 -1
  24. package/dist/tools/delete-note.js +2 -1
  25. package/dist/tools/edit-note.d.ts +1 -1
  26. package/dist/tools/edit-note.js +22 -12
  27. package/dist/tools/list-memory-projects.d.ts +1 -1
  28. package/dist/tools/list-memory-projects.js +2 -1
  29. package/dist/tools/list-workspaces.d.ts +1 -1
  30. package/dist/tools/list-workspaces.js +4 -4
  31. package/dist/tools/move-note.d.ts +1 -1
  32. package/dist/tools/move-note.js +2 -1
  33. package/dist/tools/read-note.d.ts +1 -1
  34. package/dist/tools/read-note.js +2 -1
  35. package/dist/tools/schema-diff.d.ts +1 -1
  36. package/dist/tools/schema-diff.js +2 -1
  37. package/dist/tools/schema-infer.d.ts +1 -1
  38. package/dist/tools/schema-infer.js +2 -1
  39. package/dist/tools/schema-validate.d.ts +1 -1
  40. package/dist/tools/schema-validate.js +2 -1
  41. package/dist/tools/search-notes.d.ts +1 -1
  42. package/dist/tools/search-notes.js +13 -6
  43. package/dist/tools/write-note.d.ts +1 -1
  44. package/dist/tools/write-note.js +11 -12
  45. package/index.ts +93 -84
  46. package/memory/basic-memory-capability.ts +253 -0
  47. package/openclaw.plugin.json +0 -2
  48. package/package.json +6 -5
  49. package/skills/manifest.json +5 -0
  50. package/skills/memory-agents/SKILL.md +23 -10
  51. package/skills/memory-capture/SKILL.md +29 -18
  52. package/skills/memory-comark/SKILL.md +17 -6
  53. package/skills/memory-continue/SKILL.md +47 -27
  54. package/skills/memory-curate/SKILL.md +40 -24
  55. package/skills/memory-defrag/SKILL.md +18 -7
  56. package/skills/memory-doctor/SKILL.md +93 -0
  57. package/skills/memory-ingest/SKILL.md +33 -17
  58. package/skills/memory-lifecycle/SKILL.md +35 -22
  59. package/skills/memory-literary-analysis/SKILL.md +112 -75
  60. package/skills/memory-metadata-search/SKILL.md +39 -28
  61. package/skills/memory-notes/SKILL.md +75 -50
  62. package/skills/memory-okf/SKILL.md +27 -15
  63. package/skills/memory-onboarding/SKILL.md +20 -9
  64. package/skills/memory-onboarding/references/assistant-setup.md +1 -1
  65. package/skills/memory-onboarding/references/conventions.md +4 -4
  66. package/skills/memory-onboarding/references/domain-playbooks.md +1 -1
  67. package/skills/memory-onboarding/references/schema-guide.md +9 -8
  68. package/skills/memory-quest/SKILL.md +18 -7
  69. package/skills/memory-reflect/SKILL.md +25 -13
  70. package/skills/memory-research/SKILL.md +28 -13
  71. package/skills/memory-schema/SKILL.md +36 -24
  72. package/skills/memory-tasks/SKILL.md +34 -22
  73. package/tools/build-context.ts +14 -5
  74. package/tools/delete-note.ts +2 -2
  75. package/tools/edit-note.ts +27 -14
  76. package/tools/list-memory-projects.ts +6 -2
  77. package/tools/list-workspaces.ts +8 -5
  78. package/tools/move-note.ts +2 -2
  79. package/tools/read-note.ts +2 -2
  80. package/tools/schema-diff.ts +2 -2
  81. package/tools/schema-infer.ts +2 -2
  82. package/tools/schema-validate.ts +2 -2
  83. package/tools/search-notes.ts +18 -7
  84. package/tools/write-note.ts +12 -12
  85. package/dist/tools/memory-provider.d.ts +0 -15
  86. package/dist/tools/memory-provider.js +0 -275
  87. package/tools/memory-provider.ts +0 -336
@@ -13,6 +13,17 @@ month, and puts it where someone will look for it.
13
13
  Inspired by sleep-time compute: memory forms best between working sessions,
14
14
  not during them.
15
15
 
16
+ ## Project scope
17
+
18
+ If the project is unknown, discover it with `bm_project(command="list")`.
19
+ Pass a nonempty `project` on every note, folder, context and schema call, even
20
+ with one project or a qualified `memory://` URL. Use the chosen project's name,
21
+ `workspace/project`, or external ID; `<project>` below is a placeholder for it.
22
+ Recent activity also requires `project`. Search accepts `project` for one
23
+ project, `projects` for selected projects, or `all_projects=True` explicitly.
24
+ Discovery and bundled manuals need no project. Keep the same scope when following
25
+ returned paths, paginating, or recovering from a refusal.
26
+
16
27
  ## When to Run
17
28
 
18
29
  - **Scheduled**: once or twice a day, or on a heartbeat
@@ -26,23 +37,23 @@ not during them.
26
37
  ```python
27
38
  # The last reflection is the checkpoint. Titles carry date and time, so the
28
39
  # newest sorts first.
29
- list_directory(dir_name="reflections", sort="title_desc", page_size=1)
40
+ bm_list(path="reflections", sort="title_desc", page_size=1, project="<project>")
30
41
 
31
42
  # Everything changed since that date (json gives one row per note). With no
32
- # reflection yet, use timeframe="7d".
33
- recent_activity(timeframe="2026-10-13", output_format="json", page_size=50)
34
- # Ask for page=2, 3, ... until a page returns fewer than page_size rows.
43
+ # reflection yet, use since="7d".
44
+ bm_project(command="recent", since="2026-10-13", items=["note"], output_format="json", page_size=50, project="<project>")
45
+ # Ask for page=2, 3, ... until has_more is false.
35
46
 
36
47
  # Read each changed note in full before judging it
37
- read_note(identifier="meetings/2026-10-14-launch-sync")
48
+ bm_read(identifier="meetings/2026-10-14-launch-sync", project="<project>")
38
49
  ```
39
50
 
40
51
  Timeframes count whole days, so the window can overlap the last reflection.
41
52
  Skip notes it lists as `reviewed`, and notes you wrote in an earlier
42
53
  reflection, unless someone edited them since.
43
54
 
44
- `recent_activity` looks back at most a year. If the last reflection is older
45
- than that, use `timeframe="365d"` and record in this reflection that changes
55
+ `bm_project(command="recent", project="<project>")` looks back at most a year. If the last reflection is older
56
+ than that, use `since="365d"` and record in this reflection that changes
46
57
  before that window were not reviewed.
47
58
 
48
59
  ### 2. Decide what lasts
@@ -65,8 +76,8 @@ For each item, ask:
65
76
  Find the durable note for each item before writing anything:
66
77
 
67
78
  ```python
68
- search_notes(query="launch checklist owner")
69
- build_context(url="memory://projects/launch")
79
+ bm_search(query="launch checklist owner", project="<project>")
80
+ bm_context(identifier="memory://projects/launch", project="<project>")
70
81
  ```
71
82
 
72
83
  - **Say when it holds.** A fact with a date gets a qualifier (see
@@ -91,19 +102,20 @@ build_context(url="memory://projects/launch")
91
102
  checksum to the edit, so the edit applies only to the revision you read.
92
103
 
93
104
  ```python
94
- note = read_note(identifier="Launch Plan", output_format="json")
95
- edit_note(
105
+ note = bm_read(identifier="Launch Plan", output_format="json", project="<project>")
106
+ bm_edit(
96
107
  identifier="Launch Plan",
97
- operation="replace_section",
108
+ command="replace_section",
98
109
  section="## Owners",
99
110
  content="- [owner] Maya owns the launch checklist",
100
111
  expected_checksum=note["checksum"],
112
+ project="<project>",
101
113
  )
102
114
  ```
103
115
 
104
116
  If the edit reports a checksum conflict, someone changed the note after you
105
117
  read it: read it again and decide again from the new text.
106
- - **The topic has no note**: create one with `write_note`, following
118
+ - **The topic has no note**: create one with `bm_write`, following
107
119
  **memory-notes**: a clear title, observations with categories, and relations.
108
120
  - **Link back**: every consolidated fact names its source, e.g.
109
121
  `- derived_from [[2026-10-14 Launch Sync]]`, so a reader can check it.
@@ -7,6 +7,17 @@ description: "Research an external subject on the web and synthesize it into a s
7
7
 
8
8
  Research an external subject, synthesize what you find, and create a structured Basic Memory entity — with the user's approval.
9
9
 
10
+ ## Project scope
11
+
12
+ If the project is unknown, discover it with `bm_project(command="list")`.
13
+ Pass a nonempty `project` on every note, folder, context and schema call, even
14
+ with one project or a qualified `memory://` URL. Use the chosen project's name,
15
+ `workspace/project`, or external ID; `<project>` below is a placeholder for it.
16
+ Recent activity also requires `project`. Search accepts `project` for one
17
+ project, `projects` for selected projects, or `all_projects=True` explicitly.
18
+ Discovery and bundled manuals need no project. Keep the same scope when following
19
+ returned paths, paginating, or recovering from a refusal.
20
+
10
21
  ## Workflow
11
22
 
12
23
  ### Step 1: Web Research
@@ -36,8 +47,8 @@ Search for current information across multiple sources. Aim for 3-5 searches to
36
47
  Before proposing a new entity, search Basic Memory:
37
48
 
38
49
  ```python
39
- search_notes(query="Acme Corp")
40
- search_notes(query="acme")
50
+ bm_search(query="Acme Corp", project="<project>")
51
+ bm_search(query="acme", project="<project>")
41
52
  ```
42
53
 
43
54
  Try name variations — full name, abbreviation, acronym, domain name.
@@ -45,7 +56,7 @@ Try name variations — full name, abbreviation, acronym, domain name.
45
56
  If the entity already exists:
46
57
  - Report what you found in Basic Memory alongside your web research
47
58
  - Offer to update the existing note with new information
48
- - Use `edit_note` to append new observations or update outdated ones
59
+ - Use `bm_edit` to append new observations or update outdated ones
49
60
 
50
61
  If the entity doesn't exist, proceed to evaluation.
51
62
 
@@ -109,7 +120,7 @@ After approval, create a structured note. Adapt the template to the entity type:
109
120
  #### Organization
110
121
 
111
122
  ```python
112
- write_note(
123
+ bm_write(
113
124
  title="Acme Corp",
114
125
  directory="organizations",
115
126
  note_type="organization",
@@ -135,14 +146,15 @@ write_note(
135
146
  - [additional observations from research findings]
136
147
 
137
148
  ## Relations
138
- - [Link to related entities already in the knowledge graph]"""
149
+ - [Link to related entities already in the knowledge graph]""",
150
+ project="<project>",
139
151
  )
140
152
  ```
141
153
 
142
154
  #### Person
143
155
 
144
156
  ```python
145
- write_note(
157
+ bm_write(
146
158
  title="Jane Smith",
147
159
  directory="people",
148
160
  note_type="person",
@@ -163,14 +175,15 @@ write_note(
163
175
  - [source] Researched on YYYY-MM-DD
164
176
 
165
177
  ## Relations
166
- - works_at [[Organization]]"""
178
+ - works_at [[Organization]]""",
179
+ project="<project>",
167
180
  )
168
181
  ```
169
182
 
170
183
  #### Technology
171
184
 
172
185
  ```python
173
- write_note(
186
+ bm_write(
174
187
  title="Technology Name",
175
188
  directory="concepts",
176
189
  note_type="concept",
@@ -192,13 +205,14 @@ write_note(
192
205
  - [source] Researched on YYYY-MM-DD
193
206
 
194
207
  ## Relations
195
- - [Link to related concepts, tools, or projects in the knowledge graph]"""
208
+ - [Link to related concepts, tools, or projects in the knowledge graph]""",
209
+ project="<project>",
196
210
  )
197
211
  ```
198
212
 
199
213
  Adapt these templates freely. The key elements are: note_type/tags parameters, an overview, structured details, observations with categories, and relations.
200
214
 
201
- If `write_note` reports similar existing notes, check whether one of them is this
215
+ If `bm_write` reports similar existing notes, check whether one of them is this
202
216
  entity before keeping the new note; merge or link instead of duplicating.
203
217
 
204
218
  ### Step 6: Store Source Context
@@ -207,11 +221,12 @@ If the user provided context with their request, capture it in the entity:
207
221
 
208
222
  ```python
209
223
  # User said: "Acme Corp — saw their demo at the conference last week"
210
- edit_note(
224
+ bm_edit(
211
225
  identifier="Acme Corp",
212
- operation="insert_after_section",
226
+ command="insert_after_section",
213
227
  section="Observations",
214
- content="- [context] Saw their demo at conference, week of 2026-02-17"
228
+ content="- [context] Saw their demo at conference, week of 2026-02-17",
229
+ project="<project>",
215
230
  )
216
231
  ```
217
232
 
@@ -7,6 +7,17 @@ description: "Manage Basic Memory schemas: find unschemaed notes, infer and edit
7
7
 
8
8
  Manage structured note types using Basic Memory's Picoschema system. Schemas define what fields a note type should have, making notes uniform, queryable, and validatable.
9
9
 
10
+ ## Project scope
11
+
12
+ If the project is unknown, discover it with `bm_project(command="list")`.
13
+ Pass a nonempty `project` on every note, folder, context and schema call, even
14
+ with one project or a qualified `memory://` URL. Use the chosen project's name,
15
+ `workspace/project`, or external ID; `<project>` below is a placeholder for it.
16
+ Recent activity also requires `project`. Search accepts `project` for one
17
+ project, `projects` for selected projects, or `all_projects=True` explicitly.
18
+ Discovery and bundled manuals need no project. Keep the same scope when following
19
+ returned paths, paginating, or recovering from a refusal.
20
+
10
21
  ## When to Use
11
22
 
12
23
  - **New note type emerging** — you notice several notes share the same structure (meetings, people, decisions)
@@ -112,12 +123,12 @@ settings:
112
123
 
113
124
  Look for clusters of notes that share structure but have no schema:
114
125
 
115
- 1. **Search by type**: `search_notes(note_types=["meeting"])` — if many notes share a `type` but no `schema/Meeting.md` exists, it's a candidate.
126
+ 1. **Search by type**: `bm_search(note_types=["meeting"], project="<project>")` — if many notes share a `type` but no `schema/Meeting.md` exists, it's a candidate.
116
127
 
117
- 2. **Infer a schema**: Use `schema_infer` to analyze existing notes and generate a suggested schema:
128
+ 2. **Infer a schema**: Use `bm_schema(command="infer", project="<project>")` to analyze existing notes and generate a suggested schema:
118
129
  ```python
119
- schema_infer(note_type="Meeting")
120
- schema_infer(note_type="Meeting", threshold=0.5) # fields in 50%+ of notes
130
+ bm_schema(command="infer", note_type="Meeting", project="<project>")
131
+ bm_schema(command="infer", note_type="Meeting", threshold=0.5, project="<project>") # fields in 50%+ of notes
121
132
  ```
122
133
  The threshold (0.0–1.0) controls how common a field must be to be included. Default is usually fine; lower it to catch rarer fields.
123
134
 
@@ -128,7 +139,7 @@ Look for clusters of notes that share structure but have no schema:
128
139
  Write the schema note to `schema/<EntityName>`:
129
140
 
130
141
  ```python
131
- write_note(
142
+ bm_write(
132
143
  title="Meeting",
133
144
  directory="schema",
134
145
  note_type="schema",
@@ -150,7 +161,8 @@ Schema for meeting notes.
150
161
  ## Observations
151
162
  - [convention] Meeting notes live in memory/meetings/ or as daily entries
152
163
  - [convention] Always include date and topic
153
- - [convention] Action items should become tasks when complex"""
164
+ - [convention] Action items should become tasks when complex""",
165
+ project="<project>",
154
166
  )
155
167
  ```
156
168
 
@@ -168,13 +180,13 @@ Check how well existing notes conform to their schema:
168
180
 
169
181
  ```python
170
182
  # Validate all notes of a type
171
- schema_validate(note_type="Meeting")
183
+ bm_schema(command="validate", note_type="Meeting", project="<project>")
172
184
 
173
185
  # Validate a single note
174
- schema_validate(identifier="meetings/2026-02-10-standup")
186
+ bm_schema(command="validate", identifier="meetings/2026-02-10-standup", project="<project>")
175
187
  ```
176
188
 
177
- **Important:** `schema_validate` checks for schema fields as **observation categories** in the note body — e.g., a `status` field expects `- [status] active` as an observation. Fields stored only in frontmatter metadata won't satisfy validation. To pass cleanly, include schema fields as both frontmatter values (for metadata search) and observations (for schema validation).
189
+ **Important:** `bm_schema(command="validate", project="<project>")` checks for schema fields as **observation categories** in the note body — e.g., a `status` field expects `- [status] active` as an observation. Fields stored only in frontmatter metadata won't satisfy validation. To pass cleanly, include schema fields as both frontmatter values (for metadata search) and observations (for schema validation).
178
190
 
179
191
  Validation reports **missing required fields** (as observation categories, or relations
180
192
  for entity-reference fields) and **invalid enum values**. Undeclared observation categories
@@ -188,10 +200,10 @@ type-checked.
188
200
 
189
201
  ## Detecting Drift
190
202
 
191
- Over time, notes evolve and schemas lag behind. Use `schema_diff` to find divergence:
203
+ Over time, notes evolve and schemas lag behind. Use `bm_schema(command="diff", project="<project>")` to find divergence:
192
204
 
193
205
  ```python
194
- schema_diff(note_type="Meeting")
206
+ bm_schema(command="diff", note_type="Meeting", project="<project>")
195
207
  ```
196
208
 
197
209
  Diff reports:
@@ -203,15 +215,14 @@ Diff reports:
203
215
 
204
216
  When note structure changes:
205
217
 
206
- 1. **Run diff** to see current state: `schema_diff(note_type="Meeting")`
207
- 2. **Update the schema note** via `edit_note` with the `metadata` parameter. Top-level
218
+ 1. **Run diff** to see current state: `bm_schema(command="diff", note_type="Meeting", project="<project>")`
219
+ 2. **Update the schema note** via `bm_edit` with the `metadata` parameter. Top-level
208
220
  keys are replaced whole, so pass the complete updated `schema` map along with the new
209
221
  `version`:
210
222
  ```python
211
- edit_note(
223
+ bm_edit(
212
224
  identifier="schema/Meeting",
213
- operation="append",
214
- content="",
225
+ command="set_metadata",
215
226
  metadata={
216
227
  "version": 2,
217
228
  "schema": {
@@ -221,10 +232,11 @@ When note structure changes:
221
232
  "decisions?(array)": "string, decisions made",
222
233
  "location?": "string, where it happened"
223
234
  }
224
- }
235
+ },
236
+ project="<project>",
225
237
  )
226
238
  ```
227
- 3. **Re-validate** to confirm existing notes still pass: `schema_validate(note_type="Meeting")`
239
+ 3. **Re-validate** to confirm existing notes still pass: `bm_schema(command="validate", note_type="Meeting", project="<project>")`
228
240
  4. **Fix outliers** — update notes that don't conform to the new schema
229
241
 
230
242
  ### Evolution Guidelines
@@ -238,10 +250,10 @@ When note structure changes:
238
250
  ## Workflow Summary
239
251
 
240
252
  ```
241
- 1. Notice repeated note structure → infer schema (schema_infer)
242
- 2. Review + create schema note → write to schema/ (write_note)
243
- 3. Validate existing notes → check conformance (schema_validate)
244
- 4. Fix outliers → edit non-conforming notes (edit_note)
245
- 5. Periodically check drift → detect divergence (schema_diff)
246
- 6. Evolve schema as needed → update schema note (edit_note)
253
+ 1. Notice repeated note structure → infer schema (bm_schema(command="infer", project="<project>"))
254
+ 2. Review + create schema note → write to schema/ (bm_write)
255
+ 3. Validate existing notes → check conformance (bm_schema(command="validate", project="<project>"))
256
+ 4. Fix outliers → edit non-conforming notes (bm_edit)
257
+ 5. Periodically check drift → detect divergence (bm_schema(command="diff", project="<project>"))
258
+ 6. Evolve schema as needed → update schema note (bm_edit)
247
259
  ```
@@ -7,6 +7,17 @@ description: "Task management via Basic Memory schemas: create, track, and resum
7
7
 
8
8
  Manage work-in-progress using Basic Memory's schema system. Tasks are just notes with `type: Task` — they live in the knowledge graph, validate against a schema, and survive context compaction.
9
9
 
10
+ ## Project scope
11
+
12
+ If the project is unknown, discover it with `bm_project(command="list")`.
13
+ Pass a nonempty `project` on every note, folder, context and schema call, even
14
+ with one project or a qualified `memory://` URL. Use the chosen project's name,
15
+ `workspace/project`, or external ID; `<project>` below is a placeholder for it.
16
+ Recent activity also requires `project`. Search accepts `project` for one
17
+ project, `projects` for selected projects, or `all_projects=True` explicitly.
18
+ Discovery and bundled manuals need no project. Keep the same scope when following
19
+ returned paths, paginating, or recovering from a refusal.
20
+
10
21
  ## When to Use
11
22
 
12
23
  - **Starting multi-step work** (3+ steps, or anything that might outlast the context window)
@@ -42,10 +53,10 @@ settings:
42
53
 
43
54
  ## Creating a Task
44
55
 
45
- When work qualifies, create a task note. Use `write_note` with `note_type="Task"` and put queryable fields in `metadata`:
56
+ When work qualifies, create a task note. Use `bm_write` with `note_type="Task"` and put queryable fields in `metadata`:
46
57
 
47
58
  ```python
48
- write_note(
59
+ bm_write(
49
60
  title="Descriptive task name",
50
61
  directory="tasks",
51
62
  note_type="Task",
@@ -75,18 +86,19 @@ What future-you needs to pick up this work. Include:
75
86
  - Key file paths and repos involved
76
87
  - Decisions already made and why
77
88
  - What was tried and what worked/didn't
78
- - Where to look for related context"""
89
+ - Where to look for related context""",
90
+ project="<project>",
79
91
  )
80
92
  ```
81
93
 
82
- **Why both frontmatter and observations?** Fields in `metadata` (stored as frontmatter) power `search_notes` with `metadata_filters`. Fields as observations (`- [status] active`) power `schema_validate`. Include queryable fields in both places for full coverage.
94
+ **Why both frontmatter and observations?** Fields in `metadata` (stored as frontmatter) power `bm_search` with `filters`. Fields as observations (`- [status] active`) power `bm_schema(command="validate", project="<project>")`. Include queryable fields in both places for full coverage.
83
95
 
84
96
  ### Key Principles
85
97
 
86
98
  - **Steps are concrete and checkable** — "Implement X in file Y", not "figure out stuff"
87
99
  - **Context is for post-amnesia resumption** — Write it as if explaining to a smart person who knows nothing about what you've been doing
88
100
  - **Relations link to other entities** — `parent_task [[Other Task]]`, `related_to [[Some Note]]`
89
- - **Note types are normalized to snake_case** — `write_note(note_type="Task")` stores `type: task`, and `note_types=["Task"]` or `note_types=["task"]` both match it.
101
+ - **Note types are normalized to snake_case** — `bm_write(note_type="Task", project="<project>")` stores `type: task`, and `note_types=["Task"]` or `note_types=["task"]` both match it.
90
102
 
91
103
  ## Resuming After Compaction
92
104
 
@@ -94,7 +106,7 @@ On session start or after compaction:
94
106
 
95
107
  1. **Search for active tasks:**
96
108
  ```python
97
- search_notes(note_types=["task"], status="active")
109
+ bm_search(note_types=["task"], filters={"status": "active"}, project="<project>")
98
110
  ```
99
111
 
100
112
  2. **Read the task note** to get full context
@@ -122,15 +134,15 @@ edit. Two sessions often update the same task, for example in a pre-compaction
122
134
  flush; a revision conflict means the other one wrote first, so re-read before
123
135
  writing.
124
136
 
125
- Update frontmatter too, with the `metadata` parameter (empty `content` leaves the body alone):
137
+ Update frontmatter too, with `command="set_metadata"` and the `metadata` parameter (the body stays unchanged):
126
138
  ```python
127
- task = read_note(identifier="tasks/descriptive-task-name", output_format="json")
128
- edit_note(
139
+ task = bm_read(identifier="tasks/descriptive-task-name", output_format="json", project="<project>")
140
+ bm_edit(
129
141
  identifier="tasks/descriptive-task-name",
130
- operation="append",
131
- content="",
142
+ command="set_metadata",
132
143
  metadata={"current_step": 3},
133
- expected_checksum=task["checksum"]
144
+ expected_checksum=task["checksum"],
145
+ project="<project>",
134
146
  )
135
147
  ```
136
148
 
@@ -148,7 +160,7 @@ Add a brief summary of what was accomplished and any follow-up needed.
148
160
 
149
161
  When a compaction event is imminent:
150
162
 
151
- 1. Find all active tasks: `search_notes(note_types=["task"], status="active")`
163
+ 1. Find all active tasks: `bm_search(note_types=["task"], filters={"status": "active"}, project="<project>")`
152
164
  2. For each, update:
153
165
  - `current_step` to reflect actual progress
154
166
  - `context` with everything needed to resume
@@ -161,14 +173,14 @@ With BM's schema system, tasks are fully queryable:
161
173
 
162
174
  | Query | What it finds |
163
175
  |-------|--------------|
164
- | `search_notes(note_types=["task"])` | All tasks |
165
- | `search_notes(note_types=["task"], status="active")` | Active tasks |
166
- | `search_notes(note_types=["task"], status="blocked")` | Blocked tasks |
167
- | `search_notes(note_types=["task"], metadata_filters={"assigned_to": "claude"})` | My tasks |
168
- | `search_notes("blockers", note_types=["task"])` | Tasks with blockers |
169
- | `search_notes(note_types=["task"], time_kind="due", valid_overlaps="[2026-10-14,2026-10-21)")` | Tasks with a step due this week |
170
- | `schema_validate(note_type="Task")` | Validate all tasks against schema |
171
- | `schema_diff(note_type="Task")` | Detect drift between schema and actual task notes |
176
+ | `bm_search(note_types=["task"], project="<project>")` | All tasks |
177
+ | `bm_search(note_types=["task"], filters={"status": "active"}, project="<project>")` | Active tasks |
178
+ | `bm_search(note_types=["task"], filters={"status": "blocked"}, project="<project>")` | Blocked tasks |
179
+ | `bm_search(note_types=["task"], filters={"assigned_to": "claude"}, project="<project>")` | My tasks |
180
+ | `bm_search("blockers", note_types=["task"], project="<project>")` | Tasks with blockers |
181
+ | `bm_search(note_types=["task"], time_kind="due", valid_overlaps="[2026-10-14,2026-10-21)", project="<project>")` | Tasks with a step due this week |
182
+ | `bm_schema(command="validate", note_type="Task", project="<project>")` | Validate all tasks against schema |
183
+ | `bm_schema(command="diff", note_type="Task", project="<project>")` | Detect drift between schema and actual task notes |
172
184
 
173
185
  ### Due Dates
174
186
 
@@ -189,4 +201,4 @@ also returns overdue items.
189
201
  - **Context > steps** — Steps tell you what to do; context tells you why and how
190
202
  - **Close finished tasks** — Don't leave completed work as `active`
191
203
  - **Link related tasks** — Use `parent_task [[X]]` or relations to connect related work
192
- - **Schema validation is your friend** — Run `schema_validate(note_type="Task")` periodically to catch incomplete tasks
204
+ - **Schema validation is your friend** — Run `bm_schema(command="validate", note_type="Task", project="<project>")` periodically to catch incomplete tasks
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import { type BmClient, describeToolError } from "../bm-client.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
6
6
  export function registerContextTool(
@@ -67,8 +67,15 @@ export function registerContextTool(
67
67
  const primary = result.primary_result
68
68
  sections.push(`## ${primary.title}\n${primary.content}`)
69
69
 
70
- if (result.observations?.length > 0) {
71
- const obs = result.observations
70
+ // The note body already contains its observations as
71
+ // "- [category] text" lines, so listing them again doubled the
72
+ // output. Only observations missing from the shown body (for
73
+ // example when the server truncates a long note) are listed.
74
+ const unseen = (result.observations ?? []).filter(
75
+ (o) => !primary.content?.includes(`[${o.category}] ${o.content}`),
76
+ )
77
+ if (unseen.length > 0) {
78
+ const obs = unseen
72
79
  .map((o) => `- [${o.category}] ${o.content}`)
73
80
  .join("\n")
74
81
  sections.push(`### Observations\n${obs}`)
@@ -78,7 +85,9 @@ export function registerContextTool(
78
85
  const rels = result.related_results
79
86
  .map((r) => {
80
87
  if (r.type === "relation") {
81
- return `- ${r.relation_type} → **${r.to_entity}**`
88
+ return r.to_entity
89
+ ? `- ${r.relation_type} → **${r.to_entity}**`
90
+ : `- ${r.relation_type} → **${r.to_name ?? "unknown"}** (no note yet)`
82
91
  }
83
92
  const label = r.relation_type
84
93
  ? `${r.relation_type} → **${r.title}**`
@@ -111,7 +120,7 @@ export function registerContextTool(
111
120
  content: [
112
121
  {
113
122
  type: "text" as const,
114
- text: `Failed to build context for "${params.url}". Check logs for details.`,
123
+ text: `Failed to build context for "${params.url}": ${describeToolError(err)}`,
115
124
  },
116
125
  ],
117
126
  details: {
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import { type BmClient, describeNoteError } from "../bm-client.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
6
6
  export function registerDeleteTool(
@@ -55,7 +55,7 @@ export function registerDeleteTool(
55
55
  content: [
56
56
  {
57
57
  type: "text" as const,
58
- text: `Failed to delete "${params.identifier}". It may not exist.`,
58
+ text: `Failed to delete "${params.identifier}": ${describeNoteError(err)}`,
59
59
  },
60
60
  ],
61
61
  details: {
@@ -1,6 +1,10 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient } from "../bm-client.ts"
3
+ import {
4
+ type BmClient,
5
+ describeNoteError,
6
+ type EditOperation,
7
+ } from "../bm-client.ts"
4
8
  import { log } from "../logger.ts"
5
9
 
6
10
  export function registerEditTool(
@@ -13,8 +17,11 @@ export function registerEditTool(
13
17
  label: "Edit Note",
14
18
  description:
15
19
  "Incrementally edit an existing note in the Basic Memory knowledge graph. " +
16
- "Supports append, prepend, find/replace, and section replacement " +
17
- "without rewriting the entire note. The result returns the note's " +
20
+ "Supports append, prepend, find/replace, replacing a section, and inserting " +
21
+ "next to a section heading, without rewriting the entire note. To add to a " +
22
+ "specific section (for example a new observation), use insert_after_section " +
23
+ "with that heading; append always adds to the very end of the note. " +
24
+ "The result returns the note's " +
18
25
  "permalink and file_path: cite those exact values when confirming " +
19
26
  "the edit — never report a save that no tool call performed.",
20
27
  parameters: Type.Object({
@@ -27,11 +34,15 @@ export function registerEditTool(
27
34
  Type.Literal("prepend"),
28
35
  Type.Literal("find_replace"),
29
36
  Type.Literal("replace_section"),
37
+ Type.Literal("insert_before_section"),
38
+ Type.Literal("insert_after_section"),
30
39
  ],
31
40
  {
32
41
  description:
33
- "Edit operation: append (add to end), prepend (add to start), " +
34
- "find_replace (replace matching text), replace_section (replace a heading section)",
42
+ "Edit operation: append (add to end of note), prepend (add to start), " +
43
+ "find_replace (replace matching text), replace_section (replace a heading section), " +
44
+ "insert_after_section (add directly below a heading, at the top of its section), " +
45
+ "insert_before_section (add directly above a heading, at the end of the section before it)",
35
46
  },
36
47
  ),
37
48
  content: Type.String({
@@ -45,7 +56,8 @@ export function registerEditTool(
45
56
  section: Type.Optional(
46
57
  Type.String({
47
58
  description:
48
- "Section heading to replace (required for replace_section)",
59
+ "Section heading, e.g. 'Observations' (required for replace_section, " +
60
+ "insert_before_section and insert_after_section; not allowed with other operations)",
49
61
  }),
50
62
  ),
51
63
  expected_replacements: Type.Optional(
@@ -74,7 +86,7 @@ export function registerEditTool(
74
86
  _toolCallId: string,
75
87
  params: {
76
88
  identifier: string
77
- operation: "append" | "prepend" | "find_replace" | "replace_section"
89
+ operation: EditOperation
78
90
  content: string
79
91
  find_text?: string
80
92
  section?: string
@@ -103,7 +115,7 @@ export function registerEditTool(
103
115
  content: [
104
116
  {
105
117
  type: "text" as const,
106
- text: `Note updated: ${note.title} (${note.permalink})`,
118
+ text: `Note updated: ${note.title} (${note.permalink})\nFile: ${note.file_path}`,
107
119
  },
108
120
  ],
109
121
  details: {
@@ -116,13 +128,14 @@ export function registerEditTool(
116
128
  }
117
129
  } catch (err) {
118
130
  log.error("edit_note failed", err)
131
+ // Trigger: any edit failure, including invalid arguments and
132
+ // checksum conflicts, not only a missing note.
133
+ // Why: a blanket "may not exist" sent agents hunting for the wrong
134
+ // problem; the server's message says what to change.
135
+ // Outcome: the agent sees the server's reason verbatim.
136
+ const text = `Failed to edit note "${params.identifier}": ${describeNoteError(err)}`
119
137
  return {
120
- content: [
121
- {
122
- type: "text" as const,
123
- text: `Failed to edit note "${params.identifier}". It may not exist.`,
124
- },
125
- ],
138
+ content: [{ type: "text" as const, text }],
126
139
  details: {
127
140
  identifier: params.identifier,
128
141
  operation: params.operation,
@@ -1,6 +1,10 @@
1
1
  import { Type } from "@sinclair/typebox"
2
2
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
3
- import type { BmClient, ProjectListResult } from "../bm-client.ts"
3
+ import {
4
+ type BmClient,
5
+ describeToolError,
6
+ type ProjectListResult,
7
+ } from "../bm-client.ts"
4
8
  import { log } from "../logger.ts"
5
9
 
6
10
  function normalizeProject(project: ProjectListResult) {
@@ -102,7 +106,7 @@ export function registerProjectListTool(
102
106
  content: [
103
107
  {
104
108
  type: "text" as const,
105
- text: "Failed to list Basic Memory projects. Is Basic Memory running? Check logs for details.",
109
+ text: `Failed to list Basic Memory projects: ${describeToolError(err)}`,
106
110
  },
107
111
  ],
108
112
  details: {