@persistio/openclaw-plugin 0.1.3 → 0.1.4

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
@@ -25,7 +25,14 @@ Then register it in your OpenClaw config:
25
25
  "package": "@persistio/openclaw-plugin",
26
26
  "config": {
27
27
  "baseURL": "https://api.persistio.ai",
28
- "apiKey": "your-vault-api-key"
28
+ "apiKey": "your-vault-api-key",
29
+ "send": {
30
+ "roles": {
31
+ "user": "enabled",
32
+ "agent": "enabled",
33
+ "tool": "disabled"
34
+ }
35
+ }
29
36
  }
30
37
  }
31
38
  }
@@ -42,6 +49,11 @@ Then register it in your OpenClaw config:
42
49
  | `tokenBudget` | number | | `2000` | Max tokens to inject into the system prompt |
43
50
  | `recallTopK` | number | | `10` | Number of memories to retrieve per recall |
44
51
  | `recallTimeout` | number | | `5000` | HTTP timeout for recall requests (ms) |
52
+ | `send.roles.user` | `"enabled"` or `"disabled"` | | `"enabled"` | Send user messages to Persistio ingest |
53
+ | `send.roles.agent` | `"enabled"` or `"disabled"` | | `"enabled"` | Send agent/assistant messages to Persistio ingest |
54
+ | `send.roles.tool` | `"enabled"` or `"disabled"` | | `"disabled"` | Send tool messages to Persistio ingest |
55
+
56
+ `agent_end` receives a snapshot of the active OpenClaw transcript, so the plugin deduplicates per session and only sends each user, agent, or enabled tool message once per plugin process. Deduplication keys are bounded in memory and expire after 24 hours of session inactivity.
45
57
 
46
58
  ## Tools exposed
47
59
 
package/dist/client.d.ts CHANGED
@@ -4,6 +4,15 @@ export interface PersistioConfig {
4
4
  tokenBudget: number;
5
5
  recallTopK: number;
6
6
  recallTimeout: number;
7
+ send: PersistioSendConfig;
8
+ }
9
+ export type PersistioSendRoleStatus = 'enabled' | 'disabled';
10
+ export interface PersistioSendConfig {
11
+ roles: {
12
+ user: PersistioSendRoleStatus;
13
+ agent: PersistioSendRoleStatus;
14
+ tool: PersistioSendRoleStatus;
15
+ };
7
16
  }
8
17
  export interface PersistioMemory {
9
18
  id: string;
package/dist/index.js CHANGED
@@ -1,6 +1,30 @@
1
1
  import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
2
2
  import { Type } from '@sinclair/typebox';
3
3
  import { PersistioClient } from './client.js';
4
+ const DEFAULT_SEND_ROLES = {
5
+ user: 'enabled',
6
+ agent: 'enabled',
7
+ tool: 'disabled',
8
+ };
9
+ const MESSAGE_KEY_TTL_MS = 24 * 60 * 60 * 1000;
10
+ const MAX_TRACKED_SESSIONS = 250;
11
+ const MAX_SENT_KEYS_PER_SESSION = 2000;
12
+ function resolveSendConfig(raw) {
13
+ const send = raw['send'];
14
+ const roles = typeof send === 'object' && send !== null
15
+ ? send['roles']
16
+ : undefined;
17
+ const rawRoles = typeof roles === 'object' && roles !== null
18
+ ? roles
19
+ : {};
20
+ return {
21
+ roles: {
22
+ user: rawRoles['user'] === 'disabled' ? 'disabled' : DEFAULT_SEND_ROLES.user,
23
+ agent: rawRoles['agent'] === 'disabled' ? 'disabled' : DEFAULT_SEND_ROLES.agent,
24
+ tool: rawRoles['tool'] === 'enabled' ? 'enabled' : DEFAULT_SEND_ROLES.tool,
25
+ },
26
+ };
27
+ }
4
28
  function resolveConfig(raw) {
5
29
  const c = (raw ?? {});
6
30
  return {
@@ -9,6 +33,7 @@ function resolveConfig(raw) {
9
33
  tokenBudget: typeof c['tokenBudget'] === 'number' ? c['tokenBudget'] : 2000,
10
34
  recallTopK: typeof c['recallTopK'] === 'number' ? c['recallTopK'] : 10,
11
35
  recallTimeout: typeof c['recallTimeout'] === 'number' ? c['recallTimeout'] : 5000,
36
+ send: resolveSendConfig(c),
12
37
  };
13
38
  }
14
39
  function estimateTokens(text) {
@@ -117,13 +142,23 @@ function buildMemoryBlock(bundle, budget) {
117
142
  }
118
143
  return lines.length > 1 ? lines.join('\n') : '';
119
144
  }
145
+ function normalizeRole(role) {
146
+ if (role === 'user' || role === 'assistant' || role === 'tool')
147
+ return role;
148
+ return null;
149
+ }
150
+ function shouldSendRole(role, config) {
151
+ if (role === 'assistant')
152
+ return config.send.roles.agent === 'enabled';
153
+ return config.send.roles[role] === 'enabled';
154
+ }
120
155
  /** Extract plain text from a pi-agent-core message content array */
121
- function extractTextFromMessage(msg) {
156
+ function extractTextFromMessage(msg, allowedRoles = ['user', 'assistant']) {
122
157
  if (typeof msg !== 'object' || msg === null)
123
158
  return null;
124
159
  const m = msg;
125
- const role = m['role'];
126
- if (role !== 'user' && role !== 'assistant')
160
+ const role = normalizeRole(m['role']);
161
+ if (!role || !allowedRoles.includes(role))
127
162
  return null;
128
163
  const content = m['content'];
129
164
  if (!Array.isArray(content)) {
@@ -143,6 +178,72 @@ function extractTextFromMessage(msg) {
143
178
  }
144
179
  return parts.length > 0 ? parts.join(' ') : null;
145
180
  }
181
+ function resolveMessageTimestamp(msg) {
182
+ if (typeof msg['timestamp'] === 'number')
183
+ return new Date(msg['timestamp']).toISOString();
184
+ if (typeof msg['timestamp'] === 'string')
185
+ return msg['timestamp'];
186
+ return null;
187
+ }
188
+ function hashString(input) {
189
+ let hash = 0x811c9dc5;
190
+ for (let i = 0; i < input.length; i += 1) {
191
+ hash ^= input.charCodeAt(i);
192
+ hash = Math.imul(hash, 0x01000193);
193
+ }
194
+ return (hash >>> 0).toString(16);
195
+ }
196
+ function buildMessageFingerprint(params) {
197
+ const id = params.msg['id'];
198
+ if (typeof id === 'string' && id.length > 0) {
199
+ return `id:${params.sessionId}:${id}`;
200
+ }
201
+ const idempotencyKey = params.msg['idempotencyKey'];
202
+ if (typeof idempotencyKey === 'string' && idempotencyKey.length > 0) {
203
+ return `idempotency:${params.sessionId}:${idempotencyKey}`;
204
+ }
205
+ const timestamp = resolveMessageTimestamp(params.msg);
206
+ const basis = timestamp ?? `index:${params.index}`;
207
+ return `content:${params.sessionId}:${basis}:${params.role}:${hashString(params.text)}`;
208
+ }
209
+ function pruneSessionKeyStores(stores, now) {
210
+ for (const [sessionId, store] of stores) {
211
+ if (now - store.lastSeen > MESSAGE_KEY_TTL_MS)
212
+ stores.delete(sessionId);
213
+ }
214
+ while (stores.size > MAX_TRACKED_SESSIONS) {
215
+ const oldest = [...stores.entries()].sort((a, b) => a[1].lastSeen - b[1].lastSeen)[0];
216
+ if (!oldest)
217
+ return;
218
+ stores.delete(oldest[0]);
219
+ }
220
+ }
221
+ function getSessionKeyStore(stores, sessionId, now) {
222
+ pruneSessionKeyStores(stores, now);
223
+ const existing = stores.get(sessionId);
224
+ if (existing) {
225
+ existing.lastSeen = now;
226
+ return existing.keys;
227
+ }
228
+ const created = { keys: new Set(), lastSeen: now };
229
+ stores.set(sessionId, created);
230
+ return created.keys;
231
+ }
232
+ function rememberKeys(target, keys, limit = Number.POSITIVE_INFINITY) {
233
+ for (const key of keys) {
234
+ target.add(key);
235
+ while (target.size > limit) {
236
+ const oldest = target.values().next().value;
237
+ if (!oldest)
238
+ break;
239
+ target.delete(oldest);
240
+ }
241
+ }
242
+ }
243
+ function forgetKeys(target, keys) {
244
+ for (const key of keys)
245
+ target.delete(key);
246
+ }
146
247
  const PERSISTIO_MEMORY_PATH_PREFIX = 'persistio://memory/';
147
248
  function createClient(config, recallTopK = config.recallTopK) {
148
249
  return new PersistioClient({ ...config, recallTopK });
@@ -275,6 +376,8 @@ export default definePluginEntry({
275
376
  return;
276
377
  }
277
378
  const client = createClient(cfg);
379
+ const sentMessageKeysBySession = new Map();
380
+ const pendingMessageKeysBySession = new Map();
278
381
  api.registerMemoryCapability({
279
382
  runtime: createMemoryRuntime(cfg),
280
383
  });
@@ -307,26 +410,37 @@ export default definePluginEntry({
307
410
  if (sessionId.startsWith('announce:'))
308
411
  return;
309
412
  const chunks = [];
310
- for (const msg of event.messages) {
413
+ const chunkKeys = [];
414
+ const now = Date.now();
415
+ const sentKeys = getSessionKeyStore(sentMessageKeysBySession, sessionId, now);
416
+ const pendingKeys = getSessionKeyStore(pendingMessageKeysBySession, sessionId, now);
417
+ for (const [index, msg] of event.messages.entries()) {
311
418
  const m = msg;
312
- const role = m['role'];
313
- if (role !== 'user' && role !== 'assistant')
419
+ const role = normalizeRole(m['role']);
420
+ if (!role || !shouldSendRole(role, cfg))
421
+ continue;
422
+ const text = extractTextFromMessage(msg, ['user', 'assistant', 'tool']);
423
+ if (!text || text.length === 0)
424
+ continue;
425
+ const key = buildMessageFingerprint({ sessionId, msg: m, role, text, index });
426
+ if (sentKeys.has(key) || pendingKeys.has(key))
314
427
  continue;
315
- const text = extractTextFromMessage(msg);
316
- const ts = typeof m['timestamp'] === 'number'
317
- ? new Date(m['timestamp']).toISOString()
318
- : typeof m['timestamp'] === 'string'
319
- ? m['timestamp']
320
- : new Date().toISOString();
321
- if (text && text.length > 0) {
322
- chunks.push({ role: role, content: text, timestamp: ts });
323
- }
428
+ const ts = resolveMessageTimestamp(m) ?? new Date().toISOString();
429
+ chunkKeys.push(key);
430
+ chunks.push({ role, content: text, timestamp: ts });
324
431
  }
325
432
  if (chunks.length === 0)
326
433
  return;
327
- // Fire and forget — agent_end is async but result is ignored
328
- client.ingest(sessionId, chunks).catch((err) => {
434
+ rememberKeys(pendingKeys, chunkKeys);
435
+ client.ingest(sessionId, chunks)
436
+ .then(() => {
437
+ rememberKeys(sentKeys, chunkKeys, MAX_SENT_KEYS_PER_SESSION);
438
+ })
439
+ .catch((err) => {
329
440
  api.logger?.warn?.(`openclaw-persistio: ingest error: ${String(err)}`);
441
+ })
442
+ .finally(() => {
443
+ forgetKeys(pendingKeys, chunkKeys);
330
444
  });
331
445
  }
332
446
  catch (err) {
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-persistio",
3
3
  "name": "Persistio Memory",
4
4
  "description": "Persistent semantic memory for OpenClaw via Persistio",
5
- "version": "0.1.3",
5
+ "version": "0.1.4",
6
6
  "kind": "memory",
7
7
  "activation": {
8
8
  "onStartup": true
@@ -33,6 +33,39 @@
33
33
  },
34
34
  "recallTimeout": {
35
35
  "type": "number"
36
+ },
37
+ "send": {
38
+ "type": "object",
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "roles": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "properties": {
45
+ "user": {
46
+ "type": "string",
47
+ "enum": [
48
+ "enabled",
49
+ "disabled"
50
+ ]
51
+ },
52
+ "agent": {
53
+ "type": "string",
54
+ "enum": [
55
+ "enabled",
56
+ "disabled"
57
+ ]
58
+ },
59
+ "tool": {
60
+ "type": "string",
61
+ "enum": [
62
+ "enabled",
63
+ "disabled"
64
+ ]
65
+ }
66
+ }
67
+ }
68
+ }
36
69
  }
37
70
  },
38
71
  "required": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistio/openclaw-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "OpenClaw plugin for Persistio \u2014 persistent semantic memory for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/client.ts CHANGED
@@ -4,6 +4,17 @@ export interface PersistioConfig {
4
4
  tokenBudget: number;
5
5
  recallTopK: number;
6
6
  recallTimeout: number;
7
+ send: PersistioSendConfig;
8
+ }
9
+
10
+ export type PersistioSendRoleStatus = 'enabled' | 'disabled';
11
+
12
+ export interface PersistioSendConfig {
13
+ roles: {
14
+ user: PersistioSendRoleStatus;
15
+ agent: PersistioSendRoleStatus;
16
+ tool: PersistioSendRoleStatus;
17
+ };
7
18
  }
8
19
 
9
20
  export interface PersistioMemory {
package/src/index.ts CHANGED
@@ -8,6 +8,41 @@ import type {
8
8
  import { Type } from '@sinclair/typebox';
9
9
  import { PersistioClient, type PersistioConfig, type PersistioMemory, type RecallBundle } from './client.js';
10
10
 
11
+ type OpenClawMessageRole = 'user' | 'assistant' | 'tool';
12
+
13
+ interface SessionMessageKeyStore {
14
+ keys: Set<string>;
15
+ lastSeen: number;
16
+ }
17
+
18
+ const DEFAULT_SEND_ROLES: PersistioConfig['send']['roles'] = {
19
+ user: 'enabled',
20
+ agent: 'enabled',
21
+ tool: 'disabled',
22
+ };
23
+
24
+ const MESSAGE_KEY_TTL_MS = 24 * 60 * 60 * 1000;
25
+ const MAX_TRACKED_SESSIONS = 250;
26
+ const MAX_SENT_KEYS_PER_SESSION = 2000;
27
+
28
+ function resolveSendConfig(raw: Record<string, unknown>): PersistioConfig['send'] {
29
+ const send = raw['send'];
30
+ const roles = typeof send === 'object' && send !== null
31
+ ? (send as Record<string, unknown>)['roles']
32
+ : undefined;
33
+ const rawRoles = typeof roles === 'object' && roles !== null
34
+ ? roles as Record<string, unknown>
35
+ : {};
36
+
37
+ return {
38
+ roles: {
39
+ user: rawRoles['user'] === 'disabled' ? 'disabled' : DEFAULT_SEND_ROLES.user,
40
+ agent: rawRoles['agent'] === 'disabled' ? 'disabled' : DEFAULT_SEND_ROLES.agent,
41
+ tool: rawRoles['tool'] === 'enabled' ? 'enabled' : DEFAULT_SEND_ROLES.tool,
42
+ },
43
+ };
44
+ }
45
+
11
46
  function resolveConfig(raw: unknown): PersistioConfig {
12
47
  const c = (raw ?? {}) as Record<string, unknown>;
13
48
  return {
@@ -16,6 +51,7 @@ function resolveConfig(raw: unknown): PersistioConfig {
16
51
  tokenBudget: typeof c['tokenBudget'] === 'number' ? c['tokenBudget'] : 2000,
17
52
  recallTopK: typeof c['recallTopK'] === 'number' ? c['recallTopK'] : 10,
18
53
  recallTimeout: typeof c['recallTimeout'] === 'number' ? c['recallTimeout'] : 5000,
54
+ send: resolveSendConfig(c),
19
55
  };
20
56
  }
21
57
 
@@ -136,12 +172,22 @@ function buildMemoryBlock(bundle: RecallBundle, budget: number): string {
136
172
  return lines.length > 1 ? lines.join('\n') : '';
137
173
  }
138
174
 
175
+ function normalizeRole(role: unknown): OpenClawMessageRole | null {
176
+ if (role === 'user' || role === 'assistant' || role === 'tool') return role;
177
+ return null;
178
+ }
179
+
180
+ function shouldSendRole(role: OpenClawMessageRole, config: PersistioConfig): boolean {
181
+ if (role === 'assistant') return config.send.roles.agent === 'enabled';
182
+ return config.send.roles[role] === 'enabled';
183
+ }
184
+
139
185
  /** Extract plain text from a pi-agent-core message content array */
140
- function extractTextFromMessage(msg: unknown): string | null {
186
+ function extractTextFromMessage(msg: unknown, allowedRoles: OpenClawMessageRole[] = ['user', 'assistant']): string | null {
141
187
  if (typeof msg !== 'object' || msg === null) return null;
142
188
  const m = msg as Record<string, unknown>;
143
- const role = m['role'];
144
- if (role !== 'user' && role !== 'assistant') return null;
189
+ const role = normalizeRole(m['role']);
190
+ if (!role || !allowedRoles.includes(role)) return null;
145
191
  const content = m['content'];
146
192
  if (!Array.isArray(content)) {
147
193
  // Some messages have content as a plain string
@@ -160,6 +206,83 @@ function extractTextFromMessage(msg: unknown): string | null {
160
206
  return parts.length > 0 ? parts.join(' ') : null;
161
207
  }
162
208
 
209
+ function resolveMessageTimestamp(msg: Record<string, unknown>): string | null {
210
+ if (typeof msg['timestamp'] === 'number') return new Date(msg['timestamp']).toISOString();
211
+ if (typeof msg['timestamp'] === 'string') return msg['timestamp'];
212
+ return null;
213
+ }
214
+
215
+ function hashString(input: string): string {
216
+ let hash = 0x811c9dc5;
217
+ for (let i = 0; i < input.length; i += 1) {
218
+ hash ^= input.charCodeAt(i);
219
+ hash = Math.imul(hash, 0x01000193);
220
+ }
221
+ return (hash >>> 0).toString(16);
222
+ }
223
+
224
+ function buildMessageFingerprint(params: {
225
+ sessionId: string;
226
+ msg: Record<string, unknown>;
227
+ role: OpenClawMessageRole;
228
+ text: string;
229
+ index: number;
230
+ }): string {
231
+ const id = params.msg['id'];
232
+ if (typeof id === 'string' && id.length > 0) {
233
+ return `id:${params.sessionId}:${id}`;
234
+ }
235
+
236
+ const idempotencyKey = params.msg['idempotencyKey'];
237
+ if (typeof idempotencyKey === 'string' && idempotencyKey.length > 0) {
238
+ return `idempotency:${params.sessionId}:${idempotencyKey}`;
239
+ }
240
+
241
+ const timestamp = resolveMessageTimestamp(params.msg);
242
+ const basis = timestamp ?? `index:${params.index}`;
243
+ return `content:${params.sessionId}:${basis}:${params.role}:${hashString(params.text)}`;
244
+ }
245
+
246
+ function pruneSessionKeyStores(stores: Map<string, SessionMessageKeyStore>, now: number): void {
247
+ for (const [sessionId, store] of stores) {
248
+ if (now - store.lastSeen > MESSAGE_KEY_TTL_MS) stores.delete(sessionId);
249
+ }
250
+
251
+ while (stores.size > MAX_TRACKED_SESSIONS) {
252
+ const oldest = [...stores.entries()].sort((a, b) => a[1].lastSeen - b[1].lastSeen)[0];
253
+ if (!oldest) return;
254
+ stores.delete(oldest[0]);
255
+ }
256
+ }
257
+
258
+ function getSessionKeyStore(stores: Map<string, SessionMessageKeyStore>, sessionId: string, now: number): Set<string> {
259
+ pruneSessionKeyStores(stores, now);
260
+ const existing = stores.get(sessionId);
261
+ if (existing) {
262
+ existing.lastSeen = now;
263
+ return existing.keys;
264
+ }
265
+
266
+ const created: SessionMessageKeyStore = { keys: new Set(), lastSeen: now };
267
+ stores.set(sessionId, created);
268
+ return created.keys;
269
+ }
270
+
271
+ function rememberKeys(target: Set<string>, keys: string[], limit = Number.POSITIVE_INFINITY): void {
272
+ for (const key of keys) {
273
+ target.add(key);
274
+ while (target.size > limit) {
275
+ const oldest = target.values().next().value as string | undefined;
276
+ if (!oldest) break;
277
+ target.delete(oldest);
278
+ }
279
+ }
280
+ }
281
+
282
+ function forgetKeys(target: Set<string>, keys: string[]): void {
283
+ for (const key of keys) target.delete(key);
284
+ }
285
+
163
286
  const PERSISTIO_MEMORY_PATH_PREFIX = 'persistio://memory/';
164
287
 
165
288
  function createClient(config: PersistioConfig, recallTopK = config.recallTopK): PersistioClient {
@@ -329,6 +452,8 @@ export default definePluginEntry({
329
452
  }
330
453
 
331
454
  const client = createClient(cfg);
455
+ const sentMessageKeysBySession = new Map<string, SessionMessageKeyStore>();
456
+ const pendingMessageKeysBySession = new Map<string, SessionMessageKeyStore>();
332
457
  api.registerMemoryCapability({
333
458
  runtime: createMemoryRuntime(cfg),
334
459
  });
@@ -360,27 +485,38 @@ export default definePluginEntry({
360
485
  const sessionId = context?.sessionId ?? event.runId ?? 'unknown-session';
361
486
  if (sessionId.startsWith('announce:')) return;
362
487
  const chunks: Array<{ role: string; content: string; timestamp: string }> = [];
488
+ const chunkKeys: string[] = [];
489
+ const now = Date.now();
490
+ const sentKeys = getSessionKeyStore(sentMessageKeysBySession, sessionId, now);
491
+ const pendingKeys = getSessionKeyStore(pendingMessageKeysBySession, sessionId, now);
363
492
 
364
- for (const msg of event.messages) {
493
+ for (const [index, msg] of event.messages.entries()) {
365
494
  const m = msg as Record<string, unknown>;
366
- const role = m['role'];
367
- if (role !== 'user' && role !== 'assistant') continue;
368
- const text = extractTextFromMessage(msg);
369
- const ts = typeof m['timestamp'] === 'number'
370
- ? new Date(m['timestamp']).toISOString()
371
- : typeof m['timestamp'] === 'string'
372
- ? m['timestamp']
373
- : new Date().toISOString();
374
- if (text && text.length > 0) {
375
- chunks.push({ role: role as string, content: text, timestamp: ts });
376
- }
495
+ const role = normalizeRole(m['role']);
496
+ if (!role || !shouldSendRole(role, cfg)) continue;
497
+ const text = extractTextFromMessage(msg, ['user', 'assistant', 'tool']);
498
+ if (!text || text.length === 0) continue;
499
+
500
+ const key = buildMessageFingerprint({ sessionId, msg: m, role, text, index });
501
+ if (sentKeys.has(key) || pendingKeys.has(key)) continue;
502
+
503
+ const ts = resolveMessageTimestamp(m) ?? new Date().toISOString();
504
+ chunkKeys.push(key);
505
+ chunks.push({ role, content: text, timestamp: ts });
377
506
  }
378
507
 
379
508
  if (chunks.length === 0) return;
380
- // Fire and forget — agent_end is async but result is ignored
381
- client.ingest(sessionId, chunks).catch((err: unknown) => {
382
- api.logger?.warn?.(`openclaw-persistio: ingest error: ${String(err)}`);
383
- });
509
+ rememberKeys(pendingKeys, chunkKeys);
510
+ client.ingest(sessionId, chunks)
511
+ .then(() => {
512
+ rememberKeys(sentKeys, chunkKeys, MAX_SENT_KEYS_PER_SESSION);
513
+ })
514
+ .catch((err: unknown) => {
515
+ api.logger?.warn?.(`openclaw-persistio: ingest error: ${String(err)}`);
516
+ })
517
+ .finally(() => {
518
+ forgetKeys(pendingKeys, chunkKeys);
519
+ });
384
520
  } catch (err) {
385
521
  api.logger?.warn?.(`openclaw-persistio: agent_end error: ${String(err)}`);
386
522
  }