Skip to content

Commit 6acdff4

Browse files
Merge pull request #281 from MicScalise/fix-279-openclaw-memory-host-hooks
fix(openclaw-agent-memory): wire memory-host hooks for auto-recall (#279)
2 parents 6087b12 + bb26975 commit 6acdff4

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
@@ -272,5 +272,148 @@ export default definePluginEntry({
272272
parameters: Type.Object({ request_id: Type.String() }),
273273
run: (client, input) => client.getRecallTrace(input.request_id),
274274
});
275+
276+
// ------------------------------------------------------------------
277+
// Memory-host hooks (fix for #279).
278+
//
279+
// The plugin registers tools above, but without participating in the
280+
// OpenClaw memory-host lifecycle, agents only invoke OB1 if they
281+
// remember to. We register two additive memory hooks so OB1 lands
282+
// in every turn automatically:
283+
//
284+
// 1. registerMemoryPromptSupplement — auto-injects an OB1 discipline
285+
// block into the system prompt (recall-before / writeback-after
286+
// reminder, instruction-vs-evidence semantics, tool list).
287+
// 2. registerMemoryCorpusSupplement — exposes OB1 as a searchable
288+
// corpus so OpenClaw's native recall flow queries OB1 alongside
289+
// whatever other corpora are active.
290+
//
291+
// Both are additive (per the SDK contract), coexist with the active
292+
// exclusive memory plugin, and no-op cleanly when OB1 isn't configured.
293+
// ------------------------------------------------------------------
294+
295+
const OB1_TOOL_NAMES = [
296+
"openbrain_recall",
297+
"openbrain_writeback",
298+
"openbrain_report_usage",
299+
"openbrain_inspect_memory",
300+
"openbrain_list_review_queue",
301+
"openbrain_review_memory",
302+
"openbrain_get_recall_trace",
303+
] as const;
304+
305+
function isConfigured(): boolean {
306+
const raw = ((api as any).pluginConfig || {}) as Record<string, unknown>;
307+
return typeof raw.endpoint === "string"
308+
&& raw.endpoint.length > 0
309+
&& typeof raw.workspaceId === "string"
310+
&& raw.workspaceId.length > 0;
311+
}
312+
313+
if (typeof (api as any).registerMemoryPromptSupplement === "function") {
314+
(api as any).registerMemoryPromptSupplement((params: { availableTools: Set<string> }) => {
315+
if (!isConfigured()) return [];
316+
const present = OB1_TOOL_NAMES.filter((t) => params.availableTools.has(t));
317+
if (present.length === 0) return [];
318+
return [
319+
"## OB1 Agent Memory",
320+
"Long-term governed memory is available via OB1. Use it as a discipline, not a fallback.",
321+
"",
322+
"Workflow:",
323+
"- Before meaningful work, call `openbrain_recall` with a task-scoped query.",
324+
"- Treat returned memories tagged `instruction` as binding rules; `evidence`-tagged ones as supporting context only.",
325+
"- After meaningful work, call `openbrain_writeback` with compact, provenance-labeled findings (decisions, lessons, constraints, outputs, failures).",
326+
"- After acting on recalled memories, call `openbrain_report_usage` with `request_id` and the IDs you used vs. ignored — closes the recall-quality loop.",
327+
"",
328+
`Available tools: ${present.map((t) => "`" + t + "`").join(", ")}.`,
329+
];
330+
});
331+
}
332+
333+
if (typeof (api as any).registerMemoryCorpusSupplement === "function") {
334+
(api as any).registerMemoryCorpusSupplement({
335+
async search(input: { query: string; maxResults?: number; agentSessionKey?: string }) {
336+
if (!isConfigured()) return [];
337+
let client: AgentMemoryClient;
338+
try {
339+
client = await clientFromApi(api);
340+
} catch {
341+
return [];
342+
}
343+
const limit = Math.min(Math.max(input.maxResults ?? 10, 1), 50);
344+
let response: any;
345+
try {
346+
response = await client.recall({
347+
query: input.query.slice(0, 2000),
348+
task_type: "general",
349+
limits: { max_items: limit, max_tokens: 4000 },
350+
scope: { project_only: false, include_unconfirmed: false, include_stale: false },
351+
});
352+
} catch {
353+
return [];
354+
}
355+
const memories: any[] = Array.isArray(response?.memories) ? response.memories : [];
356+
return memories.map((m, i) => {
357+
const policy = m?.use_policy ?? {};
358+
const provenance = policy?.can_use_as_instruction
359+
? "instruction"
360+
: policy?.can_use_as_evidence
361+
? "evidence"
362+
: undefined;
363+
return {
364+
corpus: "openbrain",
365+
path: `openbrain://memory/${m?.id ?? i}`,
366+
title: typeof m?.summary === "string" ? m.summary.slice(0, 80) : undefined,
367+
kind: "memory",
368+
score: typeof m?.score === "number" ? m.score : Math.max(0, 1 - i / Math.max(memories.length, 1)),
369+
snippet: String(m?.summary ?? m?.content ?? "").slice(0, 600),
370+
id: typeof m?.id === "string" ? m.id : undefined,
371+
provenanceLabel: provenance,
372+
source: "openbrain.agent_memory",
373+
sourceType: "openbrain.agent_memory",
374+
updatedAt: typeof m?.updated_at === "string" ? m.updated_at : undefined,
375+
};
376+
});
377+
},
378+
async get(input: { lookup: string }) {
379+
if (!isConfigured()) return null;
380+
const id = input.lookup.replace(/^openbrain:\/\/memory\//, "");
381+
if (!id) return null;
382+
let client: AgentMemoryClient;
383+
try {
384+
client = await clientFromApi(api);
385+
} catch {
386+
return null;
387+
}
388+
let memory: any;
389+
try {
390+
memory = await client.inspectMemory(id);
391+
} catch {
392+
return null;
393+
}
394+
if (!memory || typeof memory !== "object") return null;
395+
const policy = (memory as any)?.use_policy ?? {};
396+
const provenance = policy?.can_use_as_instruction
397+
? "instruction"
398+
: policy?.can_use_as_evidence
399+
? "evidence"
400+
: undefined;
401+
const content = String((memory as any)?.content ?? (memory as any)?.summary ?? "");
402+
return {
403+
corpus: "openbrain",
404+
path: input.lookup,
405+
title: typeof (memory as any)?.summary === "string" ? (memory as any).summary.slice(0, 80) : undefined,
406+
kind: "memory",
407+
content,
408+
fromLine: 1,
409+
lineCount: content.split("\n").length,
410+
id: typeof (memory as any)?.id === "string" ? (memory as any).id : id,
411+
provenanceLabel: provenance,
412+
sourceType: "openbrain.agent_memory",
413+
updatedAt: typeof (memory as any)?.updated_at === "string" ? (memory as any).updated_at : undefined,
414+
};
415+
},
416+
});
417+
}
275418
},
276419
});

0 commit comments

Comments
 (0)