@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
package/commands/cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
2
- import type { BmClient } from "../bm-client.ts"
2
+ import { type BmClient, searchResultText } from "../bm-client.ts"
3
3
  import type { BasicMemoryConfig } from "../config.ts"
4
4
  import { log } from "../logger.ts"
5
5
 
@@ -31,13 +31,12 @@ export function registerCli(
31
31
  }
32
32
 
33
33
  for (const r of results) {
34
- const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : ""
34
+ const score = r.score ? ` (score ${r.score.toFixed(2)})` : ""
35
35
  console.log(`- ${r.title}${score}`)
36
- if (r.content) {
36
+ const text = searchResultText(r)
37
+ if (text) {
37
38
  const preview =
38
- r.content.length > 100
39
- ? `${r.content.slice(0, 100)}...`
40
- : r.content
39
+ text.length > 100 ? `${text.slice(0, 100)}...` : text
41
40
  console.log(` ${preview}`)
42
41
  }
43
42
  }
package/commands/slash.ts CHANGED
@@ -2,7 +2,7 @@ import { execSync } from "node:child_process"
2
2
  import { dirname, resolve } from "node:path"
3
3
  import { fileURLToPath } from "node:url"
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"
5
- import type { BmClient } from "../bm-client.ts"
5
+ import { type BmClient, describeToolError } from "../bm-client.ts"
6
6
  import { log } from "../logger.ts"
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -62,7 +62,7 @@ export function registerCommands(
62
62
  } catch (err) {
63
63
  log.error("/remember failed", err)
64
64
  return {
65
- text: "Failed to save memory. Is Basic Memory running?",
65
+ text: `Failed to save memory: ${describeToolError(err)}`,
66
66
  }
67
67
  }
68
68
  },
@@ -89,7 +89,7 @@ export function registerCommands(
89
89
  }
90
90
 
91
91
  const lines = results.map((r, i) => {
92
- const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : ""
92
+ const score = r.score ? ` (score ${r.score.toFixed(2)})` : ""
93
93
  return `${i + 1}. ${r.title}${score}`
94
94
  })
95
95
 
@@ -99,7 +99,7 @@ export function registerCommands(
99
99
  } catch (err) {
100
100
  log.error("/recall failed", err)
101
101
  return {
102
- text: "Failed to search. Is Basic Memory running?",
102
+ text: `Failed to search: ${describeToolError(err)}`,
103
103
  }
104
104
  }
105
105
  },
@@ -1,25 +1,30 @@
1
- import type {
2
- AssembleResult,
3
- BootstrapResult,
4
- CompactResult,
5
- ContextEngine,
6
- SubagentSpawnPreparation,
7
- } from "openclaw/plugin-sdk"
8
1
  import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core"
9
2
  import type { BmClient } from "../bm-client.ts"
10
3
  import type { BasicMemoryConfig } from "../config.ts"
11
4
  import { selectCaptureTurn } from "../hooks/capture.ts"
12
5
  import { loadRecallState } from "../hooks/recall.ts"
13
6
  import { log } from "../logger.ts"
7
+ import type {
8
+ AssembleResult,
9
+ BootstrapResult,
10
+ CommitTurnResult,
11
+ CompactResult,
12
+ ContextEngine,
13
+ SubagentSpawnPreparation,
14
+ } from "./types.ts"
14
15
 
15
16
  export const MAX_ASSEMBLE_RECALL_CHARS = 1200
16
17
  const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]"
17
18
  const SUBAGENT_HANDOFF_FOLDER = "agent/subagents"
18
19
  const MAX_SUBAGENT_RECALL_CHARS = 800
20
+ // OpenClaw's reply for "nothing to say", used by heartbeat polls and silent
21
+ // maintenance turns.
22
+ const SILENT_REPLY_TOKEN = "NO_REPLY"
19
23
 
20
24
  type BootstrapParams = Parameters<NonNullable<ContextEngine["bootstrap"]>>[0]
21
25
  type AssembleParams = Parameters<ContextEngine["assemble"]>[0]
22
26
  type AfterTurnParams = Parameters<NonNullable<ContextEngine["afterTurn"]>>[0]
27
+ type CommitTurnParams = Parameters<NonNullable<ContextEngine["commitTurn"]>>[0]
23
28
  type CompactParams = Parameters<ContextEngine["compact"]>[0]
24
29
  type PrepareSubagentSpawnParams = Parameters<
25
30
  NonNullable<ContextEngine["prepareSubagentSpawn"]>
@@ -53,6 +58,19 @@ function boundRecallContext(context: string): string {
53
58
  return `${trimmed}${TRUNCATED_RECALL_SUFFIX}`
54
59
  }
55
60
 
61
+ /**
62
+ * Heartbeat polls and silent replies are host housekeeping, not
63
+ * conversation, so they are acknowledged without being recorded.
64
+ */
65
+ function isHousekeepingTurn(
66
+ isHeartbeat: boolean | undefined,
67
+ turn: { assistantText: string },
68
+ ): boolean {
69
+ return (
70
+ isHeartbeat === true || turn.assistantText.trim() === SILENT_REPLY_TOKEN
71
+ )
72
+ }
73
+
56
74
  function slugifySessionKey(sessionKey: string): string {
57
75
  return sessionKey
58
76
  .toLowerCase()
@@ -98,7 +116,7 @@ function buildSubagentCompletionUpdate(params: {
98
116
  }): string {
99
117
  const statusLine =
100
118
  params.reason === "completed"
101
- ? "Child run completed. Durable conversation capture continues through the normal afterTurn path."
119
+ ? "Child run completed. Durable conversation capture continues through the normal turn commit path."
102
120
  : `Child run ended with reason: ${params.reason}.`
103
121
 
104
122
  return [
@@ -118,6 +136,18 @@ export class BasicMemoryContextEngine implements ContextEngine {
118
136
  name: "Basic Memory Context Engine",
119
137
  version: "0.1.5",
120
138
  ownsCompaction: false,
139
+ // OpenClaw 2026.10+ runs an engine for durable turns only when it
140
+ // declares both semantics; otherwise the turn degrades to "legacy" and
141
+ // nothing is captured (#1880).
142
+ // - The fence holds because assemble() passes the host's messages through
143
+ // unchanged and the engine keeps no transcript of its own, so it never
144
+ // reads past the admitted user entry.
145
+ // - Advancement is idempotent because commitTurn() records the
146
+ // advancement key in the conversation note and skips keys it has seen.
147
+ transcriptSemantics: {
148
+ currentTurnFence: "before-current-turn-entry-v1",
149
+ turnAdvancementIdempotency: "atomic-idempotent-v1",
150
+ },
121
151
  } as const
122
152
 
123
153
  private readonly sessionState = new Map<string, SessionMemoryState>()
@@ -171,6 +201,41 @@ export class BasicMemoryContextEngine implements ContextEngine {
171
201
  }
172
202
  }
173
203
 
204
+ /**
205
+ * Capture one accepted turn on hosts with durable turn advancement.
206
+ *
207
+ * The host calls this instead of afterTurn() for admitted turns, and may
208
+ * call it again with the same advancement key after a crash. Errors
209
+ * propagate so the host keeps the turn queued and retries it.
210
+ */
211
+ async commitTurn(params: CommitTurnParams): Promise<CommitTurnResult> {
212
+ if (!this.cfg.autoCapture) return { status: "committed" }
213
+
214
+ // messages spans exactly the admitted user entry through the terminal
215
+ // entry, so the whole payload is the current turn.
216
+ const turn = selectCaptureTurn(params.messages, this.cfg.captureMinChars)
217
+ if (!turn || isHousekeepingTurn(params.isHeartbeat, turn)) {
218
+ return { status: "committed" }
219
+ }
220
+
221
+ log.debug(
222
+ `context-engine commitTurn: session=${params.sessionId} user=${turn.userText.length} assistant=${turn.assistantText.length}`,
223
+ )
224
+
225
+ const status = await this.client.indexConversation(
226
+ turn.userText,
227
+ turn.assistantText,
228
+ { turnKey: params.advancementKey },
229
+ )
230
+ return { status }
231
+ }
232
+
233
+ /**
234
+ * Capture fallback for hosts that predate commitTurn().
235
+ *
236
+ * Hosts that route a turn through commitTurn() skip afterTurn() for that
237
+ * turn, so the two paths never record the same exchange.
238
+ */
174
239
  async afterTurn(params: AfterTurnParams): Promise<void> {
175
240
  if (!this.cfg.autoCapture) return
176
241
 
@@ -179,7 +244,7 @@ export class BasicMemoryContextEngine implements ContextEngine {
179
244
  selectCaptureTurn(newMessages, this.cfg.captureMinChars) ??
180
245
  selectCaptureTurn(params.messages, this.cfg.captureMinChars)
181
246
 
182
- if (!turn) return
247
+ if (!turn || isHousekeepingTurn(params.isHeartbeat, turn)) return
183
248
 
184
249
  log.debug(
185
250
  `context-engine afterTurn: session=${params.sessionId} user=${turn.userText.length} assistant=${turn.assistantText.length}`,
@@ -0,0 +1,21 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"
2
+
3
+ // OpenClaw 2026.10 dropped the bare "openclaw/plugin-sdk" entry point that used
4
+ // to export ContextEngine and its result types. Derive them from the public
5
+ // registerContextEngine signature so we depend only on plugin-sdk/core.
6
+ type ContextEngineFactory = Parameters<
7
+ OpenClawPluginApi["registerContextEngine"]
8
+ >[1]
9
+
10
+ export type ContextEngine = Awaited<ReturnType<ContextEngineFactory>>
11
+ export type AssembleResult = Awaited<ReturnType<ContextEngine["assemble"]>>
12
+ export type BootstrapResult = Awaited<
13
+ ReturnType<NonNullable<ContextEngine["bootstrap"]>>
14
+ >
15
+ export type CompactResult = Awaited<ReturnType<ContextEngine["compact"]>>
16
+ export type SubagentSpawnPreparation = NonNullable<
17
+ Awaited<ReturnType<NonNullable<ContextEngine["prepareSubagentSpawn"]>>>
18
+ >
19
+ export type CommitTurnResult = Awaited<
20
+ ReturnType<NonNullable<ContextEngine["commitTurn"]>>
21
+ >
@@ -1,16 +1,17 @@
1
1
  export declare class NoteAlreadyExistsError extends Error {
2
2
  readonly permalink: string;
3
- constructor(title: string, permalink: string);
3
+ constructor(title: string, permalink: string, message?: string);
4
4
  }
5
5
  /**
6
- * write_note refused a conditional overwrite: the note is no longer the
6
+ * bm_write refused a conditional overwrite: the note is no longer the
7
7
  * revision named by expected_checksum. Nothing was written; currentChecksum is
8
- * the revision to re-read and retry against (null once no note owns the path).
8
+ * available revision when supplied by a server. A null value means unknown;
9
+ * 1.0 puts revision details in the refusal message, not a checksum field.
9
10
  */
10
11
  export declare class NoteRevisionConflictError extends Error {
11
12
  readonly permalink: string | null;
12
13
  readonly currentChecksum: string | null;
13
- constructor(title: string, permalink: string | null, currentChecksum: string | null);
14
+ constructor(title: string, permalink: string | null, currentChecksum: string | null, message?: string);
14
15
  }
15
16
  /** A BM MCP tool returned isError; toolMessage is the tool's own error text. */
16
17
  export declare class BmToolError extends Error {
@@ -24,7 +25,14 @@ export interface SearchResult {
24
25
  content: string;
25
26
  score?: number;
26
27
  file_path: string;
28
+ matched_chunk?: string | null;
27
29
  }
30
+ /**
31
+ * The text to preview for a search hit. `content` is the start of the note,
32
+ * so for a long note (a day of captured conversation) the matching part can
33
+ * sit far below any preview cut-off; the matched chunk is the part that hit.
34
+ */
35
+ export declare function searchResultText(result: SearchResult): string;
28
36
  export interface NoteResult {
29
37
  title: string;
30
38
  permalink: string;
@@ -38,7 +46,7 @@ export interface EditNoteResult {
38
46
  title: string;
39
47
  permalink: string;
40
48
  file_path: string;
41
- operation: "append" | "prepend" | "find_replace" | "replace_section";
49
+ operation: EditOperation;
42
50
  checksum?: string | null;
43
51
  }
44
52
  interface ReadNoteOptions {
@@ -55,6 +63,7 @@ interface WriteNoteOptions {
55
63
  /** Checksum of the revision read; overwrite only while it is still current. */
56
64
  expectedChecksum?: string;
57
65
  }
66
+ export type EditOperation = "append" | "prepend" | "find_replace" | "replace_section" | "insert_before_section" | "insert_after_section";
58
67
  interface EditNoteOptions {
59
68
  find_text?: string;
60
69
  section?: string;
@@ -74,7 +83,8 @@ export interface ContextResult {
74
83
  permalink: string;
75
84
  relation_type?: string;
76
85
  from_entity?: string;
77
- to_entity?: string;
86
+ to_entity?: string | null;
87
+ to_name?: string | null;
78
88
  }>;
79
89
  }>;
80
90
  }
@@ -102,10 +112,7 @@ export interface ProjectListResult {
102
112
  export interface WorkspaceResult {
103
113
  tenant_id: string;
104
114
  name: string;
105
- workspace_type: string;
106
- role: string;
107
- organization_id?: string | null;
108
- has_active_subscription: boolean;
115
+ workspace_type: string | null;
109
116
  }
110
117
  export interface SchemaValidationResult {
111
118
  entity_type: string | null;
@@ -156,21 +163,49 @@ export interface SchemaDiffResult {
156
163
  }>;
157
164
  cardinality_changes: string[];
158
165
  }
166
+ export declare function isNoteNotFoundError(err: unknown): boolean;
167
+ export type ConversationCaptureStatus = "committed" | "duplicate";
168
+ /**
169
+ * The server's own explanation of a failed tool call, for showing to an agent.
170
+ * A refusal payload becomes "CODE: message"; other tool text passes through.
171
+ */
172
+ export declare function describeToolError(err: unknown): string;
173
+ /** describeToolError, but saying plainly when the note does not exist. */
174
+ export declare function describeNoteError(err: unknown): string;
159
175
  export declare class BmClient {
160
176
  private bmPath;
161
177
  private project;
162
178
  private cwd?;
163
179
  private env?;
164
180
  private shouldRun;
181
+ private serviceRunning;
182
+ private startup;
183
+ private startupRetry;
184
+ private projectPath?;
185
+ private prepare?;
165
186
  private client;
166
187
  private transport;
167
188
  private connectPromise;
168
189
  private retryDelaysMs;
190
+ private conversationWrites;
169
191
  constructor(bmPath: string, project: string);
192
+ /**
193
+ * Connect to BM and create the plugin's project.
194
+ *
195
+ * Returns the startup promise; callers that must not block (the plugin
196
+ * service, which OpenClaw gives only a few seconds) can leave it running.
197
+ * Tool calls made meanwhile wait for it. If it fails, the next tool call
198
+ * runs it again, so a transient failure does not leave the plugin dead.
199
+ */
170
200
  start(options?: {
171
201
  cwd?: string;
172
202
  env?: Record<string, string>;
203
+ projectPath?: string;
204
+ prepare?: () => Promise<void>;
173
205
  }): Promise<void>;
206
+ private runStartup;
207
+ private waitForStartup;
208
+ isServiceRunning(): boolean;
174
209
  stop(): Promise<void>;
175
210
  private connectWithRetries;
176
211
  private ensureConnected;
@@ -179,9 +214,11 @@ export declare class BmClient {
179
214
  private disconnectCurrent;
180
215
  private callToolRaw;
181
216
  private callTool;
217
+ private callStructured;
182
218
  private routedProject;
183
219
  private routing;
184
220
  ensureProject(projectPath: string): Promise<void>;
221
+ private createProject;
185
222
  listWorkspaces(): Promise<WorkspaceResult[]>;
186
223
  listProjects(workspace?: string): Promise<ProjectListResult[]>;
187
224
  search(query?: string, limit?: number, project?: string, metadata?: {
@@ -195,7 +232,7 @@ export declare class BmClient {
195
232
  writeNote(title: string, content: string, folder: string, project?: string, overwrite?: boolean, options?: WriteNoteOptions): Promise<NoteResult>;
196
233
  buildContext(url: string, depth?: number, project?: string): Promise<ContextResult>;
197
234
  recentActivity(timeframe?: string, project?: string): Promise<RecentResult[]>;
198
- editNote(identifier: string, operation: "append" | "prepend" | "find_replace" | "replace_section", content: string, options?: EditNoteOptions, project?: string): Promise<EditNoteResult>;
235
+ editNote(identifier: string, operation: EditOperation, content: string, options?: EditNoteOptions, project?: string): Promise<EditNoteResult>;
199
236
  deleteNote(identifier: string, project?: string): Promise<{
200
237
  title: string;
201
238
  permalink: string;
@@ -205,7 +242,19 @@ export declare class BmClient {
205
242
  schemaValidate(noteType?: string, identifier?: string, project?: string): Promise<SchemaValidationResult>;
206
243
  schemaInfer(noteType: string, threshold?: number, project?: string): Promise<SchemaInferResult>;
207
244
  schemaDiff(noteType: string, project?: string): Promise<SchemaDiffResult>;
208
- indexConversation(userMessage: string, assistantResponse: string): Promise<void>;
245
+ /**
246
+ * Append one user/assistant exchange to today's conversation note.
247
+ *
248
+ * With a turnKey the capture is idempotent: the entry carries a marker
249
+ * derived from the key, and a key already present in today's or
250
+ * yesterday's note is reported as a duplicate instead of written again.
251
+ * Yesterday is checked because a host retry can cross midnight.
252
+ */
253
+ indexConversation(userMessage: string, assistantResponse: string, options?: {
254
+ turnKey?: string;
255
+ }): Promise<ConversationCaptureStatus>;
256
+ private writeConversationEntry;
257
+ private noteContains;
209
258
  getProject(): string;
210
259
  }
211
260
  export {};