@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/README.md +35 -10
- package/dist/client.d.ts +12 -1
- package/dist/client.js +130 -60
- package/dist/index.js +303 -64
- package/openclaw.plugin.json +48 -17
- package/package.json +2 -2
- package/src/client.ts +135 -53
- package/src/index.ts +366 -72
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:
|
|
63
|
-
recallTopK:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
556
|
+
const recallTopK = resolveMemorySearchLimit({ maxResults: opts?.maxResults, fallback: config.recallTopK });
|
|
402
557
|
const recallClient = createClient(config, recallTopK);
|
|
403
|
-
const memories = await
|
|
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,
|
|
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:
|
|
442
|
-
from
|
|
443
|
-
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
|
628
|
-
description: 'Search
|
|
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: '
|
|
631
|
-
|
|
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 {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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: '
|
|
648
|
-
label: 'Add Memory',
|
|
649
|
-
description: 'Manually store a fact in
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
677
|
-
label: 'List Memories',
|
|
678
|
-
description: 'List
|
|
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
|
},
|