@persistio/openclaw-plugin 0.1.7 → 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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  OpenClaw plugin for [Persistio](https://persistio.ai) — persistent semantic memory for AI agents.
4
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.
5
+ Hooks into OpenClaw's `before_prompt_build` and `agent_end` events to recall a small memory context into prompts and ingest new conversation turns after each run. Registers as an OpenClaw memory provider with compatible `memory_search` and `memory_get` tools, plus optional Persistio management tools under the `persistio_*` namespace.
6
6
 
7
7
  ## Requirements
8
8
 
@@ -18,6 +18,14 @@ openclaw gateway restart
18
18
  openclaw plugins inspect openclaw-persistio --runtime --json
19
19
  ```
20
20
 
21
+ To upgrade an existing install, use the same pinned npm source and restart the Gateway:
22
+
23
+ ```bash
24
+ openclaw plugins install npm:@persistio/openclaw-plugin@0.1.8
25
+ openclaw gateway restart
26
+ openclaw plugins inspect openclaw-persistio --runtime --json
27
+ ```
28
+
21
29
  Then register it in your OpenClaw config:
22
30
 
23
31
  ```json
@@ -30,6 +38,9 @@ Then register it in your OpenClaw config:
30
38
  "config": {
31
39
  "baseURL": "https://api.persistio.ai",
32
40
  "apiKey": "your-vault-api-key",
41
+ "tokenBudget": 400,
42
+ "recallTopK": 4,
43
+ "recallTimeout": 1500,
33
44
  "recallMinSimilarity": 0.3,
34
45
  "send": {
35
46
  "roles": {
@@ -40,6 +51,9 @@ Then register it in your OpenClaw config:
40
51
  }
41
52
  }
42
53
  }
54
+ },
55
+ "slots": {
56
+ "memory": "openclaw-persistio"
43
57
  }
44
58
  }
45
59
  }
@@ -51,10 +65,12 @@ Then register it in your OpenClaw config:
51
65
  |---|---|---|---|---|
52
66
  | `baseURL` | string | ✅ | — | Base URL of your Persistio instance |
53
67
  | `apiKey` | string | ✅ | — | Vault API key |
54
- | `tokenBudget` | number | | `2000` | Max tokens to inject into the system prompt |
55
- | `recallTopK` | number | | `10` | Number of memories to retrieve per recall |
68
+ | `tokenBudget` | number | | `400` | Max tokens to inject into the system prompt |
69
+ | `recallTopK` | number | | `4` | Number of memories to retrieve per recall |
56
70
  | `recallMinSimilarity` | number from `0` to `1` | | Persistio server default | Optional semantic recall quality floor |
57
- | `recallTimeout` | number | | `5000` | HTTP timeout for recall requests (ms) |
71
+ | `recallTimeout` | number | | `1500` | HTTP timeout for recall requests (ms) |
72
+ | `recallIncludePending` | boolean | | `false` | Include fresh candidate memories in recall results |
73
+ | `includeRelatedMemories` | boolean | | `false` | Include graph-related memories in prompt recall bundles |
58
74
  | `ingest.timeoutMs` | number | | `30000` | HTTP timeout for ingest requests (ms). Timed-out requests are treated as ambiguous and not retried automatically |
59
75
  | `ingest.maxChunkChars` | number | | `6000` | Maximum characters per chunk sent to Persistio |
60
76
  | `ingest.maxChunksPerTurn` | number | | `12` | Maximum chunks sent from a single OpenClaw turn |
@@ -70,6 +86,8 @@ Then register it in your OpenClaw config:
70
86
 
71
87
  Recall is fail-open by design. If Persistio does not answer within `recallTimeout`, the plugin returns no memory for that turn instead of blocking the OpenClaw lane. After three consecutive recall/search failures it opens a 60 second circuit breaker and skips recall immediately during the cooldown. The plugin also registers a bounded `before_prompt_build` hook timeout; operators can still override this in OpenClaw with `plugins.entries.<id>.hooks.timeouts.before_prompt_build`.
72
88
 
89
+ Prompt recall intentionally defaults to a small direct semantic bundle. `includeRelatedMemories` and `recallIncludePending` are opt-in because graph expansion and fresh candidates increase context size and tail latency on interactive channels.
90
+
73
91
  `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.
74
92
 
75
93
  Assistant ingest is bounded before any network call. By default the plugin skips non-main `agent:*` sessions, collapses oversized code/log/diff/blob/table-shaped assistant content into omission markers, caps assistant ingest per message and per turn, then chunks all ingest content below `ingest.maxChunkChars`. Persistio still performs server-side extraction and curation; the plugin only enforces a deterministic transport-safe shape.
@@ -78,10 +96,11 @@ Assistant ingest is bounded before any network call. By default the plugin skips
78
96
 
79
97
  | Tool | Description |
80
98
  |---|---|
81
- | `memory_search` | Search memories by semantic query |
82
- | `memory_add` | Manually store a fact |
83
- | `memory_delete` | Delete a memory by ID |
84
- | `memory_list` | List all memories in the vault |
99
+ | `memory_search` | Required OpenClaw-compatible semantic memory search. Returns bounded structured results with `persistio://memory/<id>` paths |
100
+ | `memory_get` | Required OpenClaw-compatible exact memory read for paths returned by `memory_search` |
101
+ | `persistio_memory_add` | Optional manual fact store |
102
+ | `persistio_memory_delete` | Optional memory deletion by ID |
103
+ | `persistio_memory_list` | Optional vault memory listing |
85
104
 
86
105
  ## License
87
106
 
package/dist/client.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface PersistioConfig {
6
6
  recallTopK: number;
7
7
  recallMinSimilarity?: number;
8
8
  recallTimeout: number;
9
+ recallIncludePending: boolean;
10
+ includeRelatedMemories: boolean;
9
11
  ingest: PersistioIngestPolicy;
10
12
  send: PersistioSendConfig;
11
13
  }
@@ -44,6 +46,9 @@ export interface RecallBundleResponse {
44
46
  bundle: RecallBundle;
45
47
  related_bundle?: RecallBundle;
46
48
  }
49
+ export interface RecallBundleOptions {
50
+ includeRelated?: boolean;
51
+ }
47
52
  export declare class PersistioTimeoutError extends Error {
48
53
  constructor(operation: string, timeoutMs: number);
49
54
  }
@@ -53,12 +58,14 @@ export declare class PersistioClient {
53
58
  private readonly recallTopK;
54
59
  private readonly recallMinSimilarity?;
55
60
  private readonly recallTimeout;
61
+ private readonly recallIncludePending;
62
+ private readonly includeRelatedMemories;
56
63
  private readonly ingestTimeout;
57
64
  private readonly writeTimeout;
58
65
  constructor(config: PersistioConfig);
59
66
  private headers;
60
67
  recall(query: string): Promise<PersistioMemory[]>;
61
- recallBundle(query: string, topK?: number): Promise<RecallBundleResponse>;
68
+ recallBundle(query: string, topK?: number, options?: RecallBundleOptions): Promise<RecallBundleResponse>;
62
69
  ingest(sessionId: string, chunks: Array<{
63
70
  role: string;
64
71
  content: string;
package/dist/client.js CHANGED
@@ -10,6 +10,8 @@ export class PersistioClient {
10
10
  recallTopK;
11
11
  recallMinSimilarity;
12
12
  recallTimeout;
13
+ recallIncludePending;
14
+ includeRelatedMemories;
13
15
  ingestTimeout;
14
16
  writeTimeout;
15
17
  constructor(config) {
@@ -18,6 +20,8 @@ export class PersistioClient {
18
20
  this.recallTopK = config.recallTopK;
19
21
  this.recallMinSimilarity = config.recallMinSimilarity;
20
22
  this.recallTimeout = config.recallTimeout;
23
+ this.recallIncludePending = config.recallIncludePending;
24
+ this.includeRelatedMemories = config.includeRelatedMemories;
21
25
  this.ingestTimeout = config.ingest.timeoutMs;
22
26
  this.writeTimeout = config.ingest.timeoutMs;
23
27
  }
@@ -29,7 +33,11 @@ export class PersistioClient {
29
33
  }
30
34
  async recall(query) {
31
35
  return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
32
- const body = { query, top_k: this.recallTopK, include_pending: true };
36
+ const body = {
37
+ query,
38
+ top_k: this.recallTopK,
39
+ include_pending: this.recallIncludePending
40
+ };
33
41
  if (typeof this.recallMinSimilarity === 'number') {
34
42
  body.min_similarity = this.recallMinSimilarity;
35
43
  }
@@ -45,9 +53,14 @@ export class PersistioClient {
45
53
  return data.memories ?? [];
46
54
  });
47
55
  }
48
- async recallBundle(query, topK) {
56
+ async recallBundle(query, topK, options = {}) {
49
57
  return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
50
- const body = { query, top_k: topK ?? this.recallTopK, include_pending: true };
58
+ const body = {
59
+ query,
60
+ top_k: topK ?? this.recallTopK,
61
+ include_pending: this.recallIncludePending,
62
+ include_related: options.includeRelated ?? this.includeRelatedMemories
63
+ };
51
64
  if (typeof this.recallMinSimilarity === 'number') {
52
65
  body.min_similarity = this.recallMinSimilarity;
53
66
  }
package/dist/index.js CHANGED
@@ -13,6 +13,12 @@ const MAX_SENT_KEYS_PER_SESSION = 2000;
13
13
  const RECALL_CIRCUIT_BREAKER_FAILURE_THRESHOLD = 3;
14
14
  const RECALL_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
15
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;
16
22
  class RecallCircuitBreaker {
17
23
  consecutiveFailures = 0;
18
24
  openedUntil = 0;
@@ -61,15 +67,28 @@ function resolvePositiveInteger(value, fallback) {
61
67
  ? Math.floor(value)
62
68
  : fallback;
63
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
+ }
64
81
  function resolveConfig(raw) {
65
82
  const c = (raw ?? {});
66
83
  return {
67
84
  baseURL: typeof c['baseURL'] === 'string' ? c['baseURL'] : '',
68
85
  apiKey: typeof c['apiKey'] === 'string' ? c['apiKey'] : '',
69
- tokenBudget: resolvePositiveInteger(c['tokenBudget'], 2000),
70
- recallTopK: resolvePositiveInteger(c['recallTopK'], 10),
86
+ tokenBudget: resolvePositiveInteger(c['tokenBudget'], DEFAULT_TOKEN_BUDGET),
87
+ recallTopK: resolvePositiveInteger(c['recallTopK'], DEFAULT_RECALL_TOP_K),
71
88
  recallMinSimilarity: resolveRecallMinSimilarity(c['recallMinSimilarity']),
72
- recallTimeout: resolvePositiveInteger(c['recallTimeout'], 5000),
89
+ recallTimeout: resolvePositiveInteger(c['recallTimeout'], DEFAULT_RECALL_TIMEOUT_MS),
90
+ recallIncludePending: resolveBoolean(c['recallIncludePending'], false),
91
+ includeRelatedMemories: resolveBoolean(c['includeRelatedMemories'], false),
73
92
  ingest: resolveIngestPolicy(c['ingest']),
74
93
  send: resolveSendConfig(c),
75
94
  };
@@ -149,7 +168,7 @@ function buildMemoryBlock(bundle, budget, relatedBundle) {
149
168
  if (!bundle || typeof bundle !== 'object')
150
169
  return '';
151
170
  const sections = [
152
- { title: 'Behavioural rules', items: toStringArray(bundle.user_rules) },
171
+ { title: 'Behavioural rules', items: [...toStringArray(bundle.global_user_rules), ...toStringArray(bundle.user_rules)] },
153
172
  { title: 'Preferences', items: toStringArray(bundle.user_preferences) },
154
173
  { title: 'Task patterns', items: toStringArray(bundle.task_patterns) },
155
174
  { title: 'Workflows', items: toStringArray(bundle.workflows) },
@@ -174,10 +193,10 @@ function buildMemoryBlock(bundle, budget, relatedBundle) {
174
193
  let tentativeUsed = used + estimateTokens(`\n\n${header}`);
175
194
  const includedItems = [];
176
195
  for (const item of candidates) {
177
- const line = `- ${item}`;
196
+ const line = `- ${truncate(item.replace(/\s+/g, ' ').trim(), MAX_PROMPT_MEMORY_ITEM_CHARS)}`;
178
197
  const cost = estimateTokens(`\n${line}`);
179
198
  if (tentativeUsed + cost > budget) {
180
- return lines.length > 1 ? lines.join('\n') : '';
199
+ break;
181
200
  }
182
201
  includedItems.push(line);
183
202
  tentativeUsed += cost;
@@ -353,6 +372,27 @@ async function withPluginDeadline(operation, timeoutMs, run) {
353
372
  }
354
373
  }
355
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
+ }
356
396
  function createClient(config, recallTopK = config.recallTopK) {
357
397
  return new PersistioClient({ ...config, recallTopK });
358
398
  }
@@ -401,7 +441,7 @@ function createMemorySearchManager(config, recallBreaker, logger) {
401
441
  if (opts?.sources && !opts.sources.includes('memory')) {
402
442
  return [];
403
443
  }
404
- const recallTopK = typeof opts?.maxResults === 'number' ? opts.maxResults : config.recallTopK;
444
+ const recallTopK = resolveMemorySearchLimit({ maxResults: opts?.maxResults, fallback: config.recallTopK });
405
445
  const recallClient = createClient(config, recallTopK);
406
446
  const memories = await runGuardedRecall({
407
447
  operation: 'memory search recall',
@@ -420,7 +460,7 @@ function createMemorySearchManager(config, recallBreaker, logger) {
420
460
  endLine: 1,
421
461
  score,
422
462
  vectorScore: typeof memory.similarity === 'number' ? memory.similarity : undefined,
423
- snippet: truncate(memory.data, 400),
463
+ snippet: truncate(memory.data.replace(/\s+/g, ' ').trim(), MAX_MEMORY_SNIPPET_CHARS),
424
464
  source: 'memory',
425
465
  citation: memory.subject,
426
466
  };
@@ -437,12 +477,17 @@ function createMemorySearchManager(config, recallBreaker, logger) {
437
477
  throw new Error(`Persistio memory not found: ${memoryId}`);
438
478
  }
439
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');
440
485
  return {
441
486
  path: params.relPath,
442
- text,
443
- truncated: false,
444
- from: params.from ?? 1,
445
- lines: params.lines,
487
+ text: truncate(sliced, 2000),
488
+ truncated: startIndex + requestedLines < lines.length || sliced.length > 2000,
489
+ from,
490
+ lines: requestedLines,
446
491
  };
447
492
  },
448
493
  status() {
@@ -479,6 +524,24 @@ function createMemoryRuntime(config, recallBreaker, logger) {
479
524
  },
480
525
  };
481
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
+ }
482
545
  export default definePluginEntry({
483
546
  id: 'openclaw-persistio',
484
547
  name: 'Persistio Memory',
@@ -494,6 +557,7 @@ export default definePluginEntry({
494
557
  const sentMessageKeysBySession = new Map();
495
558
  const pendingMessageKeysBySession = new Map();
496
559
  api.registerMemoryCapability({
560
+ promptBuilder: buildPersistioMemoryPromptSection,
497
561
  runtime: createMemoryRuntime(cfg, recallBreaker, api.logger),
498
562
  });
499
563
  // -------------------------------------------------------------------------
@@ -510,8 +574,8 @@ export default definePluginEntry({
510
574
  breaker: recallBreaker,
511
575
  logger: api.logger,
512
576
  run: async () => {
513
- const recall = await client.recallBundle(query);
514
- return buildMemoryBlock(recall.bundle, cfg.tokenBudget, recall.related_bundle);
577
+ const recall = await client.recallBundle(query, undefined, { includeRelated: cfg.includeRelatedMemories });
578
+ return buildMemoryBlock(recall.bundle, cfg.tokenBudget, cfg.includeRelatedMemories ? recall.related_bundle : undefined);
515
579
  },
516
580
  });
517
581
  if (!block)
@@ -615,37 +679,102 @@ export default definePluginEntry({
615
679
  // execute(_id: string, params: unknown): Promise<AgentToolResult>
616
680
  // AgentToolResult: { content: Array<{ type: "text", text: string }>, details: unknown }
617
681
  // -------------------------------------------------------------------------
682
+ const memoryManager = createMemorySearchManager(cfg, recallBreaker, api.logger);
618
683
  api.registerTool({
619
684
  name: 'memory_search',
620
- label: 'Search Memory',
621
- 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.',
622
687
  parameters: Type.Object({
623
- query: Type.String({ description: 'What to search for' }),
624
- top_k: Type.Optional(Type.Number({ description: 'Max results to return' })),
625
- }),
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 }),
626
698
  async execute(_id, params) {
627
699
  const p = params;
628
- const overrideTopK = resolvePositiveInteger(p.top_k, cfg.recallTopK);
629
- const overrideCfg = { ...cfg, recallTopK: overrideTopK };
630
- const recallClient = createClient(overrideCfg);
631
- const memories = await runGuardedRecall({
632
- operation: 'memory_search tool recall',
633
- timeoutMs: cfg.recallTimeout,
634
- fallback: [],
635
- breaker: recallBreaker,
636
- logger: api.logger,
637
- run: () => recallClient.recall(p.query),
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,
638
707
  });
639
- const text = memories.length > 0
640
- ? memories.map(m => `- ${m.data} [${m.subject}]`).join('\n')
641
- : 'No memories found.';
642
- return { content: [{ type: 'text', text }], details: null };
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
+ }
643
740
  },
644
741
  });
645
742
  api.registerTool({
646
- name: 'memory_add',
647
- label: 'Add Memory',
648
- description: 'Manually store a fact in persistent memory.',
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
+ }
772
+ },
773
+ });
774
+ api.registerTool({
775
+ name: 'persistio_memory_add',
776
+ label: 'Add Persistio Memory',
777
+ description: 'Manually store a fact in Persistio memory.',
649
778
  parameters: Type.Object({
650
779
  data: Type.String({ description: 'The fact to remember' }),
651
780
  subject: Type.String({ description: 'The entity or topic this fact is about' }),
@@ -657,11 +786,11 @@ export default definePluginEntry({
657
786
  }
658
787
  catch (err) {
659
788
  if (isTimeoutLikeError(err)) {
660
- api.logger?.warn?.(`openclaw-persistio: memory_add timeout after ${cfg.ingest.timeoutMs}ms; outcome is ambiguous`);
789
+ api.logger?.warn?.(`openclaw-persistio: persistio_memory_add timeout after ${cfg.ingest.timeoutMs}ms; outcome is ambiguous`);
661
790
  return {
662
791
  content: [{
663
792
  type: 'text',
664
- text: 'Memory store request timed out; it may still complete. Check memory_list before retrying.',
793
+ text: 'Memory store request timed out; it may still complete. Check persistio_memory_list before retrying.',
665
794
  }],
666
795
  details: { ambiguous: true },
667
796
  };
@@ -670,11 +799,11 @@ export default definePluginEntry({
670
799
  }
671
800
  return { content: [{ type: 'text', text: 'Memory stored.' }], details: null };
672
801
  },
673
- });
802
+ }, { optional: true });
674
803
  api.registerTool({
675
- name: 'memory_delete',
676
- label: 'Delete Memory',
677
- 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.',
678
807
  parameters: Type.Object({
679
808
  id: Type.String({ description: 'The memory ID to delete' }),
680
809
  }),
@@ -685,14 +814,14 @@ export default definePluginEntry({
685
814
  },
686
815
  }, { optional: true });
687
816
  api.registerTool({
688
- name: 'memory_list',
689
- label: 'List Memories',
690
- description: 'List all stored memories.',
817
+ name: 'persistio_memory_list',
818
+ label: 'List Persistio Memories',
819
+ description: 'List stored Persistio memories.',
691
820
  parameters: Type.Object({}),
692
821
  async execute(_id, _params) {
693
822
  const memories = await client.listMemories();
694
823
  const text = memories.length > 0
695
- ? 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')
696
825
  : 'No memories stored.';
697
826
  return { content: [{ type: 'text', text }], details: null };
698
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.7",
5
+ "version": "0.1.8",
6
6
  "kind": "memory",
7
7
  "activation": {
8
8
  "onStartup": true
@@ -10,16 +10,20 @@
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
  },
18
19
  "toolMetadata": {
19
- "memory_delete": {
20
+ "persistio_memory_add": {
20
21
  "optional": true
21
22
  },
22
- "memory_list": {
23
+ "persistio_memory_delete": {
24
+ "optional": true
25
+ },
26
+ "persistio_memory_list": {
23
27
  "optional": true
24
28
  }
25
29
  },
@@ -50,6 +54,12 @@
50
54
  "type": "number",
51
55
  "minimum": 1
52
56
  },
57
+ "recallIncludePending": {
58
+ "type": "boolean"
59
+ },
60
+ "includeRelatedMemories": {
61
+ "type": "boolean"
62
+ },
53
63
  "ingest": {
54
64
  "type": "object",
55
65
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@persistio/openclaw-plugin",
3
- "version": "0.1.7",
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",
package/src/client.ts CHANGED
@@ -7,6 +7,8 @@ export interface PersistioConfig {
7
7
  recallTopK: number;
8
8
  recallMinSimilarity?: number;
9
9
  recallTimeout: number;
10
+ recallIncludePending: boolean;
11
+ includeRelatedMemories: boolean;
10
12
  ingest: PersistioIngestPolicy;
11
13
  send: PersistioSendConfig;
12
14
  }
@@ -52,6 +54,10 @@ export interface RecallBundleResponse {
52
54
  related_bundle?: RecallBundle;
53
55
  }
54
56
 
57
+ export interface RecallBundleOptions {
58
+ includeRelated?: boolean;
59
+ }
60
+
55
61
  export class PersistioTimeoutError extends Error {
56
62
  constructor(operation: string, timeoutMs: number) {
57
63
  super(`Persistio ${operation} timed out after ${timeoutMs}ms`);
@@ -65,6 +71,8 @@ export class PersistioClient {
65
71
  private readonly recallTopK: number;
66
72
  private readonly recallMinSimilarity?: number;
67
73
  private readonly recallTimeout: number;
74
+ private readonly recallIncludePending: boolean;
75
+ private readonly includeRelatedMemories: boolean;
68
76
  private readonly ingestTimeout: number;
69
77
  private readonly writeTimeout: number;
70
78
 
@@ -74,6 +82,8 @@ export class PersistioClient {
74
82
  this.recallTopK = config.recallTopK;
75
83
  this.recallMinSimilarity = config.recallMinSimilarity;
76
84
  this.recallTimeout = config.recallTimeout;
85
+ this.recallIncludePending = config.recallIncludePending;
86
+ this.includeRelatedMemories = config.includeRelatedMemories;
77
87
  this.ingestTimeout = config.ingest.timeoutMs;
78
88
  this.writeTimeout = config.ingest.timeoutMs;
79
89
  }
@@ -87,7 +97,11 @@ export class PersistioClient {
87
97
 
88
98
  async recall(query: string): Promise<PersistioMemory[]> {
89
99
  return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
90
- const body: Record<string, unknown> = { query, top_k: this.recallTopK, include_pending: true };
100
+ const body: Record<string, unknown> = {
101
+ query,
102
+ top_k: this.recallTopK,
103
+ include_pending: this.recallIncludePending
104
+ };
91
105
  if (typeof this.recallMinSimilarity === 'number') {
92
106
  body.min_similarity = this.recallMinSimilarity;
93
107
  }
@@ -104,9 +118,14 @@ export class PersistioClient {
104
118
  });
105
119
  }
106
120
 
107
- async recallBundle(query: string, topK?: number): Promise<RecallBundleResponse> {
121
+ async recallBundle(query: string, topK?: number, options: RecallBundleOptions = {}): Promise<RecallBundleResponse> {
108
122
  return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
109
- const body: Record<string, unknown> = { query, top_k: topK ?? this.recallTopK, include_pending: true };
123
+ const body: Record<string, unknown> = {
124
+ query,
125
+ top_k: topK ?? this.recallTopK,
126
+ include_pending: this.recallIncludePending,
127
+ include_related: options.includeRelated ?? this.includeRelatedMemories
128
+ };
110
129
  if (typeof this.recallMinSimilarity === 'number') {
111
130
  body.min_similarity = this.recallMinSimilarity;
112
131
  }
package/src/index.ts CHANGED
@@ -32,6 +32,12 @@ const MAX_SENT_KEYS_PER_SESSION = 2000;
32
32
  const RECALL_CIRCUIT_BREAKER_FAILURE_THRESHOLD = 3;
33
33
  const RECALL_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
34
34
  const RECALL_GUARD_MARGIN_MS = 250;
35
+ const DEFAULT_TOKEN_BUDGET = 400;
36
+ const DEFAULT_RECALL_TOP_K = 4;
37
+ const DEFAULT_RECALL_TIMEOUT_MS = 1500;
38
+ const MAX_MEMORY_SEARCH_RESULTS = 8;
39
+ const MAX_PROMPT_MEMORY_ITEM_CHARS = 500;
40
+ const MAX_MEMORY_SNIPPET_CHARS = 360;
35
41
 
36
42
  interface PluginLogger {
37
43
  debug?: (message: string) => void;
@@ -95,15 +101,31 @@ function resolvePositiveInteger(value: unknown, fallback: number): number {
95
101
  : fallback;
96
102
  }
97
103
 
104
+ function resolveBoolean(value: unknown, fallback: boolean): boolean {
105
+ return typeof value === 'boolean' ? value : fallback;
106
+ }
107
+
108
+ function resolveOptionalPositiveInteger(value: unknown): number | undefined {
109
+ return typeof value === 'number' && Number.isFinite(value) && value >= 1
110
+ ? Math.floor(value)
111
+ : undefined;
112
+ }
113
+
114
+ function clampPositiveInteger(value: number, min: number, max: number): number {
115
+ return Math.min(max, Math.max(min, Math.floor(value)));
116
+ }
117
+
98
118
  function resolveConfig(raw: unknown): PersistioConfig {
99
119
  const c = (raw ?? {}) as Record<string, unknown>;
100
120
  return {
101
121
  baseURL: typeof c['baseURL'] === 'string' ? c['baseURL'] : '',
102
122
  apiKey: typeof c['apiKey'] === 'string' ? c['apiKey'] : '',
103
- tokenBudget: resolvePositiveInteger(c['tokenBudget'], 2000),
104
- recallTopK: resolvePositiveInteger(c['recallTopK'], 10),
123
+ tokenBudget: resolvePositiveInteger(c['tokenBudget'], DEFAULT_TOKEN_BUDGET),
124
+ recallTopK: resolvePositiveInteger(c['recallTopK'], DEFAULT_RECALL_TOP_K),
105
125
  recallMinSimilarity: resolveRecallMinSimilarity(c['recallMinSimilarity']),
106
- recallTimeout: resolvePositiveInteger(c['recallTimeout'], 5000),
126
+ recallTimeout: resolvePositiveInteger(c['recallTimeout'], DEFAULT_RECALL_TIMEOUT_MS),
127
+ recallIncludePending: resolveBoolean(c['recallIncludePending'], false),
128
+ includeRelatedMemories: resolveBoolean(c['includeRelatedMemories'], false),
107
129
  ingest: resolveIngestPolicy(c['ingest']),
108
130
  send: resolveSendConfig(c),
109
131
  };
@@ -190,7 +212,7 @@ function buildMemoryBlock(bundle: RecallBundle | undefined, budget: number, rela
190
212
  if (!bundle || typeof bundle !== 'object') return '';
191
213
 
192
214
  const sections: Array<{ title: string; items: string[] }> = [
193
- { title: 'Behavioural rules', items: toStringArray(bundle.user_rules) },
215
+ { title: 'Behavioural rules', items: [...toStringArray(bundle.global_user_rules), ...toStringArray(bundle.user_rules)] },
194
216
  { title: 'Preferences', items: toStringArray(bundle.user_preferences) },
195
217
  { title: 'Task patterns', items: toStringArray(bundle.task_patterns) },
196
218
  { title: 'Workflows', items: toStringArray(bundle.workflows) },
@@ -228,10 +250,10 @@ function buildMemoryBlock(bundle: RecallBundle | undefined, budget: number, rela
228
250
  const includedItems: string[] = [];
229
251
 
230
252
  for (const item of candidates) {
231
- const line = `- ${item}`;
253
+ const line = `- ${truncate(item.replace(/\s+/g, ' ').trim(), MAX_PROMPT_MEMORY_ITEM_CHARS)}`;
232
254
  const cost = estimateTokens(`\n${line}`);
233
255
  if (tentativeUsed + cost > budget) {
234
- return lines.length > 1 ? lines.join('\n') : '';
256
+ break;
235
257
  }
236
258
  includedItems.push(line);
237
259
  tentativeUsed += cost;
@@ -436,6 +458,30 @@ async function withPluginDeadline<T>(
436
458
 
437
459
  const PERSISTIO_MEMORY_PATH_PREFIX = 'persistio://memory/';
438
460
 
461
+ function jsonResult(payload: unknown) {
462
+ return {
463
+ content: [{ type: 'text' as const, text: JSON.stringify(payload, null, 2) }],
464
+ details: payload,
465
+ };
466
+ }
467
+
468
+ function buildMemorySearchUnavailableResult(error: string) {
469
+ return {
470
+ results: [],
471
+ disabled: true,
472
+ unavailable: true,
473
+ error,
474
+ warning: 'Persistio memory retrieval is currently unavailable.',
475
+ action: 'Continue without memory for this turn.',
476
+ debug: { backend: 'builtin', provider: 'persistio' },
477
+ };
478
+ }
479
+
480
+ function resolveMemorySearchLimit(params: { maxResults?: number; fallback: number }): number {
481
+ const requested = resolveOptionalPositiveInteger(params.maxResults) ?? params.fallback;
482
+ return clampPositiveInteger(requested, 1, MAX_MEMORY_SEARCH_RESULTS);
483
+ }
484
+
439
485
  function createClient(config: PersistioConfig, recallTopK = config.recallTopK): PersistioClient {
440
486
  return new PersistioClient({ ...config, recallTopK });
441
487
  }
@@ -507,7 +553,7 @@ function createMemorySearchManager(
507
553
  return [];
508
554
  }
509
555
 
510
- const recallTopK = typeof opts?.maxResults === 'number' ? opts.maxResults : config.recallTopK;
556
+ const recallTopK = resolveMemorySearchLimit({ maxResults: opts?.maxResults, fallback: config.recallTopK });
511
557
  const recallClient = createClient(config, recallTopK);
512
558
  const memories = await runGuardedRecall({
513
559
  operation: 'memory search recall',
@@ -527,7 +573,7 @@ function createMemorySearchManager(
527
573
  endLine: 1,
528
574
  score,
529
575
  vectorScore: typeof memory.similarity === 'number' ? memory.similarity : undefined,
530
- snippet: truncate(memory.data, 400),
576
+ snippet: truncate(memory.data.replace(/\s+/g, ' ').trim(), MAX_MEMORY_SNIPPET_CHARS),
531
577
  source: 'memory',
532
578
  citation: memory.subject,
533
579
  };
@@ -551,12 +597,17 @@ function createMemorySearchManager(
551
597
  }
552
598
 
553
599
  const text = formatMemoryDocument(memory);
600
+ const from = params.from ?? 1;
601
+ const lines = text.split('\n');
602
+ const startIndex = Math.max(0, from - 1);
603
+ const requestedLines = params.lines && params.lines > 0 ? params.lines : 40;
604
+ const sliced = lines.slice(startIndex, startIndex + requestedLines).join('\n');
554
605
  return {
555
606
  path: params.relPath,
556
- text,
557
- truncated: false,
558
- from: params.from ?? 1,
559
- lines: params.lines,
607
+ text: truncate(sliced, 2000),
608
+ truncated: startIndex + requestedLines < lines.length || sliced.length > 2000,
609
+ from,
610
+ lines: requestedLines,
560
611
  };
561
612
  },
562
613
 
@@ -599,6 +650,26 @@ function createMemoryRuntime(config: PersistioConfig, recallBreaker: RecallCircu
599
650
  };
600
651
  }
601
652
 
653
+ function buildPersistioMemoryPromptSection({ availableTools }: { availableTools: Set<string> }): string[] {
654
+ const hasMemorySearch = availableTools.has('memory_search');
655
+ const hasMemoryGet = availableTools.has('memory_get');
656
+ if (!hasMemorySearch && !hasMemoryGet) return [];
657
+
658
+ if (hasMemorySearch && hasMemoryGet) {
659
+ return [
660
+ '## Memory Recall',
661
+ '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.',
662
+ '',
663
+ ];
664
+ }
665
+
666
+ return [
667
+ '## Memory Recall',
668
+ '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.',
669
+ '',
670
+ ];
671
+ }
672
+
602
673
  export default definePluginEntry({
603
674
  id: 'openclaw-persistio',
604
675
  name: 'Persistio Memory',
@@ -617,6 +688,7 @@ export default definePluginEntry({
617
688
  const sentMessageKeysBySession = new Map<string, SessionMessageKeyStore>();
618
689
  const pendingMessageKeysBySession = new Map<string, SessionMessageKeyStore>();
619
690
  api.registerMemoryCapability({
691
+ promptBuilder: buildPersistioMemoryPromptSection,
620
692
  runtime: createMemoryRuntime(cfg, recallBreaker, api.logger),
621
693
  });
622
694
 
@@ -634,8 +706,12 @@ export default definePluginEntry({
634
706
  breaker: recallBreaker,
635
707
  logger: api.logger,
636
708
  run: async () => {
637
- const recall = await client.recallBundle(query);
638
- return buildMemoryBlock(recall.bundle, cfg.tokenBudget, recall.related_bundle);
709
+ const recall = await client.recallBundle(query, undefined, { includeRelated: cfg.includeRelatedMemories });
710
+ return buildMemoryBlock(
711
+ recall.bundle,
712
+ cfg.tokenBudget,
713
+ cfg.includeRelatedMemories ? recall.related_bundle : undefined,
714
+ );
639
715
  },
640
716
  });
641
717
  if (!block) return;
@@ -744,38 +820,111 @@ export default definePluginEntry({
744
820
  // AgentToolResult: { content: Array<{ type: "text", text: string }>, details: unknown }
745
821
  // -------------------------------------------------------------------------
746
822
 
823
+ const memoryManager = createMemorySearchManager(cfg, recallBreaker, api.logger);
824
+
747
825
  api.registerTool({
748
826
  name: 'memory_search',
749
- label: 'Search Memory',
750
- description: 'Search persistent memory for relevant facts from past conversations.',
827
+ label: 'Memory Search',
828
+ description: 'Search Persistio semantic memory. Returns bounded structured results with persistio://memory/<id> paths for memory_get.',
751
829
  parameters: Type.Object({
752
- query: Type.String({ description: 'What to search for' }),
753
- top_k: Type.Optional(Type.Number({ description: 'Max results to return' })),
754
- }),
830
+ query: Type.String({ description: 'Search query' }),
831
+ maxResults: Type.Optional(Type.Number({ description: 'Maximum results to return' })),
832
+ minScore: Type.Optional(Type.Number({ description: 'Optional minimum score from 0 to 1' })),
833
+ corpus: Type.Optional(Type.Union([
834
+ Type.Literal('memory'),
835
+ Type.Literal('wiki'),
836
+ Type.Literal('all'),
837
+ Type.Literal('sessions'),
838
+ ], { description: 'Persistio supports memory corpus results' })),
839
+ }, { additionalProperties: false }),
755
840
  async execute(_id, params) {
756
- const p = params as { query: string; top_k?: number };
757
- const overrideTopK = resolvePositiveInteger(p.top_k, cfg.recallTopK);
758
- const overrideCfg = { ...cfg, recallTopK: overrideTopK };
759
- const recallClient = createClient(overrideCfg);
760
- const memories = await runGuardedRecall({
761
- operation: 'memory_search tool recall',
762
- timeoutMs: cfg.recallTimeout,
763
- fallback: [],
764
- breaker: recallBreaker,
765
- logger: api.logger,
766
- run: () => recallClient.recall(p.query),
841
+ const p = params as {
842
+ query?: string;
843
+ maxResults?: number;
844
+ minScore?: number;
845
+ corpus?: 'memory' | 'wiki' | 'all' | 'sessions';
846
+ };
847
+ const query = typeof p.query === 'string' ? p.query.trim() : '';
848
+ if (!query) {
849
+ return jsonResult(buildMemorySearchUnavailableResult('memory_search requires a non-empty query'));
850
+ }
851
+
852
+ const maxResults = resolveMemorySearchLimit({
853
+ maxResults: p.maxResults,
854
+ fallback: cfg.recallTopK,
767
855
  });
768
- const text = memories.length > 0
769
- ? memories.map(m => `- ${m.data} [${m.subject}]`).join('\n')
770
- : 'No memories found.';
771
- return { content: [{ type: 'text' as const, text }], details: null };
856
+ const requestedCorpus = p.corpus ?? 'memory';
857
+ const sources: Array<'memory' | 'sessions'> = requestedCorpus === 'sessions' || requestedCorpus === 'wiki'
858
+ ? []
859
+ : ['memory' as const];
860
+ const startedAt = Date.now();
861
+
862
+ try {
863
+ const results: MemorySearchResult[] = sources.length === 0
864
+ ? []
865
+ : await memoryManager.search(query, {
866
+ maxResults,
867
+ minScore: p.minScore,
868
+ sources,
869
+ });
870
+
871
+ return jsonResult({
872
+ results: results.map((result) => ({ ...result, corpus: 'memory' })),
873
+ provider: 'persistio',
874
+ model: undefined,
875
+ fallback: false,
876
+ citations: 'off',
877
+ mode: 'persistio',
878
+ debug: {
879
+ backend: 'builtin',
880
+ effectiveMode: 'persistio',
881
+ requestedCorpus,
882
+ searchMs: Math.max(0, Date.now() - startedAt),
883
+ hits: results.length,
884
+ },
885
+ });
886
+ } catch (err) {
887
+ return jsonResult(buildMemorySearchUnavailableResult(String(err)));
888
+ }
772
889
  },
773
890
  });
774
891
 
775
892
  api.registerTool({
776
- name: 'memory_add',
777
- label: 'Add Memory',
778
- description: 'Manually store a fact in persistent memory.',
893
+ name: 'memory_get',
894
+ label: 'Memory Get',
895
+ description: 'Read a bounded exact Persistio memory document by persistio://memory/<id> path.',
896
+ parameters: Type.Object({
897
+ path: Type.String({ description: 'Memory path returned by memory_search' }),
898
+ from: Type.Optional(Type.Number({ description: 'Starting line, 1-based' })),
899
+ lines: Type.Optional(Type.Number({ description: 'Maximum number of lines to return' })),
900
+ corpus: Type.Optional(Type.Union([
901
+ Type.Literal('memory'),
902
+ Type.Literal('wiki'),
903
+ Type.Literal('all'),
904
+ ])),
905
+ }, { additionalProperties: false }),
906
+ async execute(_id, params) {
907
+ const p = params as { path?: string; from?: number; lines?: number; corpus?: 'memory' | 'wiki' | 'all' };
908
+ const path = typeof p.path === 'string' ? p.path : '';
909
+ if (p.corpus === 'wiki') {
910
+ return jsonResult({ path, text: '', disabled: true, error: 'Persistio does not provide a wiki corpus' });
911
+ }
912
+ try {
913
+ return jsonResult(await memoryManager.readFile({
914
+ relPath: path,
915
+ from: resolveOptionalPositiveInteger(p.from),
916
+ lines: resolveOptionalPositiveInteger(p.lines),
917
+ }));
918
+ } catch (err) {
919
+ return jsonResult({ path, text: '', disabled: true, error: String(err) });
920
+ }
921
+ },
922
+ });
923
+
924
+ api.registerTool({
925
+ name: 'persistio_memory_add',
926
+ label: 'Add Persistio Memory',
927
+ description: 'Manually store a fact in Persistio memory.',
779
928
  parameters: Type.Object({
780
929
  data: Type.String({ description: 'The fact to remember' }),
781
930
  subject: Type.String({ description: 'The entity or topic this fact is about' }),
@@ -787,12 +936,12 @@ export default definePluginEntry({
787
936
  } catch (err) {
788
937
  if (isTimeoutLikeError(err)) {
789
938
  api.logger?.warn?.(
790
- `openclaw-persistio: memory_add timeout after ${cfg.ingest.timeoutMs}ms; outcome is ambiguous`,
939
+ `openclaw-persistio: persistio_memory_add timeout after ${cfg.ingest.timeoutMs}ms; outcome is ambiguous`,
791
940
  );
792
941
  return {
793
942
  content: [{
794
943
  type: 'text' as const,
795
- text: 'Memory store request timed out; it may still complete. Check memory_list before retrying.',
944
+ text: 'Memory store request timed out; it may still complete. Check persistio_memory_list before retrying.',
796
945
  }],
797
946
  details: { ambiguous: true },
798
947
  };
@@ -801,12 +950,12 @@ export default definePluginEntry({
801
950
  }
802
951
  return { content: [{ type: 'text' as const, text: 'Memory stored.' }], details: null };
803
952
  },
804
- });
953
+ }, { optional: true });
805
954
 
806
955
  api.registerTool({
807
- name: 'memory_delete',
808
- label: 'Delete Memory',
809
- description: 'Delete a specific memory by its ID.',
956
+ name: 'persistio_memory_delete',
957
+ label: 'Delete Persistio Memory',
958
+ description: 'Delete a specific Persistio memory by its ID.',
810
959
  parameters: Type.Object({
811
960
  id: Type.String({ description: 'The memory ID to delete' }),
812
961
  }),
@@ -818,14 +967,14 @@ export default definePluginEntry({
818
967
  }, { optional: true });
819
968
 
820
969
  api.registerTool({
821
- name: 'memory_list',
822
- label: 'List Memories',
823
- description: 'List all stored memories.',
970
+ name: 'persistio_memory_list',
971
+ label: 'List Persistio Memories',
972
+ description: 'List stored Persistio memories.',
824
973
  parameters: Type.Object({}),
825
974
  async execute(_id, _params) {
826
975
  const memories = await client.listMemories();
827
976
  const text = memories.length > 0
828
- ? memories.map(m => `[${m.id}] ${m.data} (${m.subject})`).join('\n')
977
+ ? memories.map(m => `[${m.id}] ${truncate(m.data.replace(/\s+/g, ' ').trim(), MAX_MEMORY_SNIPPET_CHARS)} (${m.subject})`).join('\n')
829
978
  : 'No memories stored.';
830
979
  return { content: [{ type: 'text' as const, text }], details: null };
831
980
  },