@clawmem-ai/clawmem 0.1.17 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -156,6 +156,7 @@ Full config with all options:
156
156
  - Summary or memory-capture failures do not block finalization; the conversation issue still closes, and the mirrored transcript remains the durable source of truth for manual follow-up.
157
157
  - Memory search and auto-recall only return open `type:memory` issues. Closed memory issues are treated as stale.
158
158
  - ClawMem automatically injects a small set of relevant memories before each turn using the agent's default repo and the backend recall API. Auto-recall is best-effort and quietly skips injection when backend recall is unavailable.
159
+ - Always-on ClawMem prompt guidance uses the dedicated memory prompt-registration API on OpenClaw `2026.3.22+`. On `2026.3.7` through `2026.3.21`, ClawMem falls back to `before_prompt_build` `prependSystemContext`. Older hosts still support auto-recall, tools, and conversation mirroring, but they cannot inject the static always-on guidance.
159
160
  - `memory_recall` uses the backend `/api/v3/search/issues` endpoint scoped to the current repo plus `label:"type:memory"`. When backend recall is unavailable, use `memory_list` or `memory_get` to inspect memories explicitly.
160
161
  - Automatic durable capture happens when the session resets or ends. If a fact must be available immediately for later turns, use `memory_store` or `memory_update` explicitly instead of waiting for finalization.
161
162
  - The plugin exposes `memory_repos`, `memory_repo_create`, `memory_list`, `memory_get`, `memory_labels`, `memory_recall`, `memory_store`, `memory_update`, and `memory_forget` for mid-session use.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "clawmem",
3
3
  "name": "ClawMem",
4
- "version": "0.1.17",
4
+ "version": "0.1.18",
5
5
  "description": "Repo-backed long-term memory plugin for OpenClaw with auto recall, durable memory capture, and conversation mirroring.",
6
6
  "kind": "memory",
7
7
  "skills": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawmem-ai/clawmem",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Repo-backed long-term memory plugin for OpenClaw with auto recall, durable memory capture, and conversation mirroring.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -108,22 +108,34 @@ function testBuildClawMemPromptSection(): void {
108
108
  function createFakePluginApi(options?: {
109
109
  slot?: string;
110
110
  exposeCapability?: boolean;
111
+ exposePromptSection?: boolean;
112
+ runtimeVersion?: string;
111
113
  }) {
112
114
  let registeredCapability: { promptBuilder?: typeof buildClawMemPromptSection } | undefined;
113
115
  let registeredPromptSection: typeof buildClawMemPromptSection | undefined;
116
+ const handlers = new Map<string, Array<(...args: any[]) => unknown>>();
117
+ const warnings: string[] = [];
118
+ const infos: string[] = [];
114
119
  const api = {
115
120
  id: "clawmem",
116
121
  name: "ClawMem",
117
122
  source: "test",
118
123
  registrationMode: "test",
119
124
  config: {},
120
- pluginConfig: {},
125
+ pluginConfig: {
126
+ agents: {
127
+ main: {
128
+ token: "test-token",
129
+ defaultRepo: "acme/memory",
130
+ },
131
+ },
132
+ },
121
133
  logger: {
122
- info: () => {},
123
- warn: () => {},
134
+ info: (message: string) => { infos.push(message); },
135
+ warn: (message: string) => { warnings.push(message); },
124
136
  },
125
137
  runtime: {
126
- version: "2026.4.9",
138
+ version: options?.runtimeVersion ?? "2026.4.9",
127
139
  config: {
128
140
  loadConfig: () => ({
129
141
  plugins: {
@@ -138,7 +150,11 @@ function createFakePluginApi(options?: {
138
150
  },
139
151
  subagent: {},
140
152
  },
141
- on: () => {},
153
+ on: (event: string, handler: (...args: any[]) => unknown) => {
154
+ const current = handlers.get(event) ?? [];
155
+ current.push(handler);
156
+ handlers.set(event, current);
157
+ },
142
158
  registerTool: () => {},
143
159
  registerService: () => {},
144
160
  ...(options?.exposeCapability === false
@@ -148,15 +164,22 @@ function createFakePluginApi(options?: {
148
164
  registeredCapability = capability;
149
165
  },
150
166
  }),
151
- registerMemoryPromptSection: (builder: typeof buildClawMemPromptSection) => {
152
- registeredPromptSection = builder;
153
- },
167
+ ...(options?.exposePromptSection === false
168
+ ? {}
169
+ : {
170
+ registerMemoryPromptSection: (builder: typeof buildClawMemPromptSection) => {
171
+ registeredPromptSection = builder;
172
+ },
173
+ }),
154
174
  };
155
175
 
156
176
  return {
157
177
  api,
158
178
  getRegisteredCapability: () => registeredCapability,
159
179
  getRegisteredPromptSection: () => registeredPromptSection,
180
+ getWarnings: () => warnings,
181
+ getInfos: () => infos,
182
+ getHandler: (event: string) => handlers.get(event)?.[0],
160
183
  };
161
184
  }
162
185
 
@@ -181,6 +204,50 @@ function testFallsBackToLegacyMemoryPromptSectionRegistration(): void {
181
204
  assert(prompt.includes("## ClawMem"), "expected the fallback builder to emit ClawMem guidance");
182
205
  }
183
206
 
207
+ function testOlderHostWithoutPromptRegistrationDoesNotWarn(): void {
208
+ const fake = createFakePluginApi({
209
+ exposeCapability: false,
210
+ exposePromptSection: false,
211
+ runtimeVersion: "2026.3.13",
212
+ });
213
+ createClawMemPlugin(fake.api as never);
214
+
215
+ assert(fake.getWarnings().length === 0, "expected older hosts without prompt registration to avoid warnings");
216
+ assert(
217
+ fake.getInfos().some((message) => message.includes("falling back to before_prompt_build prependSystemContext")),
218
+ "expected older hosts to log an informational compatibility note",
219
+ );
220
+ }
221
+
222
+ function testModernHostWithoutPromptRegistrationWarns(): void {
223
+ const fake = createFakePluginApi({
224
+ exposeCapability: false,
225
+ exposePromptSection: false,
226
+ runtimeVersion: "2026.3.22",
227
+ });
228
+ createClawMemPlugin(fake.api as never);
229
+
230
+ assert(
231
+ fake.getWarnings().some((message) => message.includes("falling back to before_prompt_build prependSystemContext")),
232
+ "expected warning when a new-enough host is missing prompt registration",
233
+ );
234
+ }
235
+
236
+ async function testOlderModernHostInjectsPromptGuidanceViaPrependSystemContext(): Promise<void> {
237
+ const fake = createFakePluginApi({
238
+ exposeCapability: false,
239
+ exposePromptSection: false,
240
+ runtimeVersion: "2026.3.13",
241
+ });
242
+ createClawMemPlugin(fake.api as never);
243
+
244
+ const handler = fake.getHandler("before_prompt_build");
245
+ assert(typeof handler === "function", "expected before_prompt_build handler to be registered for modern hosts");
246
+ const result = await handler?.({ prompt: "hi" }, { agentId: "main" }) as { prependContext?: string; prependSystemContext?: string } | void;
247
+ assert(Boolean(result && result.prependSystemContext?.includes("## ClawMem")), "expected static ClawMem guidance to use prependSystemContext fallback");
248
+ assert(!result || !result.prependContext, "expected no dynamic recall context when the prompt is too short for auto-recall");
249
+ }
250
+
184
251
  function testSkipsAlwaysOnPromptWhenClawMemIsNotSelectedMemoryPlugin(): void {
185
252
  const fake = createFakePluginApi({ slot: "other-memory" });
186
253
  createClawMemPlugin(fake.api as never);
@@ -262,6 +329,9 @@ testResolvePromptHookModeLegacy();
262
329
  testResolvePromptHookModeLegacyForUnknownVersion();
263
330
  testRegistersAlwaysOnMemoryPromptCapability();
264
331
  testFallsBackToLegacyMemoryPromptSectionRegistration();
332
+ testOlderHostWithoutPromptRegistrationDoesNotWarn();
333
+ testModernHostWithoutPromptRegistrationWarns();
265
334
  testSkipsAlwaysOnPromptWhenClawMemIsNotSelectedMemoryPlugin();
335
+ await testOlderModernHostInjectsPromptGuidanceViaPrependSystemContext();
266
336
 
267
337
  console.log("service tests passed");
package/src/service.ts CHANGED
@@ -19,8 +19,21 @@ type CollaborationPermission = "read" | "write" | "admin";
19
19
  type CollaborationTeamRole = "member" | "maintainer";
20
20
  type MemoryPromptBuilder = NonNullable<MemoryPluginCapability["promptBuilder"]>;
21
21
  type MemoryPromptBuilderParams = Parameters<MemoryPromptBuilder>[0];
22
+ type PromptBuildInjection = { prependContext?: string; prependSystemContext?: string };
22
23
 
23
24
  const MODERN_PROMPT_HOOK_MIN_HOST_VERSION = "2026.3.7";
25
+ const MEMORY_PROMPT_REGISTRATION_MIN_HOST_VERSION = "2026.3.22";
26
+ const CLAWMEM_PROMPT_GUIDANCE_TOOL_NAMES = [
27
+ "memory_repos",
28
+ "memory_repo_create",
29
+ "memory_list",
30
+ "memory_labels",
31
+ "memory_recall",
32
+ "memory_get",
33
+ "memory_store",
34
+ "memory_update",
35
+ "memory_forget",
36
+ ] as const;
24
37
  type PromptHookMode = "modern" | "legacy";
25
38
 
26
39
  class ClawMemService {
@@ -34,6 +47,7 @@ class ClawMemService {
34
47
  private unsubTranscript?: () => void;
35
48
  private loadPromise: Promise<void> | null = null;
36
49
  private readonly configPromises = new Map<string, Promise<boolean>>();
50
+ private injectPromptGuidanceViaSystemContext = false;
37
51
 
38
52
  constructor(private readonly api: OpenClawPluginApi) {
39
53
  this.config = resolvePluginConfig(api);
@@ -41,7 +55,7 @@ class ClawMemService {
41
55
 
42
56
  register(): void {
43
57
  const promptHookMode = resolvePromptHookMode(this.api);
44
- this.registerMemoryPromptGuidance();
58
+ this.registerMemoryPromptGuidance(promptHookMode);
45
59
  if (promptHookMode === "modern") {
46
60
  this.api.on("before_prompt_build", async (ev, ctx) => this.handleBeforePromptBuild(ev, ctx.agentId));
47
61
  } else {
@@ -97,7 +111,7 @@ class ClawMemService {
97
111
  });
98
112
  }
99
113
 
100
- private registerMemoryPromptGuidance(): void {
114
+ private registerMemoryPromptGuidance(promptHookMode: PromptHookMode): void {
101
115
  if (!this.isSelectedMemoryPlugin()) return;
102
116
 
103
117
  const api = this.api as OpenClawPluginApi & {
@@ -115,7 +129,37 @@ class ClawMemService {
115
129
  return;
116
130
  }
117
131
 
118
- this.api.logger.warn?.("clawmem: host does not expose memory prompt registration; always-on prompt guidance is disabled");
132
+ const hostVersion = resolveOpenClawHostVersion(this.api);
133
+ const comparison = hostVersion ? compareOpenClawVersions(hostVersion, MEMORY_PROMPT_REGISTRATION_MIN_HOST_VERSION) : null;
134
+ if (promptHookMode === "modern") {
135
+ this.injectPromptGuidanceViaSystemContext = true;
136
+ if (comparison !== null && comparison < 0) {
137
+ this.api.logger.info?.(
138
+ `clawmem: OpenClaw ${hostVersion} predates memory prompt registration (requires ${MEMORY_PROMPT_REGISTRATION_MIN_HOST_VERSION}+); falling back to before_prompt_build prependSystemContext for always-on prompt guidance`,
139
+ );
140
+ return;
141
+ }
142
+
143
+ this.api.logger.warn?.(
144
+ hostVersion
145
+ ? `clawmem: OpenClaw ${hostVersion} does not expose memory prompt registration; falling back to before_prompt_build prependSystemContext for always-on prompt guidance`
146
+ : "clawmem: host does not expose memory prompt registration; falling back to before_prompt_build prependSystemContext for always-on prompt guidance",
147
+ );
148
+ return;
149
+ }
150
+
151
+ if (comparison !== null && comparison < 0) {
152
+ this.api.logger.info?.(
153
+ `clawmem: OpenClaw ${hostVersion} predates memory prompt registration and prompt-level system-context fallback; always-on prompt guidance is unavailable on this host`,
154
+ );
155
+ return;
156
+ }
157
+
158
+ this.api.logger.warn?.(
159
+ hostVersion
160
+ ? `clawmem: OpenClaw ${hostVersion} does not expose memory prompt registration; always-on prompt guidance is disabled`
161
+ : "clawmem: host does not expose memory prompt registration; always-on prompt guidance is disabled",
162
+ );
119
163
  }
120
164
 
121
165
  private isSelectedMemoryPlugin(): boolean {
@@ -1766,12 +1810,17 @@ class ClawMemService {
1766
1810
  });
1767
1811
  }
1768
1812
 
1769
- private async handleBeforePromptBuild(event: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
1813
+ private async handleBeforePromptBuild(event: unknown, agentId?: string): Promise<PromptBuildInjection | void> {
1770
1814
  const context = await this.collectAutoRecallContext(event, agentId);
1815
+ const systemContext = this.injectPromptGuidanceViaSystemContext ? buildFallbackPromptGuidanceText(event) : undefined;
1771
1816
  // Auto-recall is per-turn dynamic context, so keep it out of the system prompt.
1772
1817
  // OpenClaw documents dynamic context on `prependContext`: https://github.com/maweibin/openclaw/blob/d9a2869ad69db9449336a2e2846bd9de0e647ac6/docs/concepts/agent-loop.md?plain=1#L85
1773
1818
  // Changing the system prompt can defeat provider prefix caching.
1774
- return context ? { prependContext: context } : undefined;
1819
+ if (!context && !systemContext) return undefined;
1820
+ return {
1821
+ ...(systemContext ? { prependSystemContext: systemContext } : {}),
1822
+ ...(context ? { prependContext: context } : {}),
1823
+ };
1775
1824
  }
1776
1825
 
1777
1826
  private async handleBeforeAgentStart(event: unknown, agentId?: string): Promise<{ prependContext: string } | void> {
@@ -2389,6 +2438,14 @@ export function buildClawMemPromptSection(params: MemoryPromptBuilderParams): st
2389
2438
  return lines;
2390
2439
  }
2391
2440
 
2441
+ function buildFallbackPromptGuidanceText(event: unknown): string | undefined {
2442
+ const record = asRecord(event);
2443
+ const availableTools = resolvePromptGuidanceAvailableTools(record.availableTools);
2444
+ const citationsMode = typeof record.citationsMode === "string" ? record.citationsMode.trim() || undefined : undefined;
2445
+ const text = buildClawMemPromptSection({ availableTools, ...(citationsMode ? { citationsMode } : {}) }).join("\n").trim();
2446
+ return text || undefined;
2447
+ }
2448
+
2392
2449
  export function extractPromptTextForRecall(event: unknown): string | undefined {
2393
2450
  const direct = normalizePromptText(event);
2394
2451
  if (direct) return direct;
@@ -2416,6 +2473,25 @@ function joinNaturalLanguageList(items: string[]): string {
2416
2473
  return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
2417
2474
  }
2418
2475
 
2476
+ function resolvePromptGuidanceAvailableTools(value: unknown): Set<string> {
2477
+ const names = collectToolNames(value);
2478
+ return names.size > 0 ? names : new Set(CLAWMEM_PROMPT_GUIDANCE_TOOL_NAMES);
2479
+ }
2480
+
2481
+ function collectToolNames(value: unknown): Set<string> {
2482
+ const names = new Set<string>();
2483
+ const values = value instanceof Set ? [...value] : Array.isArray(value) ? value : [];
2484
+ for (const entry of values) {
2485
+ if (typeof entry === "string" && entry.trim()) {
2486
+ names.add(entry.trim());
2487
+ continue;
2488
+ }
2489
+ const record = asRecord(entry);
2490
+ if (typeof record.name === "string" && record.name.trim()) names.add(record.name.trim());
2491
+ }
2492
+ return names;
2493
+ }
2494
+
2419
2495
  function extractPromptTextFromMessages(value: unknown): string | undefined {
2420
2496
  if (!Array.isArray(value)) return undefined;
2421
2497
  let fallback: string | undefined;
package/tsconfig.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "extends": "../openclaw/tsconfig.json",
3
3
  "compilerOptions": {
4
+ "outDir": "./dist",
4
5
  "baseUrl": ".",
5
6
  "paths": {
6
7
  "openclaw/plugin-sdk": ["../openclaw/src/plugin-sdk/index.ts"],