Skip to content

Commit bb26975

Browse files
committed
fix(openclaw-agent-memory): wire memory-host hooks for auto-recall (#279)
Closes #279. The plugin registered seven OB1 tools but did not participate in the OpenClaw memory-host lifecycle, so agents only hit OB1 if they remembered to. As a result, recall-before / writeback- after happened only when the prompt or skill explicitly nudged it. Adds two additive memory hooks inside register(api): * registerMemoryPromptSupplement — auto-injects an OB1 discipline block into the system prompt every turn (recall-before / writeback-after, instruction-vs-evidence semantics, and the list of tools currently exposed to the agent). * registerMemoryCorpusSupplement — exposes OB1 as a searchable corpus so OpenClaw's native recall flow queries OB1 alongside whatever other corpora are active. Maps recall responses onto MemoryCorpusSearchResult and inspects single memories via MemoryCorpusGetResult, with provenanceLabel reflecting OB1's use_policy (instruction vs evidence). Both supplements are additive (per the SDK contract) so they coexist with the active exclusive memory plugin. Both no-op cleanly when OB1 is not configured (missing endpoint or workspaceId, missing access key, network failure) — never throws into the host. The hooks are called via (api as any).registerMemory* so the plugin stays compatible with older OpenClaw releases that may not have the methods on the typed surface yet; the typeof checks gate registration on availability. No new tooling, dependencies, or breaking changes. The seven existing tools are unchanged. dist/ is intentionally not committed — maintainers should run `npm run build` to regenerate it.
1 parent 151a8d1 commit bb26975

1 file changed

Lines changed: 143 additions & 0 deletions

File tree

  • integrations/openclaw-agent-memory/plugin/src

integrations/openclaw-agent-memory/plugin/src/index.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,148 @@ export default definePluginEntry({
158158
parameters: Type.Object({ request_id: Type.String() }),
159159
run: (client, input) => client.getRecallTrace(input.request_id),
160160
});
161+
162+
// ------------------------------------------------------------------
163+
// Memory-host hooks (fix for #279).
164+
//
165+
// The plugin registers tools above, but without participating in the
166+
// OpenClaw memory-host lifecycle, agents only invoke OB1 if they
167+
// remember to. We register two additive memory hooks so OB1 lands
168+
// in every turn automatically:
169+
//
170+
// 1. registerMemoryPromptSupplement — auto-injects an OB1 discipline
171+
// block into the system prompt (recall-before / writeback-after
172+
// reminder, instruction-vs-evidence semantics, tool list).
173+
// 2. registerMemoryCorpusSupplement — exposes OB1 as a searchable
174+
// corpus so OpenClaw's native recall flow queries OB1 alongside
175+
// whatever other corpora are active.
176+
//
177+
// Both are additive (per the SDK contract), coexist with the active
178+
// exclusive memory plugin, and no-op cleanly when OB1 isn't configured.
179+
// ------------------------------------------------------------------
180+
181+
const OB1_TOOL_NAMES = [
182+
"openbrain_recall",
183+
"openbrain_writeback",
184+
"openbrain_report_usage",
185+
"openbrain_inspect_memory",
186+
"openbrain_list_review_queue",
187+
"openbrain_review_memory",
188+
"openbrain_get_recall_trace",
189+
] as const;
190+
191+
function isConfigured(): boolean {
192+
const raw = ((api as any).pluginConfig || {}) as Record<string, unknown>;
193+
return typeof raw.endpoint === "string"
194+
&& raw.endpoint.length > 0
195+
&& typeof raw.workspaceId === "string"
196+
&& raw.workspaceId.length > 0;
197+
}
198+
199+
if (typeof (api as any).registerMemoryPromptSupplement === "function") {
200+
(api as any).registerMemoryPromptSupplement((params: { availableTools: Set<string> }) => {
201+
if (!isConfigured()) return [];
202+
const present = OB1_TOOL_NAMES.filter((t) => params.availableTools.has(t));
203+
if (present.length === 0) return [];
204+
return [
205+
"## OB1 Agent Memory",
206+
"Long-term governed memory is available via OB1. Use it as a discipline, not a fallback.",
207+
"",
208+
"Workflow:",
209+
"- Before meaningful work, call `openbrain_recall` with a task-scoped query.",
210+
"- Treat returned memories tagged `instruction` as binding rules; `evidence`-tagged ones as supporting context only.",
211+
"- After meaningful work, call `openbrain_writeback` with compact, provenance-labeled findings (decisions, lessons, constraints, outputs, failures).",
212+
"- After acting on recalled memories, call `openbrain_report_usage` with `request_id` and the IDs you used vs. ignored — closes the recall-quality loop.",
213+
"",
214+
`Available tools: ${present.map((t) => "`" + t + "`").join(", ")}.`,
215+
];
216+
});
217+
}
218+
219+
if (typeof (api as any).registerMemoryCorpusSupplement === "function") {
220+
(api as any).registerMemoryCorpusSupplement({
221+
async search(input: { query: string; maxResults?: number; agentSessionKey?: string }) {
222+
if (!isConfigured()) return [];
223+
let client: AgentMemoryClient;
224+
try {
225+
client = await clientFromApi(api);
226+
} catch {
227+
return [];
228+
}
229+
const limit = Math.min(Math.max(input.maxResults ?? 10, 1), 50);
230+
let response: any;
231+
try {
232+
response = await client.recall({
233+
query: input.query.slice(0, 2000),
234+
task_type: "general",
235+
limits: { max_items: limit, max_tokens: 4000 },
236+
scope: { project_only: false, include_unconfirmed: false, include_stale: false },
237+
});
238+
} catch {
239+
return [];
240+
}
241+
const memories: any[] = Array.isArray(response?.memories) ? response.memories : [];
242+
return memories.map((m, i) => {
243+
const policy = m?.use_policy ?? {};
244+
const provenance = policy?.can_use_as_instruction
245+
? "instruction"
246+
: policy?.can_use_as_evidence
247+
? "evidence"
248+
: undefined;
249+
return {
250+
corpus: "openbrain",
251+
path: `openbrain://memory/${m?.id ?? i}`,
252+
title: typeof m?.summary === "string" ? m.summary.slice(0, 80) : undefined,
253+
kind: "memory",
254+
score: typeof m?.score === "number" ? m.score : Math.max(0, 1 - i / Math.max(memories.length, 1)),
255+
snippet: String(m?.summary ?? m?.content ?? "").slice(0, 600),
256+
id: typeof m?.id === "string" ? m.id : undefined,
257+
provenanceLabel: provenance,
258+
source: "openbrain.agent_memory",
259+
sourceType: "openbrain.agent_memory",
260+
updatedAt: typeof m?.updated_at === "string" ? m.updated_at : undefined,
261+
};
262+
});
263+
},
264+
async get(input: { lookup: string }) {
265+
if (!isConfigured()) return null;
266+
const id = input.lookup.replace(/^openbrain:\/\/memory\//, "");
267+
if (!id) return null;
268+
let client: AgentMemoryClient;
269+
try {
270+
client = await clientFromApi(api);
271+
} catch {
272+
return null;
273+
}
274+
let memory: any;
275+
try {
276+
memory = await client.inspectMemory(id);
277+
} catch {
278+
return null;
279+
}
280+
if (!memory || typeof memory !== "object") return null;
281+
const policy = (memory as any)?.use_policy ?? {};
282+
const provenance = policy?.can_use_as_instruction
283+
? "instruction"
284+
: policy?.can_use_as_evidence
285+
? "evidence"
286+
: undefined;
287+
const content = String((memory as any)?.content ?? (memory as any)?.summary ?? "");
288+
return {
289+
corpus: "openbrain",
290+
path: input.lookup,
291+
title: typeof (memory as any)?.summary === "string" ? (memory as any).summary.slice(0, 80) : undefined,
292+
kind: "memory",
293+
content,
294+
fromLine: 1,
295+
lineCount: content.split("\n").length,
296+
id: typeof (memory as any)?.id === "string" ? (memory as any).id : id,
297+
provenanceLabel: provenance,
298+
sourceType: "openbrain.agent_memory",
299+
updatedAt: typeof (memory as any)?.updated_at === "string" ? (memory as any).updated_at : undefined,
300+
};
301+
},
302+
});
303+
}
161304
},
162305
});

0 commit comments

Comments
 (0)