@persistio/openclaw-plugin 0.1.6 → 0.1.8

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/dist/index.js CHANGED
@@ -10,6 +10,37 @@ const DEFAULT_SEND_ROLES = {
10
10
  const MESSAGE_KEY_TTL_MS = 24 * 60 * 60 * 1000;
11
11
  const MAX_TRACKED_SESSIONS = 250;
12
12
  const MAX_SENT_KEYS_PER_SESSION = 2000;
13
+ const RECALL_CIRCUIT_BREAKER_FAILURE_THRESHOLD = 3;
14
+ const RECALL_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
15
+ const RECALL_GUARD_MARGIN_MS = 250;
16
+ const DEFAULT_TOKEN_BUDGET = 400;
17
+ const DEFAULT_RECALL_TOP_K = 4;
18
+ const DEFAULT_RECALL_TIMEOUT_MS = 1500;
19
+ const MAX_MEMORY_SEARCH_RESULTS = 8;
20
+ const MAX_PROMPT_MEMORY_ITEM_CHARS = 500;
21
+ const MAX_MEMORY_SNIPPET_CHARS = 360;
22
+ class RecallCircuitBreaker {
23
+ consecutiveFailures = 0;
24
+ openedUntil = 0;
25
+ canAttempt(now = Date.now()) {
26
+ return now >= this.openedUntil;
27
+ }
28
+ remainingMs(now = Date.now()) {
29
+ return Math.max(0, this.openedUntil - now);
30
+ }
31
+ recordSuccess() {
32
+ this.consecutiveFailures = 0;
33
+ this.openedUntil = 0;
34
+ }
35
+ recordFailure(now = Date.now()) {
36
+ this.consecutiveFailures += 1;
37
+ if (this.consecutiveFailures >= RECALL_CIRCUIT_BREAKER_FAILURE_THRESHOLD) {
38
+ this.openedUntil = now + RECALL_CIRCUIT_BREAKER_COOLDOWN_MS;
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ }
13
44
  function resolveSendConfig(raw) {
14
45
  const send = raw['send'];
15
46
  const roles = typeof send === 'object' && send !== null
@@ -31,15 +62,33 @@ function resolveRecallMinSimilarity(value) {
31
62
  ? value
32
63
  : undefined;
33
64
  }
65
+ function resolvePositiveInteger(value, fallback) {
66
+ return typeof value === 'number' && Number.isFinite(value) && value >= 1
67
+ ? Math.floor(value)
68
+ : fallback;
69
+ }
70
+ function resolveBoolean(value, fallback) {
71
+ return typeof value === 'boolean' ? value : fallback;
72
+ }
73
+ function resolveOptionalPositiveInteger(value) {
74
+ return typeof value === 'number' && Number.isFinite(value) && value >= 1
75
+ ? Math.floor(value)
76
+ : undefined;
77
+ }
78
+ function clampPositiveInteger(value, min, max) {
79
+ return Math.min(max, Math.max(min, Math.floor(value)));
80
+ }
34
81
  function resolveConfig(raw) {
35
82
  const c = (raw ?? {});
36
83
  return {
37
84
  baseURL: typeof c['baseURL'] === 'string' ? c['baseURL'] : '',
38
85
  apiKey: typeof c['apiKey'] === 'string' ? c['apiKey'] : '',
39
- tokenBudget: typeof c['tokenBudget'] === 'number' ? c['tokenBudget'] : 2000,
40
- recallTopK: typeof c['recallTopK'] === 'number' ? c['recallTopK'] : 10,
86
+ tokenBudget: resolvePositiveInteger(c['tokenBudget'], DEFAULT_TOKEN_BUDGET),
87
+ recallTopK: resolvePositiveInteger(c['recallTopK'], DEFAULT_RECALL_TOP_K),
41
88
  recallMinSimilarity: resolveRecallMinSimilarity(c['recallMinSimilarity']),
42
- recallTimeout: typeof c['recallTimeout'] === 'number' ? c['recallTimeout'] : 5000,
89
+ recallTimeout: resolvePositiveInteger(c['recallTimeout'], DEFAULT_RECALL_TIMEOUT_MS),
90
+ recallIncludePending: resolveBoolean(c['recallIncludePending'], false),
91
+ includeRelatedMemories: resolveBoolean(c['includeRelatedMemories'], false),
43
92
  ingest: resolveIngestPolicy(c['ingest']),
44
93
  send: resolveSendConfig(c),
45
94
  };
@@ -110,20 +159,27 @@ function buildRecallQuery(event) {
110
159
  parts.push(`[task: ${taskType}]`);
111
160
  return truncate(parts.join('\n'), 600);
112
161
  }
162
+ function toStringArray(value) {
163
+ return Array.isArray(value)
164
+ ? value.filter((item) => typeof item === 'string')
165
+ : [];
166
+ }
113
167
  function buildMemoryBlock(bundle, budget, relatedBundle) {
168
+ if (!bundle || typeof bundle !== 'object')
169
+ return '';
114
170
  const sections = [
115
- { title: 'Behavioural rules', items: bundle.user_rules },
116
- { title: 'Preferences', items: bundle.user_preferences },
117
- { title: 'Task patterns', items: bundle.task_patterns },
118
- { title: 'Workflows', items: bundle.workflows },
119
- { title: 'Project', items: bundle.project },
120
- { title: 'Constraints', items: bundle.constraints },
121
- { title: 'Decisions', items: bundle.decisions },
122
- { title: 'System facts', items: bundle.system_facts },
123
- { title: 'Domain knowledge', items: bundle.domain_knowledge },
171
+ { title: 'Behavioural rules', items: [...toStringArray(bundle.global_user_rules), ...toStringArray(bundle.user_rules)] },
172
+ { title: 'Preferences', items: toStringArray(bundle.user_preferences) },
173
+ { title: 'Task patterns', items: toStringArray(bundle.task_patterns) },
174
+ { title: 'Workflows', items: toStringArray(bundle.workflows) },
175
+ { title: 'Project', items: toStringArray(bundle.project) },
176
+ { title: 'Constraints', items: toStringArray(bundle.constraints) },
177
+ { title: 'Decisions', items: toStringArray(bundle.decisions) },
178
+ { title: 'System facts', items: toStringArray(bundle.system_facts) },
179
+ { title: 'Domain knowledge', items: toStringArray(bundle.domain_knowledge) },
124
180
  ];
125
- if (relatedBundle) {
126
- sections.push({ title: 'Related behavioural rules', items: relatedBundle.user_rules }, { title: 'Related preferences', items: relatedBundle.user_preferences }, { title: 'Related task patterns', items: relatedBundle.task_patterns }, { title: 'Related workflows', items: relatedBundle.workflows }, { title: 'Related project', items: relatedBundle.project }, { title: 'Related constraints', items: relatedBundle.constraints }, { title: 'Related decisions', items: relatedBundle.decisions }, { title: 'Related system facts', items: relatedBundle.system_facts }, { title: 'Related domain knowledge', items: relatedBundle.domain_knowledge });
181
+ if (relatedBundle && typeof relatedBundle === 'object') {
182
+ sections.push({ title: 'Related behavioural rules', items: toStringArray(relatedBundle.user_rules) }, { title: 'Related preferences', items: toStringArray(relatedBundle.user_preferences) }, { title: 'Related task patterns', items: toStringArray(relatedBundle.task_patterns) }, { title: 'Related workflows', items: toStringArray(relatedBundle.workflows) }, { title: 'Related project', items: toStringArray(relatedBundle.project) }, { title: 'Related constraints', items: toStringArray(relatedBundle.constraints) }, { title: 'Related decisions', items: toStringArray(relatedBundle.decisions) }, { title: 'Related system facts', items: toStringArray(relatedBundle.system_facts) }, { title: 'Related domain knowledge', items: toStringArray(relatedBundle.domain_knowledge) });
127
183
  }
128
184
  const intro = 'Use the following as prior context and preferences. If they conflict with current instructions, follow the current instructions.';
129
185
  const lines = [intro];
@@ -137,10 +193,10 @@ function buildMemoryBlock(bundle, budget, relatedBundle) {
137
193
  let tentativeUsed = used + estimateTokens(`\n\n${header}`);
138
194
  const includedItems = [];
139
195
  for (const item of candidates) {
140
- const line = `- ${item}`;
196
+ const line = `- ${truncate(item.replace(/\s+/g, ' ').trim(), MAX_PROMPT_MEMORY_ITEM_CHARS)}`;
141
197
  const cost = estimateTokens(`\n${line}`);
142
198
  if (tentativeUsed + cost > budget) {
143
- return lines.length > 1 ? lines.join('\n') : '';
199
+ break;
144
200
  }
145
201
  includedItems.push(line);
146
202
  tentativeUsed += cost;
@@ -276,7 +332,67 @@ function isTimeoutLikeError(err) {
276
332
  const message = typeof record['message'] === 'string' ? record['message'].toLowerCase() : '';
277
333
  return message.includes('timeout') || message.includes('aborted');
278
334
  }
335
+ async function runGuardedRecall(args) {
336
+ const now = Date.now();
337
+ if (!args.breaker.canAttempt(now)) {
338
+ args.logger?.warn?.(`openclaw-persistio: ${args.operation} skipped; recall circuit breaker open `
339
+ + `for ${args.breaker.remainingMs(now)}ms`);
340
+ return args.fallback;
341
+ }
342
+ try {
343
+ const result = await withPluginDeadline(args.operation, args.timeoutMs + RECALL_GUARD_MARGIN_MS, args.run);
344
+ args.breaker.recordSuccess();
345
+ return result;
346
+ }
347
+ catch (err) {
348
+ const opened = args.breaker.recordFailure();
349
+ args.logger?.warn?.(`openclaw-persistio: ${args.operation} failed open: ${String(err)}`
350
+ + (opened ? `; recall circuit breaker open for ${RECALL_CIRCUIT_BREAKER_COOLDOWN_MS}ms` : ''));
351
+ return args.fallback;
352
+ }
353
+ }
354
+ async function withPluginDeadline(operation, timeoutMs, run) {
355
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
356
+ return run();
357
+ }
358
+ let timeout;
359
+ const deadline = new Promise((_resolve, reject) => {
360
+ timeout = setTimeout(() => {
361
+ const err = new Error(`Persistio ${operation} exceeded plugin deadline after ${timeoutMs}ms`);
362
+ err.name = 'TimeoutError';
363
+ reject(err);
364
+ }, timeoutMs);
365
+ });
366
+ try {
367
+ return await Promise.race([run(), deadline]);
368
+ }
369
+ finally {
370
+ if (timeout)
371
+ clearTimeout(timeout);
372
+ }
373
+ }
279
374
  const PERSISTIO_MEMORY_PATH_PREFIX = 'persistio://memory/';
375
+ function jsonResult(payload) {
376
+ return {
377
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
378
+ details: payload,
379
+ };
380
+ }
381
+ function buildMemorySearchUnavailableResult(error) {
382
+ return {
383
+ results: [],
384
+ disabled: true,
385
+ unavailable: true,
386
+ error,
387
+ warning: 'Persistio memory retrieval is currently unavailable.',
388
+ action: 'Continue without memory for this turn.',
389
+ debug: { backend: 'builtin', provider: 'persistio' },
390
+ };
391
+ }
392
+ function resolveMemorySearchLimit(params) {
393
+ const requested = resolveOptionalPositiveInteger(params.maxResults) ?? params.fallback;
394
+ return clampPositiveInteger(requested, 1, MAX_MEMORY_SEARCH_RESULTS);
395
+ }
280
396
  function createClient(config, recallTopK = config.recallTopK) {
281
397
  return new PersistioClient({ ...config, recallTopK });
282
398
  }
@@ -318,16 +434,23 @@ async function probePersistio(client) {
318
434
  return { ok: false, error: String(err) };
319
435
  }
320
436
  }
321
- function createMemorySearchManager(config) {
437
+ function createMemorySearchManager(config, recallBreaker, logger) {
322
438
  const client = createClient(config);
323
439
  return {
324
440
  async search(query, opts) {
325
441
  if (opts?.sources && !opts.sources.includes('memory')) {
326
442
  return [];
327
443
  }
328
- const recallTopK = typeof opts?.maxResults === 'number' ? opts.maxResults : config.recallTopK;
444
+ const recallTopK = resolveMemorySearchLimit({ maxResults: opts?.maxResults, fallback: config.recallTopK });
329
445
  const recallClient = createClient(config, recallTopK);
330
- const memories = await recallClient.recall(query);
446
+ const memories = await runGuardedRecall({
447
+ operation: 'memory search recall',
448
+ timeoutMs: config.recallTimeout,
449
+ fallback: [],
450
+ breaker: recallBreaker,
451
+ logger,
452
+ run: () => recallClient.recall(query),
453
+ });
331
454
  return memories
332
455
  .map((memory) => {
333
456
  const score = normalizeMemoryScore(memory);
@@ -337,7 +460,7 @@ function createMemorySearchManager(config) {
337
460
  endLine: 1,
338
461
  score,
339
462
  vectorScore: typeof memory.similarity === 'number' ? memory.similarity : undefined,
340
- snippet: truncate(memory.data, 400),
463
+ snippet: truncate(memory.data.replace(/\s+/g, ' ').trim(), MAX_MEMORY_SNIPPET_CHARS),
341
464
  source: 'memory',
342
465
  citation: memory.subject,
343
466
  };
@@ -354,12 +477,17 @@ function createMemorySearchManager(config) {
354
477
  throw new Error(`Persistio memory not found: ${memoryId}`);
355
478
  }
356
479
  const text = formatMemoryDocument(memory);
480
+ const from = params.from ?? 1;
481
+ const lines = text.split('\n');
482
+ const startIndex = Math.max(0, from - 1);
483
+ const requestedLines = params.lines && params.lines > 0 ? params.lines : 40;
484
+ const sliced = lines.slice(startIndex, startIndex + requestedLines).join('\n');
357
485
  return {
358
486
  path: params.relPath,
359
- text,
360
- truncated: false,
361
- from: params.from ?? 1,
362
- lines: params.lines,
487
+ text: truncate(sliced, 2000),
488
+ truncated: startIndex + requestedLines < lines.length || sliced.length > 2000,
489
+ from,
490
+ lines: requestedLines,
363
491
  };
364
492
  },
365
493
  status() {
@@ -384,11 +512,11 @@ function createMemorySearchManager(config) {
384
512
  },
385
513
  };
386
514
  }
387
- function createMemoryRuntime(config) {
515
+ function createMemoryRuntime(config, recallBreaker, logger) {
388
516
  return {
389
517
  async getMemorySearchManager() {
390
518
  return {
391
- manager: createMemorySearchManager(config),
519
+ manager: createMemorySearchManager(config, recallBreaker, logger),
392
520
  };
393
521
  },
394
522
  resolveMemoryBackendConfig() {
@@ -396,6 +524,24 @@ function createMemoryRuntime(config) {
396
524
  },
397
525
  };
398
526
  }
527
+ function buildPersistioMemoryPromptSection({ availableTools }) {
528
+ const hasMemorySearch = availableTools.has('memory_search');
529
+ const hasMemoryGet = availableTools.has('memory_get');
530
+ if (!hasMemorySearch && !hasMemoryGet)
531
+ return [];
532
+ if (hasMemorySearch && hasMemoryGet) {
533
+ return [
534
+ '## Memory Recall',
535
+ 'Persistio is the active memory provider. For prior work, decisions, dates, people, preferences, or todos, use memory_search first and memory_get only for a bounded exact read of a returned persistio://memory/<id> path.',
536
+ '',
537
+ ];
538
+ }
539
+ return [
540
+ '## Memory Recall',
541
+ 'Persistio is the active memory provider. Use the available memory tool for prior work, decisions, dates, people, preferences, or todos when memory context is needed.',
542
+ '',
543
+ ];
544
+ }
399
545
  export default definePluginEntry({
400
546
  id: 'openclaw-persistio',
401
547
  name: 'Persistio Memory',
@@ -407,10 +553,12 @@ export default definePluginEntry({
407
553
  return;
408
554
  }
409
555
  const client = createClient(cfg);
556
+ const recallBreaker = new RecallCircuitBreaker();
410
557
  const sentMessageKeysBySession = new Map();
411
558
  const pendingMessageKeysBySession = new Map();
412
559
  api.registerMemoryCapability({
413
- runtime: createMemoryRuntime(cfg),
560
+ promptBuilder: buildPersistioMemoryPromptSection,
561
+ runtime: createMemoryRuntime(cfg, recallBreaker, api.logger),
414
562
  });
415
563
  // -------------------------------------------------------------------------
416
564
  // before_prompt_build — recall relevant memories and inject into context
@@ -418,18 +566,22 @@ export default definePluginEntry({
418
566
  // Return: { appendSystemContext?: string }
419
567
  // -------------------------------------------------------------------------
420
568
  api.on('before_prompt_build', async (event) => {
421
- try {
422
- const query = buildRecallQuery(event);
423
- const recall = await client.recallBundle(query);
424
- const block = buildMemoryBlock(recall.bundle, cfg.tokenBudget, recall.related_bundle);
425
- if (!block)
426
- return;
427
- return { appendSystemContext: block };
428
- }
429
- catch (err) {
430
- api.logger?.warn?.(`openclaw-persistio: recall error: ${String(err)}`);
431
- }
432
- });
569
+ const query = buildRecallQuery(event);
570
+ const block = await runGuardedRecall({
571
+ operation: 'before_prompt_build recall',
572
+ timeoutMs: cfg.recallTimeout,
573
+ fallback: '',
574
+ breaker: recallBreaker,
575
+ logger: api.logger,
576
+ run: async () => {
577
+ const recall = await client.recallBundle(query, undefined, { includeRelated: cfg.includeRelatedMemories });
578
+ return buildMemoryBlock(recall.bundle, cfg.tokenBudget, cfg.includeRelatedMemories ? recall.related_bundle : undefined);
579
+ },
580
+ });
581
+ if (!block)
582
+ return;
583
+ return { appendSystemContext: block };
584
+ }, { timeoutMs: cfg.recallTimeout + RECALL_GUARD_MARGIN_MS + 250 });
433
585
  // -------------------------------------------------------------------------
434
586
  // agent_end — ingest new turn messages (fire and forget)
435
587
  // Event: { runId?, messages: unknown[], success: boolean, error?, durationMs? }
@@ -527,44 +679,131 @@ export default definePluginEntry({
527
679
  // execute(_id: string, params: unknown): Promise<AgentToolResult>
528
680
  // AgentToolResult: { content: Array<{ type: "text", text: string }>, details: unknown }
529
681
  // -------------------------------------------------------------------------
682
+ const memoryManager = createMemorySearchManager(cfg, recallBreaker, api.logger);
530
683
  api.registerTool({
531
684
  name: 'memory_search',
532
- label: 'Search Memory',
533
- description: 'Search persistent memory for relevant facts from past conversations.',
685
+ label: 'Memory Search',
686
+ description: 'Search Persistio semantic memory. Returns bounded structured results with persistio://memory/<id> paths for memory_get.',
534
687
  parameters: Type.Object({
535
- query: Type.String({ description: 'What to search for' }),
536
- top_k: Type.Optional(Type.Number({ description: 'Max results to return' })),
537
- }),
688
+ query: Type.String({ description: 'Search query' }),
689
+ maxResults: Type.Optional(Type.Number({ description: 'Maximum results to return' })),
690
+ minScore: Type.Optional(Type.Number({ description: 'Optional minimum score from 0 to 1' })),
691
+ corpus: Type.Optional(Type.Union([
692
+ Type.Literal('memory'),
693
+ Type.Literal('wiki'),
694
+ Type.Literal('all'),
695
+ Type.Literal('sessions'),
696
+ ], { description: 'Persistio supports memory corpus results' })),
697
+ }, { additionalProperties: false }),
538
698
  async execute(_id, params) {
539
699
  const p = params;
540
- const overrideTopK = typeof p.top_k === 'number' ? p.top_k : cfg.recallTopK;
541
- const overrideCfg = { ...cfg, recallTopK: overrideTopK };
542
- const c = new PersistioClient(overrideCfg);
543
- const memories = await c.recall(p.query);
544
- const text = memories.length > 0
545
- ? memories.map(m => `- ${m.data} [${m.subject}]`).join('\n')
546
- : 'No memories found.';
547
- return { content: [{ type: 'text', text }], details: null };
700
+ const query = typeof p.query === 'string' ? p.query.trim() : '';
701
+ if (!query) {
702
+ return jsonResult(buildMemorySearchUnavailableResult('memory_search requires a non-empty query'));
703
+ }
704
+ const maxResults = resolveMemorySearchLimit({
705
+ maxResults: p.maxResults,
706
+ fallback: cfg.recallTopK,
707
+ });
708
+ const requestedCorpus = p.corpus ?? 'memory';
709
+ const sources = requestedCorpus === 'sessions' || requestedCorpus === 'wiki'
710
+ ? []
711
+ : ['memory'];
712
+ const startedAt = Date.now();
713
+ try {
714
+ const results = sources.length === 0
715
+ ? []
716
+ : await memoryManager.search(query, {
717
+ maxResults,
718
+ minScore: p.minScore,
719
+ sources,
720
+ });
721
+ return jsonResult({
722
+ results: results.map((result) => ({ ...result, corpus: 'memory' })),
723
+ provider: 'persistio',
724
+ model: undefined,
725
+ fallback: false,
726
+ citations: 'off',
727
+ mode: 'persistio',
728
+ debug: {
729
+ backend: 'builtin',
730
+ effectiveMode: 'persistio',
731
+ requestedCorpus,
732
+ searchMs: Math.max(0, Date.now() - startedAt),
733
+ hits: results.length,
734
+ },
735
+ });
736
+ }
737
+ catch (err) {
738
+ return jsonResult(buildMemorySearchUnavailableResult(String(err)));
739
+ }
740
+ },
741
+ });
742
+ api.registerTool({
743
+ name: 'memory_get',
744
+ label: 'Memory Get',
745
+ description: 'Read a bounded exact Persistio memory document by persistio://memory/<id> path.',
746
+ parameters: Type.Object({
747
+ path: Type.String({ description: 'Memory path returned by memory_search' }),
748
+ from: Type.Optional(Type.Number({ description: 'Starting line, 1-based' })),
749
+ lines: Type.Optional(Type.Number({ description: 'Maximum number of lines to return' })),
750
+ corpus: Type.Optional(Type.Union([
751
+ Type.Literal('memory'),
752
+ Type.Literal('wiki'),
753
+ Type.Literal('all'),
754
+ ])),
755
+ }, { additionalProperties: false }),
756
+ async execute(_id, params) {
757
+ const p = params;
758
+ const path = typeof p.path === 'string' ? p.path : '';
759
+ if (p.corpus === 'wiki') {
760
+ return jsonResult({ path, text: '', disabled: true, error: 'Persistio does not provide a wiki corpus' });
761
+ }
762
+ try {
763
+ return jsonResult(await memoryManager.readFile({
764
+ relPath: path,
765
+ from: resolveOptionalPositiveInteger(p.from),
766
+ lines: resolveOptionalPositiveInteger(p.lines),
767
+ }));
768
+ }
769
+ catch (err) {
770
+ return jsonResult({ path, text: '', disabled: true, error: String(err) });
771
+ }
548
772
  },
549
773
  });
550
774
  api.registerTool({
551
- name: 'memory_add',
552
- label: 'Add Memory',
553
- description: 'Manually store a fact in persistent memory.',
775
+ name: 'persistio_memory_add',
776
+ label: 'Add Persistio Memory',
777
+ description: 'Manually store a fact in Persistio memory.',
554
778
  parameters: Type.Object({
555
779
  data: Type.String({ description: 'The fact to remember' }),
556
780
  subject: Type.String({ description: 'The entity or topic this fact is about' }),
557
781
  }),
558
782
  async execute(_id, params) {
559
783
  const p = params;
560
- await client.addMemory(p.data, p.subject);
784
+ try {
785
+ await client.addMemory(p.data, p.subject);
786
+ }
787
+ catch (err) {
788
+ if (isTimeoutLikeError(err)) {
789
+ api.logger?.warn?.(`openclaw-persistio: persistio_memory_add timeout after ${cfg.ingest.timeoutMs}ms; outcome is ambiguous`);
790
+ return {
791
+ content: [{
792
+ type: 'text',
793
+ text: 'Memory store request timed out; it may still complete. Check persistio_memory_list before retrying.',
794
+ }],
795
+ details: { ambiguous: true },
796
+ };
797
+ }
798
+ throw err;
799
+ }
561
800
  return { content: [{ type: 'text', text: 'Memory stored.' }], details: null };
562
801
  },
563
- });
802
+ }, { optional: true });
564
803
  api.registerTool({
565
- name: 'memory_delete',
566
- label: 'Delete Memory',
567
- description: 'Delete a specific memory by its ID.',
804
+ name: 'persistio_memory_delete',
805
+ label: 'Delete Persistio Memory',
806
+ description: 'Delete a specific Persistio memory by its ID.',
568
807
  parameters: Type.Object({
569
808
  id: Type.String({ description: 'The memory ID to delete' }),
570
809
  }),
@@ -575,14 +814,14 @@ export default definePluginEntry({
575
814
  },
576
815
  }, { optional: true });
577
816
  api.registerTool({
578
- name: 'memory_list',
579
- label: 'List Memories',
580
- description: 'List all stored memories.',
817
+ name: 'persistio_memory_list',
818
+ label: 'List Persistio Memories',
819
+ description: 'List stored Persistio memories.',
581
820
  parameters: Type.Object({}),
582
821
  async execute(_id, _params) {
583
822
  const memories = await client.listMemories();
584
823
  const text = memories.length > 0
585
- ? memories.map(m => `[${m.id}] ${m.data} (${m.subject})`).join('\n')
824
+ ? memories.map(m => `[${m.id}] ${truncate(m.data.replace(/\s+/g, ' ').trim(), MAX_MEMORY_SNIPPET_CHARS)} (${m.subject})`).join('\n')
586
825
  : 'No memories stored.';
587
826
  return { content: [{ type: 'text', text }], details: null };
588
827
  },
@@ -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.6",
5
+ "version": "0.1.8",
6
6
  "kind": "memory",
7
7
  "activation": {
8
8
  "onStartup": true
@@ -10,11 +10,23 @@
10
10
  "contracts": {
11
11
  "tools": [
12
12
  "memory_search",
13
- "memory_add",
14
- "memory_delete",
15
- "memory_list"
13
+ "memory_get",
14
+ "persistio_memory_add",
15
+ "persistio_memory_delete",
16
+ "persistio_memory_list"
16
17
  ]
17
18
  },
19
+ "toolMetadata": {
20
+ "persistio_memory_add": {
21
+ "optional": true
22
+ },
23
+ "persistio_memory_delete": {
24
+ "optional": true
25
+ },
26
+ "persistio_memory_list": {
27
+ "optional": true
28
+ }
29
+ },
18
30
  "configSchema": {
19
31
  "type": "object",
20
32
  "additionalProperties": false,
@@ -26,10 +38,12 @@
26
38
  "type": "string"
27
39
  },
28
40
  "tokenBudget": {
29
- "type": "number"
41
+ "type": "number",
42
+ "minimum": 1
30
43
  },
31
44
  "recallTopK": {
32
- "type": "number"
45
+ "type": "number",
46
+ "minimum": 1
33
47
  },
34
48
  "recallMinSimilarity": {
35
49
  "type": "number",
@@ -37,20 +51,30 @@
37
51
  "maximum": 1
38
52
  },
39
53
  "recallTimeout": {
40
- "type": "number"
54
+ "type": "number",
55
+ "minimum": 1
56
+ },
57
+ "recallIncludePending": {
58
+ "type": "boolean"
59
+ },
60
+ "includeRelatedMemories": {
61
+ "type": "boolean"
41
62
  },
42
63
  "ingest": {
43
64
  "type": "object",
44
65
  "additionalProperties": false,
45
66
  "properties": {
46
67
  "timeoutMs": {
47
- "type": "number"
68
+ "type": "number",
69
+ "minimum": 1
48
70
  },
49
71
  "maxChunkChars": {
50
- "type": "number"
72
+ "type": "number",
73
+ "minimum": 256
51
74
  },
52
75
  "maxChunksPerTurn": {
53
- "type": "number"
76
+ "type": "number",
77
+ "minimum": 1
54
78
  },
55
79
  "skipSubagentSessions": {
56
80
  "type": "boolean"
@@ -60,7 +84,8 @@
60
84
  "additionalProperties": false,
61
85
  "properties": {
62
86
  "maxCharsPerMessage": {
63
- "type": "number"
87
+ "type": "number",
88
+ "minimum": 1
64
89
  }
65
90
  }
66
91
  },
@@ -76,22 +101,28 @@
76
101
  ]
77
102
  },
78
103
  "maxCharsPerMessage": {
79
- "type": "number"
104
+ "type": "number",
105
+ "minimum": 1
80
106
  },
81
107
  "maxCharsAfterFiltering": {
82
- "type": "number"
108
+ "type": "number",
109
+ "minimum": 1
83
110
  },
84
111
  "maxCharsPerTurn": {
85
- "type": "number"
112
+ "type": "number",
113
+ "minimum": 1
86
114
  },
87
115
  "largeBlockThresholdChars": {
88
- "type": "number"
116
+ "type": "number",
117
+ "minimum": 1
89
118
  },
90
119
  "largeBlockThresholdLines": {
91
- "type": "number"
120
+ "type": "number",
121
+ "minimum": 1
92
122
  },
93
123
  "maxTableRows": {
94
- "type": "number"
124
+ "type": "number",
125
+ "minimum": 1
95
126
  }
96
127
  }
97
128
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistio/openclaw-plugin",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "OpenClaw plugin for Persistio \u2014 persistent semantic memory for AI agents",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsc",
45
- "test": "npm run build && node test/config-schema.test.mjs && node test/ingest-policy.test.mjs"
45
+ "test": "npm run build && node test/config-schema.test.mjs && node test/ingest-policy.test.mjs && node test/client-timeout.test.mjs"
46
46
  },
47
47
  "dependencies": {
48
48
  "@sinclair/typebox": "^0.34.0"