@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/src/index.ts CHANGED
@@ -29,6 +29,47 @@ const DEFAULT_SEND_ROLES: PersistioConfig['send']['roles'] = {
29
29
  const MESSAGE_KEY_TTL_MS = 24 * 60 * 60 * 1000;
30
30
  const MAX_TRACKED_SESSIONS = 250;
31
31
  const MAX_SENT_KEYS_PER_SESSION = 2000;
32
+ const RECALL_CIRCUIT_BREAKER_FAILURE_THRESHOLD = 3;
33
+ const RECALL_CIRCUIT_BREAKER_COOLDOWN_MS = 60_000;
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;
41
+
42
+ interface PluginLogger {
43
+ debug?: (message: string) => void;
44
+ warn?: (message: string) => void;
45
+ }
46
+
47
+ class RecallCircuitBreaker {
48
+ private consecutiveFailures = 0;
49
+ private openedUntil = 0;
50
+
51
+ canAttempt(now = Date.now()): boolean {
52
+ return now >= this.openedUntil;
53
+ }
54
+
55
+ remainingMs(now = Date.now()): number {
56
+ return Math.max(0, this.openedUntil - now);
57
+ }
58
+
59
+ recordSuccess(): void {
60
+ this.consecutiveFailures = 0;
61
+ this.openedUntil = 0;
62
+ }
63
+
64
+ recordFailure(now = Date.now()): boolean {
65
+ this.consecutiveFailures += 1;
66
+ if (this.consecutiveFailures >= RECALL_CIRCUIT_BREAKER_FAILURE_THRESHOLD) {
67
+ this.openedUntil = now + RECALL_CIRCUIT_BREAKER_COOLDOWN_MS;
68
+ return true;
69
+ }
70
+ return false;
71
+ }
72
+ }
32
73
 
33
74
  function resolveSendConfig(raw: Record<string, unknown>): PersistioConfig['send'] {
34
75
  const send = raw['send'];
@@ -54,15 +95,37 @@ function resolveRecallMinSimilarity(value: unknown): number | undefined {
54
95
  : undefined;
55
96
  }
56
97
 
98
+ function resolvePositiveInteger(value: unknown, fallback: number): number {
99
+ return typeof value === 'number' && Number.isFinite(value) && value >= 1
100
+ ? Math.floor(value)
101
+ : fallback;
102
+ }
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
+
57
118
  function resolveConfig(raw: unknown): PersistioConfig {
58
119
  const c = (raw ?? {}) as Record<string, unknown>;
59
120
  return {
60
121
  baseURL: typeof c['baseURL'] === 'string' ? c['baseURL'] : '',
61
122
  apiKey: typeof c['apiKey'] === 'string' ? c['apiKey'] : '',
62
- tokenBudget: typeof c['tokenBudget'] === 'number' ? c['tokenBudget'] : 2000,
63
- recallTopK: typeof c['recallTopK'] === 'number' ? c['recallTopK'] : 10,
123
+ tokenBudget: resolvePositiveInteger(c['tokenBudget'], DEFAULT_TOKEN_BUDGET),
124
+ recallTopK: resolvePositiveInteger(c['recallTopK'], DEFAULT_RECALL_TOP_K),
64
125
  recallMinSimilarity: resolveRecallMinSimilarity(c['recallMinSimilarity']),
65
- recallTimeout: typeof c['recallTimeout'] === 'number' ? c['recallTimeout'] : 5000,
126
+ recallTimeout: resolvePositiveInteger(c['recallTimeout'], DEFAULT_RECALL_TIMEOUT_MS),
127
+ recallIncludePending: resolveBoolean(c['recallIncludePending'], false),
128
+ includeRelatedMemories: resolveBoolean(c['includeRelatedMemories'], false),
66
129
  ingest: resolveIngestPolicy(c['ingest']),
67
130
  send: resolveSendConfig(c),
68
131
  };
@@ -139,29 +202,37 @@ function buildRecallQuery(event: { prompt?: string; messages?: unknown[] }): str
139
202
  return truncate(parts.join('\n'), 600);
140
203
  }
141
204
 
142
- function buildMemoryBlock(bundle: RecallBundle, budget: number, relatedBundle?: RecallBundle): string {
205
+ function toStringArray(value: unknown): string[] {
206
+ return Array.isArray(value)
207
+ ? value.filter((item): item is string => typeof item === 'string')
208
+ : [];
209
+ }
210
+
211
+ function buildMemoryBlock(bundle: RecallBundle | undefined, budget: number, relatedBundle?: RecallBundle): string {
212
+ if (!bundle || typeof bundle !== 'object') return '';
213
+
143
214
  const sections: Array<{ title: string; items: string[] }> = [
144
- { title: 'Behavioural rules', items: bundle.user_rules },
145
- { title: 'Preferences', items: bundle.user_preferences },
146
- { title: 'Task patterns', items: bundle.task_patterns },
147
- { title: 'Workflows', items: bundle.workflows },
148
- { title: 'Project', items: bundle.project },
149
- { title: 'Constraints', items: bundle.constraints },
150
- { title: 'Decisions', items: bundle.decisions },
151
- { title: 'System facts', items: bundle.system_facts },
152
- { title: 'Domain knowledge', items: bundle.domain_knowledge },
215
+ { title: 'Behavioural rules', items: [...toStringArray(bundle.global_user_rules), ...toStringArray(bundle.user_rules)] },
216
+ { title: 'Preferences', items: toStringArray(bundle.user_preferences) },
217
+ { title: 'Task patterns', items: toStringArray(bundle.task_patterns) },
218
+ { title: 'Workflows', items: toStringArray(bundle.workflows) },
219
+ { title: 'Project', items: toStringArray(bundle.project) },
220
+ { title: 'Constraints', items: toStringArray(bundle.constraints) },
221
+ { title: 'Decisions', items: toStringArray(bundle.decisions) },
222
+ { title: 'System facts', items: toStringArray(bundle.system_facts) },
223
+ { title: 'Domain knowledge', items: toStringArray(bundle.domain_knowledge) },
153
224
  ];
154
- if (relatedBundle) {
225
+ if (relatedBundle && typeof relatedBundle === 'object') {
155
226
  sections.push(
156
- { title: 'Related behavioural rules', items: relatedBundle.user_rules },
157
- { title: 'Related preferences', items: relatedBundle.user_preferences },
158
- { title: 'Related task patterns', items: relatedBundle.task_patterns },
159
- { title: 'Related workflows', items: relatedBundle.workflows },
160
- { title: 'Related project', items: relatedBundle.project },
161
- { title: 'Related constraints', items: relatedBundle.constraints },
162
- { title: 'Related decisions', items: relatedBundle.decisions },
163
- { title: 'Related system facts', items: relatedBundle.system_facts },
164
- { title: 'Related domain knowledge', items: relatedBundle.domain_knowledge },
227
+ { title: 'Related behavioural rules', items: toStringArray(relatedBundle.user_rules) },
228
+ { title: 'Related preferences', items: toStringArray(relatedBundle.user_preferences) },
229
+ { title: 'Related task patterns', items: toStringArray(relatedBundle.task_patterns) },
230
+ { title: 'Related workflows', items: toStringArray(relatedBundle.workflows) },
231
+ { title: 'Related project', items: toStringArray(relatedBundle.project) },
232
+ { title: 'Related constraints', items: toStringArray(relatedBundle.constraints) },
233
+ { title: 'Related decisions', items: toStringArray(relatedBundle.decisions) },
234
+ { title: 'Related system facts', items: toStringArray(relatedBundle.system_facts) },
235
+ { title: 'Related domain knowledge', items: toStringArray(relatedBundle.domain_knowledge) },
165
236
  );
166
237
  }
167
238
 
@@ -179,10 +250,10 @@ function buildMemoryBlock(bundle: RecallBundle, budget: number, relatedBundle?:
179
250
  const includedItems: string[] = [];
180
251
 
181
252
  for (const item of candidates) {
182
- const line = `- ${item}`;
253
+ const line = `- ${truncate(item.replace(/\s+/g, ' ').trim(), MAX_PROMPT_MEMORY_ITEM_CHARS)}`;
183
254
  const cost = estimateTokens(`\n${line}`);
184
255
  if (tentativeUsed + cost > budget) {
185
- return lines.length > 1 ? lines.join('\n') : '';
256
+ break;
186
257
  }
187
258
  includedItems.push(line);
188
259
  tentativeUsed += cost;
@@ -329,8 +400,88 @@ function isTimeoutLikeError(err: unknown): boolean {
329
400
  return message.includes('timeout') || message.includes('aborted');
330
401
  }
331
402
 
403
+ async function runGuardedRecall<T>(args: {
404
+ operation: string;
405
+ timeoutMs: number;
406
+ fallback: T;
407
+ breaker: RecallCircuitBreaker;
408
+ logger?: PluginLogger;
409
+ run: () => Promise<T>;
410
+ }): Promise<T> {
411
+ const now = Date.now();
412
+ if (!args.breaker.canAttempt(now)) {
413
+ args.logger?.warn?.(
414
+ `openclaw-persistio: ${args.operation} skipped; recall circuit breaker open `
415
+ + `for ${args.breaker.remainingMs(now)}ms`,
416
+ );
417
+ return args.fallback;
418
+ }
419
+
420
+ try {
421
+ const result = await withPluginDeadline(args.operation, args.timeoutMs + RECALL_GUARD_MARGIN_MS, args.run);
422
+ args.breaker.recordSuccess();
423
+ return result;
424
+ } catch (err) {
425
+ const opened = args.breaker.recordFailure();
426
+ args.logger?.warn?.(
427
+ `openclaw-persistio: ${args.operation} failed open: ${String(err)}`
428
+ + (opened ? `; recall circuit breaker open for ${RECALL_CIRCUIT_BREAKER_COOLDOWN_MS}ms` : ''),
429
+ );
430
+ return args.fallback;
431
+ }
432
+ }
433
+
434
+ async function withPluginDeadline<T>(
435
+ operation: string,
436
+ timeoutMs: number,
437
+ run: () => Promise<T>,
438
+ ): Promise<T> {
439
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
440
+ return run();
441
+ }
442
+
443
+ let timeout: ReturnType<typeof setTimeout> | undefined;
444
+ const deadline = new Promise<never>((_resolve, reject) => {
445
+ timeout = setTimeout(() => {
446
+ const err = new Error(`Persistio ${operation} exceeded plugin deadline after ${timeoutMs}ms`);
447
+ err.name = 'TimeoutError';
448
+ reject(err);
449
+ }, timeoutMs);
450
+ });
451
+
452
+ try {
453
+ return await Promise.race([run(), deadline]);
454
+ } finally {
455
+ if (timeout) clearTimeout(timeout);
456
+ }
457
+ }
458
+
332
459
  const PERSISTIO_MEMORY_PATH_PREFIX = 'persistio://memory/';
333
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
+
334
485
  function createClient(config: PersistioConfig, recallTopK = config.recallTopK): PersistioClient {
335
486
  return new PersistioClient({ ...config, recallTopK });
336
487
  }
@@ -379,7 +530,11 @@ async function probePersistio(client: PersistioClient): Promise<MemoryEmbeddingP
379
530
  }
380
531
  }
381
532
 
382
- function createMemorySearchManager(config: PersistioConfig): MemorySearchManager {
533
+ function createMemorySearchManager(
534
+ config: PersistioConfig,
535
+ recallBreaker: RecallCircuitBreaker,
536
+ logger?: PluginLogger,
537
+ ): MemorySearchManager {
383
538
  const client = createClient(config);
384
539
 
385
540
  return {
@@ -398,9 +553,16 @@ function createMemorySearchManager(config: PersistioConfig): MemorySearchManager
398
553
  return [];
399
554
  }
400
555
 
401
- const recallTopK = typeof opts?.maxResults === 'number' ? opts.maxResults : config.recallTopK;
556
+ const recallTopK = resolveMemorySearchLimit({ maxResults: opts?.maxResults, fallback: config.recallTopK });
402
557
  const recallClient = createClient(config, recallTopK);
403
- const memories = await recallClient.recall(query);
558
+ const memories = await runGuardedRecall({
559
+ operation: 'memory search recall',
560
+ timeoutMs: config.recallTimeout,
561
+ fallback: [],
562
+ breaker: recallBreaker,
563
+ logger,
564
+ run: () => recallClient.recall(query),
565
+ });
404
566
 
405
567
  return memories
406
568
  .map((memory): MemorySearchResult => {
@@ -411,7 +573,7 @@ function createMemorySearchManager(config: PersistioConfig): MemorySearchManager
411
573
  endLine: 1,
412
574
  score,
413
575
  vectorScore: typeof memory.similarity === 'number' ? memory.similarity : undefined,
414
- snippet: truncate(memory.data, 400),
576
+ snippet: truncate(memory.data.replace(/\s+/g, ' ').trim(), MAX_MEMORY_SNIPPET_CHARS),
415
577
  source: 'memory',
416
578
  citation: memory.subject,
417
579
  };
@@ -435,12 +597,17 @@ function createMemorySearchManager(config: PersistioConfig): MemorySearchManager
435
597
  }
436
598
 
437
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');
438
605
  return {
439
606
  path: params.relPath,
440
- text,
441
- truncated: false,
442
- from: params.from ?? 1,
443
- lines: params.lines,
607
+ text: truncate(sliced, 2000),
608
+ truncated: startIndex + requestedLines < lines.length || sliced.length > 2000,
609
+ from,
610
+ lines: requestedLines,
444
611
  };
445
612
  },
446
613
 
@@ -469,11 +636,11 @@ function createMemorySearchManager(config: PersistioConfig): MemorySearchManager
469
636
  };
470
637
  }
471
638
 
472
- function createMemoryRuntime(config: PersistioConfig) {
639
+ function createMemoryRuntime(config: PersistioConfig, recallBreaker: RecallCircuitBreaker, logger?: PluginLogger) {
473
640
  return {
474
641
  async getMemorySearchManager() {
475
642
  return {
476
- manager: createMemorySearchManager(config),
643
+ manager: createMemorySearchManager(config, recallBreaker, logger),
477
644
  };
478
645
  },
479
646
 
@@ -483,6 +650,26 @@ function createMemoryRuntime(config: PersistioConfig) {
483
650
  };
484
651
  }
485
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
+
486
673
  export default definePluginEntry({
487
674
  id: 'openclaw-persistio',
488
675
  name: 'Persistio Memory',
@@ -497,10 +684,12 @@ export default definePluginEntry({
497
684
  }
498
685
 
499
686
  const client = createClient(cfg);
687
+ const recallBreaker = new RecallCircuitBreaker();
500
688
  const sentMessageKeysBySession = new Map<string, SessionMessageKeyStore>();
501
689
  const pendingMessageKeysBySession = new Map<string, SessionMessageKeyStore>();
502
690
  api.registerMemoryCapability({
503
- runtime: createMemoryRuntime(cfg),
691
+ promptBuilder: buildPersistioMemoryPromptSection,
692
+ runtime: createMemoryRuntime(cfg, recallBreaker, api.logger),
504
693
  });
505
694
 
506
695
  // -------------------------------------------------------------------------
@@ -509,16 +698,25 @@ export default definePluginEntry({
509
698
  // Return: { appendSystemContext?: string }
510
699
  // -------------------------------------------------------------------------
511
700
  api.on('before_prompt_build', async (event) => {
512
- try {
513
- const query = buildRecallQuery(event);
514
- const recall = await client.recallBundle(query);
515
- const block = buildMemoryBlock(recall.bundle, cfg.tokenBudget, recall.related_bundle);
516
- if (!block) return;
517
- return { appendSystemContext: block };
518
- } catch (err) {
519
- api.logger?.warn?.(`openclaw-persistio: recall error: ${String(err)}`);
520
- }
521
- });
701
+ const query = buildRecallQuery(event);
702
+ const block = await runGuardedRecall({
703
+ operation: 'before_prompt_build recall',
704
+ timeoutMs: cfg.recallTimeout,
705
+ fallback: '',
706
+ breaker: recallBreaker,
707
+ logger: api.logger,
708
+ run: async () => {
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
+ );
715
+ },
716
+ });
717
+ if (!block) return;
718
+ return { appendSystemContext: block };
719
+ }, { timeoutMs: cfg.recallTimeout + RECALL_GUARD_MARGIN_MS + 250 });
522
720
 
523
721
  // -------------------------------------------------------------------------
524
722
  // agent_end — ingest new turn messages (fire and forget)
@@ -622,46 +820,142 @@ export default definePluginEntry({
622
820
  // AgentToolResult: { content: Array<{ type: "text", text: string }>, details: unknown }
623
821
  // -------------------------------------------------------------------------
624
822
 
823
+ const memoryManager = createMemorySearchManager(cfg, recallBreaker, api.logger);
824
+
625
825
  api.registerTool({
626
826
  name: 'memory_search',
627
- label: 'Search Memory',
628
- 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.',
629
829
  parameters: Type.Object({
630
- query: Type.String({ description: 'What to search for' }),
631
- top_k: Type.Optional(Type.Number({ description: 'Max results to return' })),
632
- }),
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 }),
633
840
  async execute(_id, params) {
634
- const p = params as { query: string; top_k?: number };
635
- const overrideTopK = typeof p.top_k === 'number' ? p.top_k : cfg.recallTopK;
636
- const overrideCfg = { ...cfg, recallTopK: overrideTopK };
637
- const c = new PersistioClient(overrideCfg);
638
- const memories = await c.recall(p.query);
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' as const, text }], details: null };
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,
855
+ });
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
+ }
889
+ },
890
+ });
891
+
892
+ api.registerTool({
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
+ }
643
921
  },
644
922
  });
645
923
 
646
924
  api.registerTool({
647
- name: 'memory_add',
648
- label: 'Add Memory',
649
- description: 'Manually store a fact in persistent memory.',
925
+ name: 'persistio_memory_add',
926
+ label: 'Add Persistio Memory',
927
+ description: 'Manually store a fact in Persistio memory.',
650
928
  parameters: Type.Object({
651
929
  data: Type.String({ description: 'The fact to remember' }),
652
930
  subject: Type.String({ description: 'The entity or topic this fact is about' }),
653
931
  }),
654
932
  async execute(_id, params) {
655
933
  const p = params as { data: string; subject: string };
656
- await client.addMemory(p.data, p.subject);
934
+ try {
935
+ await client.addMemory(p.data, p.subject);
936
+ } catch (err) {
937
+ if (isTimeoutLikeError(err)) {
938
+ api.logger?.warn?.(
939
+ `openclaw-persistio: persistio_memory_add timeout after ${cfg.ingest.timeoutMs}ms; outcome is ambiguous`,
940
+ );
941
+ return {
942
+ content: [{
943
+ type: 'text' as const,
944
+ text: 'Memory store request timed out; it may still complete. Check persistio_memory_list before retrying.',
945
+ }],
946
+ details: { ambiguous: true },
947
+ };
948
+ }
949
+ throw err;
950
+ }
657
951
  return { content: [{ type: 'text' as const, text: 'Memory stored.' }], details: null };
658
952
  },
659
- });
953
+ }, { optional: true });
660
954
 
661
955
  api.registerTool({
662
- name: 'memory_delete',
663
- label: 'Delete Memory',
664
- 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.',
665
959
  parameters: Type.Object({
666
960
  id: Type.String({ description: 'The memory ID to delete' }),
667
961
  }),
@@ -673,14 +967,14 @@ export default definePluginEntry({
673
967
  }, { optional: true });
674
968
 
675
969
  api.registerTool({
676
- name: 'memory_list',
677
- label: 'List Memories',
678
- description: 'List all stored memories.',
970
+ name: 'persistio_memory_list',
971
+ label: 'List Persistio Memories',
972
+ description: 'List stored Persistio memories.',
679
973
  parameters: Type.Object({}),
680
974
  async execute(_id, _params) {
681
975
  const memories = await client.listMemories();
682
976
  const text = memories.length > 0
683
- ? 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')
684
978
  : 'No memories stored.';
685
979
  return { content: [{ type: 'text' as const, text }], details: null };
686
980
  },