@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 +27 -8
- package/dist/client.d.ts +8 -1
- package/dist/client.js +16 -3
- package/dist/index.js +175 -46
- package/openclaw.plugin.json +16 -6
- package/package.json +1 -1
- package/src/client.ts +22 -3
- package/src/index.ts +196 -47
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
|
|
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 | | `
|
|
55
|
-
| `recallTopK` | number | | `
|
|
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 | | `
|
|
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` |
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
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 = {
|
|
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 = {
|
|
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'],
|
|
70
|
-
recallTopK: resolvePositiveInteger(c['recallTopK'],
|
|
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'],
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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:
|
|
444
|
-
from
|
|
445
|
-
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
|
|
621
|
-
description: 'Search
|
|
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: '
|
|
624
|
-
|
|
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
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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: '
|
|
647
|
-
label: '
|
|
648
|
-
description: '
|
|
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:
|
|
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
|
|
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: '
|
|
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: '
|
|
689
|
-
label: 'List Memories',
|
|
690
|
-
description: 'List
|
|
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
|
},
|
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,16 +10,20 @@
|
|
|
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
|
},
|
|
18
19
|
"toolMetadata": {
|
|
19
|
-
"
|
|
20
|
+
"persistio_memory_add": {
|
|
20
21
|
"optional": true
|
|
21
22
|
},
|
|
22
|
-
"
|
|
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
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> = {
|
|
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> = {
|
|
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'],
|
|
104
|
-
recallTopK: resolvePositiveInteger(c['recallTopK'],
|
|
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'],
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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:
|
|
558
|
-
from
|
|
559
|
-
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(
|
|
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
|
|
750
|
-
description: 'Search
|
|
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: '
|
|
753
|
-
|
|
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 {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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: '
|
|
777
|
-
label: '
|
|
778
|
-
description: '
|
|
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:
|
|
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
|
|
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: '
|
|
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: '
|
|
822
|
-
label: 'List Memories',
|
|
823
|
-
description: 'List
|
|
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
|
},
|