@persistio/openclaw-plugin 0.1.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.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # @persistio/openclaw-plugin
2
+
3
+ OpenClaw plugin for [Persistio](https://persistio.ai) — persistent semantic memory for AI agents.
4
+
5
+ Hooks into OpenClaw's `before_prompt_build` and `agent_end` events to automatically recall relevant memories into every prompt and ingest new conversation turns after each run. Exposes `memory_search`, `memory_add`, `memory_delete`, and `memory_list` as agent tools.
6
+
7
+ ## Requirements
8
+
9
+ - A running [Persistio](https://github.com/chriscoveyduck/persistio) instance (`api.persistio.ai` or self-hosted)
10
+ - OpenClaw `>=2026.3.24-beta.2`
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install -g @persistio/openclaw-plugin
16
+ ```
17
+
18
+ Then register it in your OpenClaw config:
19
+
20
+ ```json
21
+ {
22
+ "plugins": {
23
+ "entries": {
24
+ "persistio": {
25
+ "package": "@persistio/openclaw-plugin",
26
+ "config": {
27
+ "baseURL": "https://api.persistio.ai",
28
+ "apiKey": "your-vault-api-key"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Configuration
37
+
38
+ | Option | Type | Required | Default | Description |
39
+ |---|---|---|---|---|
40
+ | `baseURL` | string | ✅ | — | Base URL of your Persistio instance |
41
+ | `apiKey` | string | ✅ | — | Vault API key |
42
+ | `tokenBudget` | number | | `2000` | Max tokens to inject into the system prompt |
43
+ | `recallTopK` | number | | `10` | Number of memories to retrieve per recall |
44
+ | `recallTimeout` | number | | `5000` | HTTP timeout for recall requests (ms) |
45
+
46
+ ## Tools exposed
47
+
48
+ | Tool | Description |
49
+ |---|---|
50
+ | `memory_search` | Search memories by semantic query |
51
+ | `memory_add` | Manually store a fact |
52
+ | `memory_delete` | Delete a memory by ID |
53
+ | `memory_list` | List all memories in the vault |
54
+
55
+ ## License
56
+
57
+ MIT
@@ -0,0 +1,31 @@
1
+ export interface PersistioConfig {
2
+ baseURL: string;
3
+ apiKey: string;
4
+ tokenBudget: number;
5
+ recallTopK: number;
6
+ recallTimeout: number;
7
+ }
8
+ export interface PersistioMemory {
9
+ id: string;
10
+ data: string;
11
+ subject: string;
12
+ similarity?: number;
13
+ categories: string[];
14
+ confidence: number;
15
+ }
16
+ export declare class PersistioClient {
17
+ private readonly baseURL;
18
+ private readonly apiKey;
19
+ private readonly recallTopK;
20
+ private readonly recallTimeout;
21
+ constructor(config: PersistioConfig);
22
+ private headers;
23
+ recall(query: string): Promise<PersistioMemory[]>;
24
+ ingest(sessionId: string, chunks: Array<{
25
+ role: string;
26
+ content: string;
27
+ }>): Promise<void>;
28
+ addMemory(data: string, subject: string): Promise<void>;
29
+ deleteMemory(id: string): Promise<void>;
30
+ listMemories(): Promise<PersistioMemory[]>;
31
+ }
package/dist/client.js ADDED
@@ -0,0 +1,67 @@
1
+ export class PersistioClient {
2
+ baseURL;
3
+ apiKey;
4
+ recallTopK;
5
+ recallTimeout;
6
+ constructor(config) {
7
+ this.baseURL = config.baseURL.replace(/\/$/, '');
8
+ this.apiKey = config.apiKey;
9
+ this.recallTopK = config.recallTopK;
10
+ this.recallTimeout = config.recallTimeout;
11
+ }
12
+ headers() {
13
+ return {
14
+ 'Content-Type': 'application/json',
15
+ 'Authorization': `Bearer ${this.apiKey}`,
16
+ };
17
+ }
18
+ async recall(query) {
19
+ const res = await fetch(`${this.baseURL}/v1/recall`, {
20
+ method: 'POST',
21
+ headers: this.headers(),
22
+ body: JSON.stringify({ query, top_k: this.recallTopK }),
23
+ signal: AbortSignal.timeout(this.recallTimeout),
24
+ });
25
+ if (!res.ok)
26
+ throw new Error(`Persistio recall failed: ${res.status}`);
27
+ const data = await res.json();
28
+ return data.memories ?? [];
29
+ }
30
+ async ingest(sessionId, chunks) {
31
+ if (chunks.length === 0)
32
+ return;
33
+ const res = await fetch(`${this.baseURL}/v1/ingest`, {
34
+ method: 'POST',
35
+ headers: this.headers(),
36
+ body: JSON.stringify({ session_id: sessionId, chunks }),
37
+ });
38
+ if (!res.ok)
39
+ throw new Error(`Persistio ingest failed: ${res.status}`);
40
+ }
41
+ async addMemory(data, subject) {
42
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
43
+ method: 'POST',
44
+ headers: this.headers(),
45
+ body: JSON.stringify({ data, subject }),
46
+ });
47
+ if (!res.ok)
48
+ throw new Error(`Persistio addMemory failed: ${res.status}`);
49
+ }
50
+ async deleteMemory(id) {
51
+ const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
52
+ method: 'DELETE',
53
+ headers: this.headers(),
54
+ });
55
+ if (!res.ok)
56
+ throw new Error(`Persistio deleteMemory failed: ${res.status}`);
57
+ }
58
+ async listMemories() {
59
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
60
+ headers: this.headers(),
61
+ });
62
+ if (!res.ok)
63
+ throw new Error(`Persistio listMemories failed: ${res.status}`);
64
+ const data = await res.json();
65
+ return data.items ?? [];
66
+ }
67
+ }
@@ -0,0 +1,8 @@
1
+ declare const _default: {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ configSchema: import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginConfigSchema;
6
+ register: NonNullable<import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition["register"]>;
7
+ } & Pick<import("openclaw/plugin-sdk/plugin-entry").OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
8
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,187 @@
1
+ import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { PersistioClient } from './client.js';
4
+ function resolveConfig(raw) {
5
+ const c = (raw ?? {});
6
+ return {
7
+ baseURL: typeof c['baseURL'] === 'string' ? c['baseURL'] : '',
8
+ apiKey: typeof c['apiKey'] === 'string' ? c['apiKey'] : '',
9
+ tokenBudget: typeof c['tokenBudget'] === 'number' ? c['tokenBudget'] : 2000,
10
+ recallTopK: typeof c['recallTopK'] === 'number' ? c['recallTopK'] : 10,
11
+ recallTimeout: typeof c['recallTimeout'] === 'number' ? c['recallTimeout'] : 5000,
12
+ };
13
+ }
14
+ function estimateTokens(text) {
15
+ return Math.ceil(text.length / 4);
16
+ }
17
+ function buildMemoryBlock(memories, budget) {
18
+ if (memories.length === 0)
19
+ return '';
20
+ const lines = ['## Relevant memories from past conversations'];
21
+ let used = estimateTokens(lines[0]);
22
+ for (const m of memories) {
23
+ const line = `- ${m.data} [${m.subject}]`;
24
+ const cost = estimateTokens(line);
25
+ if (used + cost > budget)
26
+ break;
27
+ lines.push(line);
28
+ used += cost;
29
+ }
30
+ return lines.length > 1 ? lines.join('\n') : '';
31
+ }
32
+ /** Extract plain text from a pi-agent-core message content array */
33
+ function extractTextFromMessage(msg) {
34
+ if (typeof msg !== 'object' || msg === null)
35
+ return null;
36
+ const m = msg;
37
+ const role = m['role'];
38
+ if (role !== 'user' && role !== 'assistant')
39
+ return null;
40
+ const content = m['content'];
41
+ if (!Array.isArray(content)) {
42
+ // Some messages have content as a plain string
43
+ if (typeof content === 'string' && content.length > 0)
44
+ return content;
45
+ return null;
46
+ }
47
+ const parts = [];
48
+ for (const block of content) {
49
+ if (typeof block === 'object' && block !== null) {
50
+ const b = block;
51
+ if (b['type'] === 'text' && typeof b['text'] === 'string' && b['text'].length > 0) {
52
+ parts.push(b['text']);
53
+ }
54
+ }
55
+ }
56
+ return parts.length > 0 ? parts.join(' ') : null;
57
+ }
58
+ export default definePluginEntry({
59
+ id: 'openclaw-persistio',
60
+ name: 'Persistio Memory',
61
+ description: 'Persistent semantic memory for OpenClaw via Persistio',
62
+ register(api) {
63
+ const cfg = resolveConfig(api.pluginConfig);
64
+ if (!cfg.baseURL || !cfg.apiKey) {
65
+ api.logger?.warn?.('openclaw-persistio: baseURL and apiKey are required. Plugin disabled.');
66
+ return;
67
+ }
68
+ const client = new PersistioClient(cfg);
69
+ // -------------------------------------------------------------------------
70
+ // before_prompt_build — recall relevant memories and inject into context
71
+ // Event: { prompt: string, messages: unknown[] }
72
+ // Return: { appendSystemContext?: string }
73
+ // -------------------------------------------------------------------------
74
+ api.on('before_prompt_build', async (event) => {
75
+ try {
76
+ // Use the current prompt as the recall query
77
+ const query = event.prompt?.slice(0, 500) || 'recent context';
78
+ const memories = await client.recall(query);
79
+ if (memories.length === 0)
80
+ return;
81
+ const block = buildMemoryBlock(memories, cfg.tokenBudget);
82
+ if (!block)
83
+ return;
84
+ return { appendSystemContext: block };
85
+ }
86
+ catch (err) {
87
+ api.logger?.warn?.(`openclaw-persistio: recall error: ${String(err)}`);
88
+ }
89
+ });
90
+ // -------------------------------------------------------------------------
91
+ // agent_end — ingest new turn messages (fire and forget)
92
+ // Event: { runId?, messages: unknown[], success: boolean, error?, durationMs? }
93
+ // Observation only — no return value.
94
+ // -------------------------------------------------------------------------
95
+ api.on('agent_end', async (event) => {
96
+ try {
97
+ const sessionId = event.runId ?? 'unknown-session';
98
+ const chunks = [];
99
+ for (const msg of event.messages) {
100
+ const m = msg;
101
+ const role = m['role'];
102
+ if (role !== 'user' && role !== 'assistant')
103
+ continue;
104
+ const text = extractTextFromMessage(msg);
105
+ if (text && text.length > 0) {
106
+ chunks.push({ role: role, content: text });
107
+ }
108
+ }
109
+ if (chunks.length === 0)
110
+ return;
111
+ // Fire and forget — agent_end is async but result is ignored
112
+ client.ingest(sessionId, chunks).catch((err) => {
113
+ api.logger?.warn?.(`openclaw-persistio: ingest error: ${String(err)}`);
114
+ });
115
+ }
116
+ catch (err) {
117
+ api.logger?.warn?.(`openclaw-persistio: agent_end error: ${String(err)}`);
118
+ }
119
+ });
120
+ // -------------------------------------------------------------------------
121
+ // Tools
122
+ // Verified signature: api.registerTool({ name, description, parameters, execute }, opts?)
123
+ // execute(_id: string, params: unknown): Promise<AgentToolResult>
124
+ // AgentToolResult: { content: Array<{ type: "text", text: string }>, details: unknown }
125
+ // -------------------------------------------------------------------------
126
+ api.registerTool({
127
+ name: 'memory_search',
128
+ label: 'Search Memory',
129
+ description: 'Search persistent memory for relevant facts from past conversations.',
130
+ parameters: Type.Object({
131
+ query: Type.String({ description: 'What to search for' }),
132
+ top_k: Type.Optional(Type.Number({ description: 'Max results to return' })),
133
+ }),
134
+ async execute(_id, params) {
135
+ const p = params;
136
+ const overrideTopK = typeof p.top_k === 'number' ? p.top_k : cfg.recallTopK;
137
+ const overrideCfg = { ...cfg, recallTopK: overrideTopK };
138
+ const c = new PersistioClient(overrideCfg);
139
+ const memories = await c.recall(p.query);
140
+ const text = memories.length > 0
141
+ ? memories.map(m => `- ${m.data} [${m.subject}]`).join('\n')
142
+ : 'No memories found.';
143
+ return { content: [{ type: 'text', text }], details: null };
144
+ },
145
+ });
146
+ api.registerTool({
147
+ name: 'memory_add',
148
+ label: 'Add Memory',
149
+ description: 'Manually store a fact in persistent memory.',
150
+ parameters: Type.Object({
151
+ data: Type.String({ description: 'The fact to remember' }),
152
+ subject: Type.String({ description: 'The entity or topic this fact is about' }),
153
+ }),
154
+ async execute(_id, params) {
155
+ const p = params;
156
+ await client.addMemory(p.data, p.subject);
157
+ return { content: [{ type: 'text', text: 'Memory stored.' }], details: null };
158
+ },
159
+ });
160
+ api.registerTool({
161
+ name: 'memory_delete',
162
+ label: 'Delete Memory',
163
+ description: 'Delete a specific memory by its ID.',
164
+ parameters: Type.Object({
165
+ id: Type.String({ description: 'The memory ID to delete' }),
166
+ }),
167
+ async execute(_id, params) {
168
+ const p = params;
169
+ await client.deleteMemory(p.id);
170
+ return { content: [{ type: 'text', text: 'Memory deleted.' }], details: null };
171
+ },
172
+ }, { optional: true });
173
+ api.registerTool({
174
+ name: 'memory_list',
175
+ label: 'List Memories',
176
+ description: 'List all stored memories.',
177
+ parameters: Type.Object({}),
178
+ async execute(_id, _params) {
179
+ const memories = await client.listMemories();
180
+ const text = memories.length > 0
181
+ ? memories.map(m => `[${m.id}] ${m.data} (${m.subject})`).join('\n')
182
+ : 'No memories stored.';
183
+ return { content: [{ type: 'text', text }], details: null };
184
+ },
185
+ }, { optional: true });
186
+ },
187
+ });
@@ -0,0 +1,25 @@
1
+ {
2
+ "id": "@persistio/openclaw-plugin",
3
+ "name": "Persistio Memory",
4
+ "description": "Persistent semantic memory for OpenClaw via Persistio",
5
+ "version": "0.1.0",
6
+ "kind": "memory",
7
+ "activation": {
8
+ "onStartup": true
9
+ },
10
+ "contracts": {
11
+ "tools": ["memory_search", "memory_add", "memory_delete", "memory_list"]
12
+ },
13
+ "configSchema": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "baseURL": { "type": "string" },
18
+ "apiKey": { "type": "string" },
19
+ "tokenBudget": { "type": "number" },
20
+ "recallTopK": { "type": "number" },
21
+ "recallTimeout": { "type": "number" }
22
+ },
23
+ "required": ["baseURL", "apiKey"]
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@persistio/openclaw-plugin",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin for Persistio — persistent semantic memory for AI agents",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "src",
13
+ "openclaw.plugin.json",
14
+ "README.md"
15
+ ],
16
+ "license": "MIT",
17
+ "author": "Chris Coveyduck",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/persistio/openclaw-persistio.git"
21
+ },
22
+ "homepage": "https://persistio.ai",
23
+ "keywords": [
24
+ "openclaw",
25
+ "openclaw-plugin",
26
+ "memory",
27
+ "ai",
28
+ "agents",
29
+ "persistio"
30
+ ],
31
+ "openclaw": {
32
+ "extensions": [
33
+ "./src/index.ts"
34
+ ],
35
+ "runtimeExtensions": [
36
+ "./dist/index.js"
37
+ ],
38
+ "compat": {
39
+ "pluginApi": ">=2026.3.24-beta.2",
40
+ "minGatewayVersion": "2026.3.24-beta.2"
41
+ }
42
+ },
43
+ "scripts": {
44
+ "build": "tsc"
45
+ },
46
+ "dependencies": {
47
+ "@sinclair/typebox": "^0.34.0"
48
+ },
49
+ "devDependencies": {
50
+ "typescript": "^5.0.0",
51
+ "@types/node": "^22.0.0"
52
+ },
53
+ "peerDependencies": {
54
+ "openclaw": ">=2026.3.24-beta.2"
55
+ }
56
+ }
package/src/client.ts ADDED
@@ -0,0 +1,85 @@
1
+ export interface PersistioConfig {
2
+ baseURL: string;
3
+ apiKey: string;
4
+ tokenBudget: number;
5
+ recallTopK: number;
6
+ recallTimeout: number;
7
+ }
8
+
9
+ export interface PersistioMemory {
10
+ id: string;
11
+ data: string;
12
+ subject: string;
13
+ similarity?: number;
14
+ categories: string[];
15
+ confidence: number;
16
+ }
17
+
18
+ export class PersistioClient {
19
+ private readonly baseURL: string;
20
+ private readonly apiKey: string;
21
+ private readonly recallTopK: number;
22
+ private readonly recallTimeout: number;
23
+
24
+ constructor(config: PersistioConfig) {
25
+ this.baseURL = config.baseURL.replace(/\/$/, '');
26
+ this.apiKey = config.apiKey;
27
+ this.recallTopK = config.recallTopK;
28
+ this.recallTimeout = config.recallTimeout;
29
+ }
30
+
31
+ private headers(): Record<string, string> {
32
+ return {
33
+ 'Content-Type': 'application/json',
34
+ 'Authorization': `Bearer ${this.apiKey}`,
35
+ };
36
+ }
37
+
38
+ async recall(query: string): Promise<PersistioMemory[]> {
39
+ const res = await fetch(`${this.baseURL}/v1/recall`, {
40
+ method: 'POST',
41
+ headers: this.headers(),
42
+ body: JSON.stringify({ query, top_k: this.recallTopK }),
43
+ signal: AbortSignal.timeout(this.recallTimeout),
44
+ });
45
+ if (!res.ok) throw new Error(`Persistio recall failed: ${res.status}`);
46
+ const data = await res.json() as { memories: PersistioMemory[] };
47
+ return data.memories ?? [];
48
+ }
49
+
50
+ async ingest(sessionId: string, chunks: Array<{ role: string; content: string }>): Promise<void> {
51
+ if (chunks.length === 0) return;
52
+ const res = await fetch(`${this.baseURL}/v1/ingest`, {
53
+ method: 'POST',
54
+ headers: this.headers(),
55
+ body: JSON.stringify({ session_id: sessionId, chunks }),
56
+ });
57
+ if (!res.ok) throw new Error(`Persistio ingest failed: ${res.status}`);
58
+ }
59
+
60
+ async addMemory(data: string, subject: string): Promise<void> {
61
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
62
+ method: 'POST',
63
+ headers: this.headers(),
64
+ body: JSON.stringify({ data, subject }),
65
+ });
66
+ if (!res.ok) throw new Error(`Persistio addMemory failed: ${res.status}`);
67
+ }
68
+
69
+ async deleteMemory(id: string): Promise<void> {
70
+ const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
71
+ method: 'DELETE',
72
+ headers: this.headers(),
73
+ });
74
+ if (!res.ok) throw new Error(`Persistio deleteMemory failed: ${res.status}`);
75
+ }
76
+
77
+ async listMemories(): Promise<PersistioMemory[]> {
78
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
79
+ headers: this.headers(),
80
+ });
81
+ if (!res.ok) throw new Error(`Persistio listMemories failed: ${res.status}`);
82
+ const data = await res.json() as { items: PersistioMemory[] };
83
+ return data.items ?? [];
84
+ }
85
+ }
package/src/index.ts ADDED
@@ -0,0 +1,193 @@
1
+ import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
2
+ import { Type } from '@sinclair/typebox';
3
+ import { PersistioClient, type PersistioConfig } from './client.js';
4
+
5
+ function resolveConfig(raw: unknown): PersistioConfig {
6
+ const c = (raw ?? {}) as Record<string, unknown>;
7
+ return {
8
+ baseURL: typeof c['baseURL'] === 'string' ? c['baseURL'] : '',
9
+ apiKey: typeof c['apiKey'] === 'string' ? c['apiKey'] : '',
10
+ tokenBudget: typeof c['tokenBudget'] === 'number' ? c['tokenBudget'] : 2000,
11
+ recallTopK: typeof c['recallTopK'] === 'number' ? c['recallTopK'] : 10,
12
+ recallTimeout: typeof c['recallTimeout'] === 'number' ? c['recallTimeout'] : 5000,
13
+ };
14
+ }
15
+
16
+ function estimateTokens(text: string): number {
17
+ return Math.ceil(text.length / 4);
18
+ }
19
+
20
+ function buildMemoryBlock(memories: import('./client.js').PersistioMemory[], budget: number): string {
21
+ if (memories.length === 0) return '';
22
+ const lines: string[] = ['## Relevant memories from past conversations'];
23
+ let used = estimateTokens(lines[0]!);
24
+ for (const m of memories) {
25
+ const line = `- ${m.data} [${m.subject}]`;
26
+ const cost = estimateTokens(line);
27
+ if (used + cost > budget) break;
28
+ lines.push(line);
29
+ used += cost;
30
+ }
31
+ return lines.length > 1 ? lines.join('\n') : '';
32
+ }
33
+
34
+ /** Extract plain text from a pi-agent-core message content array */
35
+ function extractTextFromMessage(msg: unknown): string | null {
36
+ if (typeof msg !== 'object' || msg === null) return null;
37
+ const m = msg as Record<string, unknown>;
38
+ const role = m['role'];
39
+ if (role !== 'user' && role !== 'assistant') return null;
40
+ const content = m['content'];
41
+ if (!Array.isArray(content)) {
42
+ // Some messages have content as a plain string
43
+ if (typeof content === 'string' && content.length > 0) return content;
44
+ return null;
45
+ }
46
+ const parts: string[] = [];
47
+ for (const block of content) {
48
+ if (typeof block === 'object' && block !== null) {
49
+ const b = block as Record<string, unknown>;
50
+ if (b['type'] === 'text' && typeof b['text'] === 'string' && b['text'].length > 0) {
51
+ parts.push(b['text']);
52
+ }
53
+ }
54
+ }
55
+ return parts.length > 0 ? parts.join(' ') : null;
56
+ }
57
+
58
+ export default definePluginEntry({
59
+ id: 'openclaw-persistio',
60
+ name: 'Persistio Memory',
61
+ description: 'Persistent semantic memory for OpenClaw via Persistio',
62
+
63
+ register(api) {
64
+ const cfg = resolveConfig(api.pluginConfig);
65
+
66
+ if (!cfg.baseURL || !cfg.apiKey) {
67
+ api.logger?.warn?.('openclaw-persistio: baseURL and apiKey are required. Plugin disabled.');
68
+ return;
69
+ }
70
+
71
+ const client = new PersistioClient(cfg);
72
+
73
+ // -------------------------------------------------------------------------
74
+ // before_prompt_build — recall relevant memories and inject into context
75
+ // Event: { prompt: string, messages: unknown[] }
76
+ // Return: { appendSystemContext?: string }
77
+ // -------------------------------------------------------------------------
78
+ api.on('before_prompt_build', async (event) => {
79
+ try {
80
+ // Use the current prompt as the recall query
81
+ const query = event.prompt?.slice(0, 500) || 'recent context';
82
+ const memories = await client.recall(query);
83
+ if (memories.length === 0) return;
84
+ const block = buildMemoryBlock(memories, cfg.tokenBudget);
85
+ if (!block) return;
86
+ return { appendSystemContext: block };
87
+ } catch (err) {
88
+ api.logger?.warn?.(`openclaw-persistio: recall error: ${String(err)}`);
89
+ }
90
+ });
91
+
92
+ // -------------------------------------------------------------------------
93
+ // agent_end — ingest new turn messages (fire and forget)
94
+ // Event: { runId?, messages: unknown[], success: boolean, error?, durationMs? }
95
+ // Observation only — no return value.
96
+ // -------------------------------------------------------------------------
97
+ api.on('agent_end', async (event) => {
98
+ try {
99
+ const sessionId = event.runId ?? 'unknown-session';
100
+ const chunks: Array<{ role: string; content: string }> = [];
101
+
102
+ for (const msg of event.messages) {
103
+ const m = msg as Record<string, unknown>;
104
+ const role = m['role'];
105
+ if (role !== 'user' && role !== 'assistant') continue;
106
+ const text = extractTextFromMessage(msg);
107
+ if (text && text.length > 0) {
108
+ chunks.push({ role: role as string, content: text });
109
+ }
110
+ }
111
+
112
+ if (chunks.length === 0) return;
113
+ // Fire and forget — agent_end is async but result is ignored
114
+ client.ingest(sessionId, chunks).catch((err: unknown) => {
115
+ api.logger?.warn?.(`openclaw-persistio: ingest error: ${String(err)}`);
116
+ });
117
+ } catch (err) {
118
+ api.logger?.warn?.(`openclaw-persistio: agent_end error: ${String(err)}`);
119
+ }
120
+ });
121
+
122
+ // -------------------------------------------------------------------------
123
+ // Tools
124
+ // Verified signature: api.registerTool({ name, description, parameters, execute }, opts?)
125
+ // execute(_id: string, params: unknown): Promise<AgentToolResult>
126
+ // AgentToolResult: { content: Array<{ type: "text", text: string }>, details: unknown }
127
+ // -------------------------------------------------------------------------
128
+
129
+ api.registerTool({
130
+ name: 'memory_search',
131
+ label: 'Search Memory',
132
+ description: 'Search persistent memory for relevant facts from past conversations.',
133
+ parameters: Type.Object({
134
+ query: Type.String({ description: 'What to search for' }),
135
+ top_k: Type.Optional(Type.Number({ description: 'Max results to return' })),
136
+ }),
137
+ async execute(_id, params) {
138
+ const p = params as { query: string; top_k?: number };
139
+ const overrideTopK = typeof p.top_k === 'number' ? p.top_k : cfg.recallTopK;
140
+ const overrideCfg = { ...cfg, recallTopK: overrideTopK };
141
+ const c = new PersistioClient(overrideCfg);
142
+ const memories = await c.recall(p.query);
143
+ const text = memories.length > 0
144
+ ? memories.map(m => `- ${m.data} [${m.subject}]`).join('\n')
145
+ : 'No memories found.';
146
+ return { content: [{ type: 'text' as const, text }], details: null };
147
+ },
148
+ });
149
+
150
+ api.registerTool({
151
+ name: 'memory_add',
152
+ label: 'Add Memory',
153
+ description: 'Manually store a fact in persistent memory.',
154
+ parameters: Type.Object({
155
+ data: Type.String({ description: 'The fact to remember' }),
156
+ subject: Type.String({ description: 'The entity or topic this fact is about' }),
157
+ }),
158
+ async execute(_id, params) {
159
+ const p = params as { data: string; subject: string };
160
+ await client.addMemory(p.data, p.subject);
161
+ return { content: [{ type: 'text' as const, text: 'Memory stored.' }], details: null };
162
+ },
163
+ });
164
+
165
+ api.registerTool({
166
+ name: 'memory_delete',
167
+ label: 'Delete Memory',
168
+ description: 'Delete a specific memory by its ID.',
169
+ parameters: Type.Object({
170
+ id: Type.String({ description: 'The memory ID to delete' }),
171
+ }),
172
+ async execute(_id, params) {
173
+ const p = params as { id: string };
174
+ await client.deleteMemory(p.id);
175
+ return { content: [{ type: 'text' as const, text: 'Memory deleted.' }], details: null };
176
+ },
177
+ }, { optional: true });
178
+
179
+ api.registerTool({
180
+ name: 'memory_list',
181
+ label: 'List Memories',
182
+ description: 'List all stored memories.',
183
+ parameters: Type.Object({}),
184
+ async execute(_id, _params) {
185
+ const memories = await client.listMemories();
186
+ const text = memories.length > 0
187
+ ? memories.map(m => `[${m.id}] ${m.data} (${m.subject})`).join('\n')
188
+ : 'No memories stored.';
189
+ return { content: [{ type: 'text' as const, text }], details: null };
190
+ },
191
+ }, { optional: true });
192
+ },
193
+ });