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