@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 +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/service.test.ts +78 -8
- package/src/service.ts +81 -5
- package/tsconfig.json +1 -0
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.
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/service.test.ts
CHANGED
|
@@ -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
|
-
|
|
152
|
-
|
|
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
|
|
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<
|
|
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
|
-
|
|
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;
|