@persistio/openclaw-plugin 0.1.5 → 0.1.7
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 +19 -2
- package/dist/client.d.ts +7 -0
- package/dist/client.js +129 -57
- package/dist/index.js +212 -36
- package/dist/ingest-policy.d.ts +48 -0
- package/dist/ingest-policy.js +380 -0
- package/openclaw.plugin.json +83 -4
- package/package.json +2 -2
- package/src/client.ts +133 -51
- package/src/index.ts +264 -45
- package/src/ingest-policy.ts +508 -0
package/README.md
CHANGED
|
@@ -12,7 +12,10 @@ Hooks into OpenClaw's `before_prompt_build` and `agent_end` events to automatica
|
|
|
12
12
|
## Installation
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
|
|
15
|
+
openclaw plugins install npm:@persistio/openclaw-plugin
|
|
16
|
+
openclaw plugins enable openclaw-persistio
|
|
17
|
+
openclaw gateway restart
|
|
18
|
+
openclaw plugins inspect openclaw-persistio --runtime --json
|
|
16
19
|
```
|
|
17
20
|
|
|
18
21
|
Then register it in your OpenClaw config:
|
|
@@ -21,7 +24,8 @@ Then register it in your OpenClaw config:
|
|
|
21
24
|
{
|
|
22
25
|
"plugins": {
|
|
23
26
|
"entries": {
|
|
24
|
-
"persistio": {
|
|
27
|
+
"openclaw-persistio": {
|
|
28
|
+
"enabled": true,
|
|
25
29
|
"package": "@persistio/openclaw-plugin",
|
|
26
30
|
"config": {
|
|
27
31
|
"baseURL": "https://api.persistio.ai",
|
|
@@ -51,12 +55,25 @@ Then register it in your OpenClaw config:
|
|
|
51
55
|
| `recallTopK` | number | | `10` | Number of memories to retrieve per recall |
|
|
52
56
|
| `recallMinSimilarity` | number from `0` to `1` | | Persistio server default | Optional semantic recall quality floor |
|
|
53
57
|
| `recallTimeout` | number | | `5000` | HTTP timeout for recall requests (ms) |
|
|
58
|
+
| `ingest.timeoutMs` | number | | `30000` | HTTP timeout for ingest requests (ms). Timed-out requests are treated as ambiguous and not retried automatically |
|
|
59
|
+
| `ingest.maxChunkChars` | number | | `6000` | Maximum characters per chunk sent to Persistio |
|
|
60
|
+
| `ingest.maxChunksPerTurn` | number | | `12` | Maximum chunks sent from a single OpenClaw turn |
|
|
61
|
+
| `ingest.skipSubagentSessions` | boolean | | `true` | Skip `agent:*` sessions unless they are `agent:main:*` |
|
|
62
|
+
| `ingest.user.maxCharsPerMessage` | number | | `24000` | Maximum user-message characters considered for ingest before chunking |
|
|
63
|
+
| `ingest.agent.mode` | `"bounded"` or `"raw"` | | `"bounded"` | Assistant ingest shaping mode. `bounded` collapses obvious large noisy blocks before chunking |
|
|
64
|
+
| `ingest.agent.maxCharsPerMessage` | number | | `24000` | Maximum assistant-message characters considered after filtering |
|
|
65
|
+
| `ingest.agent.maxCharsAfterFiltering` | number | | `9000` | Maximum assistant-message characters retained after deterministic filtering |
|
|
66
|
+
| `ingest.agent.maxCharsPerTurn` | number | | `24000` | Maximum assistant-message characters sent from one turn |
|
|
54
67
|
| `send.roles.user` | `"enabled"` or `"disabled"` | | `"enabled"` | Send user messages to Persistio ingest |
|
|
55
68
|
| `send.roles.agent` | `"enabled"` or `"disabled"` | | `"enabled"` | Send agent/assistant messages to Persistio ingest |
|
|
56
69
|
| `send.roles.tool` | `"enabled"` or `"disabled"` | | `"disabled"` | Send tool messages to Persistio ingest |
|
|
57
70
|
|
|
71
|
+
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
|
+
|
|
58
73
|
`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.
|
|
59
74
|
|
|
75
|
+
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.
|
|
76
|
+
|
|
60
77
|
## Tools exposed
|
|
61
78
|
|
|
62
79
|
| Tool | Description |
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PersistioIngestPolicy } from './ingest-policy.js';
|
|
1
2
|
export interface PersistioConfig {
|
|
2
3
|
baseURL: string;
|
|
3
4
|
apiKey: string;
|
|
@@ -5,6 +6,7 @@ export interface PersistioConfig {
|
|
|
5
6
|
recallTopK: number;
|
|
6
7
|
recallMinSimilarity?: number;
|
|
7
8
|
recallTimeout: number;
|
|
9
|
+
ingest: PersistioIngestPolicy;
|
|
8
10
|
send: PersistioSendConfig;
|
|
9
11
|
}
|
|
10
12
|
export type PersistioSendRoleStatus = 'enabled' | 'disabled';
|
|
@@ -42,12 +44,17 @@ export interface RecallBundleResponse {
|
|
|
42
44
|
bundle: RecallBundle;
|
|
43
45
|
related_bundle?: RecallBundle;
|
|
44
46
|
}
|
|
47
|
+
export declare class PersistioTimeoutError extends Error {
|
|
48
|
+
constructor(operation: string, timeoutMs: number);
|
|
49
|
+
}
|
|
45
50
|
export declare class PersistioClient {
|
|
46
51
|
private readonly baseURL;
|
|
47
52
|
private readonly apiKey;
|
|
48
53
|
private readonly recallTopK;
|
|
49
54
|
private readonly recallMinSimilarity?;
|
|
50
55
|
private readonly recallTimeout;
|
|
56
|
+
private readonly ingestTimeout;
|
|
57
|
+
private readonly writeTimeout;
|
|
51
58
|
constructor(config: PersistioConfig);
|
|
52
59
|
private headers;
|
|
53
60
|
recall(query: string): Promise<PersistioMemory[]>;
|
package/dist/client.js
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
|
+
export class PersistioTimeoutError extends Error {
|
|
2
|
+
constructor(operation, timeoutMs) {
|
|
3
|
+
super(`Persistio ${operation} timed out after ${timeoutMs}ms`);
|
|
4
|
+
this.name = 'TimeoutError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
1
7
|
export class PersistioClient {
|
|
2
8
|
baseURL;
|
|
3
9
|
apiKey;
|
|
4
10
|
recallTopK;
|
|
5
11
|
recallMinSimilarity;
|
|
6
12
|
recallTimeout;
|
|
13
|
+
ingestTimeout;
|
|
14
|
+
writeTimeout;
|
|
7
15
|
constructor(config) {
|
|
8
16
|
this.baseURL = config.baseURL.replace(/\/$/, '');
|
|
9
17
|
this.apiKey = config.apiKey;
|
|
10
18
|
this.recallTopK = config.recallTopK;
|
|
11
19
|
this.recallMinSimilarity = config.recallMinSimilarity;
|
|
12
20
|
this.recallTimeout = config.recallTimeout;
|
|
21
|
+
this.ingestTimeout = config.ingest.timeoutMs;
|
|
22
|
+
this.writeTimeout = config.ingest.timeoutMs;
|
|
13
23
|
}
|
|
14
24
|
headers() {
|
|
15
25
|
return {
|
|
@@ -18,83 +28,145 @@ export class PersistioClient {
|
|
|
18
28
|
};
|
|
19
29
|
}
|
|
20
30
|
async recall(query) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
|
|
32
|
+
const body = { query, top_k: this.recallTopK, include_pending: true };
|
|
33
|
+
if (typeof this.recallMinSimilarity === 'number') {
|
|
34
|
+
body.min_similarity = this.recallMinSimilarity;
|
|
35
|
+
}
|
|
36
|
+
const res = await fetch(`${this.baseURL}/v1/recall`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
headers: this.headers(),
|
|
39
|
+
body: JSON.stringify(body),
|
|
40
|
+
signal,
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok)
|
|
43
|
+
throw new Error(`Persistio recall failed: ${res.status}`);
|
|
44
|
+
const data = await res.json();
|
|
45
|
+
return data.memories ?? [];
|
|
30
46
|
});
|
|
31
|
-
if (!res.ok)
|
|
32
|
-
throw new Error(`Persistio recall failed: ${res.status}`);
|
|
33
|
-
const data = await res.json();
|
|
34
|
-
return data.memories ?? [];
|
|
35
47
|
}
|
|
36
48
|
async recallBundle(query, topK) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
|
|
50
|
+
const body = { query, top_k: topK ?? this.recallTopK, include_pending: true };
|
|
51
|
+
if (typeof this.recallMinSimilarity === 'number') {
|
|
52
|
+
body.min_similarity = this.recallMinSimilarity;
|
|
53
|
+
}
|
|
54
|
+
const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: this.headers(),
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
signal,
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok)
|
|
61
|
+
throw new Error(`Persistio recallBundle failed: ${res.status}`);
|
|
62
|
+
const data = await res.json();
|
|
63
|
+
return data;
|
|
46
64
|
});
|
|
47
|
-
if (!res.ok)
|
|
48
|
-
throw new Error(`Persistio recallBundle failed: ${res.status}`);
|
|
49
|
-
const data = await res.json();
|
|
50
|
-
return data;
|
|
51
65
|
}
|
|
52
66
|
async ingest(sessionId, chunks) {
|
|
53
67
|
if (chunks.length === 0)
|
|
54
68
|
return;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
69
|
+
await withRequestDeadline('ingest', this.ingestTimeout, async (signal) => {
|
|
70
|
+
const res = await fetch(`${this.baseURL}/v1/ingest`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: this.headers(),
|
|
73
|
+
body: JSON.stringify({ session_id: sessionId, chunks }),
|
|
74
|
+
signal,
|
|
75
|
+
});
|
|
76
|
+
if (!res.ok)
|
|
77
|
+
throw new Error(await formatHttpError('ingest', res));
|
|
59
78
|
});
|
|
60
|
-
if (!res.ok)
|
|
61
|
-
throw new Error(`Persistio ingest failed: ${res.status}`);
|
|
62
79
|
}
|
|
63
80
|
async addMemory(data, subject) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
await withRequestDeadline('addMemory', this.writeTimeout, async (signal) => {
|
|
82
|
+
const res = await fetch(`${this.baseURL}/v1/memories`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: this.headers(),
|
|
85
|
+
body: JSON.stringify({ data, subject }),
|
|
86
|
+
signal,
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok)
|
|
89
|
+
throw new Error(`Persistio addMemory failed: ${res.status}`);
|
|
68
90
|
});
|
|
69
|
-
if (!res.ok)
|
|
70
|
-
throw new Error(`Persistio addMemory failed: ${res.status}`);
|
|
71
91
|
}
|
|
72
92
|
async deleteMemory(id) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
await withRequestDeadline('deleteMemory', this.writeTimeout, async (signal) => {
|
|
94
|
+
const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
|
|
95
|
+
method: 'DELETE',
|
|
96
|
+
headers: this.headers(),
|
|
97
|
+
signal,
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok)
|
|
100
|
+
throw new Error(`Persistio deleteMemory failed: ${res.status}`);
|
|
76
101
|
});
|
|
77
|
-
if (!res.ok)
|
|
78
|
-
throw new Error(`Persistio deleteMemory failed: ${res.status}`);
|
|
79
102
|
}
|
|
80
103
|
async getMemory(id, options = {}) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
return withRequestDeadline('getMemory', this.recallTimeout, async (signal) => {
|
|
105
|
+
const query = options.includePending ? '?include_pending=true' : '';
|
|
106
|
+
const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
|
|
107
|
+
headers: this.headers(),
|
|
108
|
+
signal,
|
|
109
|
+
});
|
|
110
|
+
if (res.status === 404)
|
|
111
|
+
return null;
|
|
112
|
+
if (!res.ok)
|
|
113
|
+
throw new Error(`Persistio getMemory failed: ${res.status}`);
|
|
114
|
+
return await res.json();
|
|
84
115
|
});
|
|
85
|
-
if (res.status === 404)
|
|
86
|
-
return null;
|
|
87
|
-
if (!res.ok)
|
|
88
|
-
throw new Error(`Persistio getMemory failed: ${res.status}`);
|
|
89
|
-
return await res.json();
|
|
90
116
|
}
|
|
91
117
|
async listMemories() {
|
|
92
|
-
|
|
93
|
-
|
|
118
|
+
return withRequestDeadline('listMemories', this.recallTimeout, async (signal) => {
|
|
119
|
+
const res = await fetch(`${this.baseURL}/v1/memories`, {
|
|
120
|
+
headers: this.headers(),
|
|
121
|
+
signal,
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
throw new Error(`Persistio listMemories failed: ${res.status}`);
|
|
125
|
+
const data = await res.json();
|
|
126
|
+
return data.items ?? [];
|
|
94
127
|
});
|
|
95
|
-
if (!res.ok)
|
|
96
|
-
throw new Error(`Persistio listMemories failed: ${res.status}`);
|
|
97
|
-
const data = await res.json();
|
|
98
|
-
return data.items ?? [];
|
|
99
128
|
}
|
|
100
129
|
}
|
|
130
|
+
async function withRequestDeadline(operation, timeoutMs, run) {
|
|
131
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
132
|
+
return run(new AbortController().signal);
|
|
133
|
+
}
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
let timeout;
|
|
136
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
137
|
+
timeout = setTimeout(() => {
|
|
138
|
+
controller.abort();
|
|
139
|
+
reject(new PersistioTimeoutError(operation, timeoutMs));
|
|
140
|
+
}, timeoutMs);
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
return await Promise.race([run(controller.signal), deadline]);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
if (controller.signal.aborted && isAbortLikeError(err)) {
|
|
147
|
+
throw new PersistioTimeoutError(operation, timeoutMs);
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (timeout)
|
|
153
|
+
clearTimeout(timeout);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isAbortLikeError(err) {
|
|
157
|
+
if (!(err instanceof Error))
|
|
158
|
+
return false;
|
|
159
|
+
return err.name === 'AbortError' || err.name === 'TimeoutError';
|
|
160
|
+
}
|
|
161
|
+
async function formatHttpError(operation, res) {
|
|
162
|
+
let detail = '';
|
|
163
|
+
try {
|
|
164
|
+
detail = (await res.text()).trim().slice(0, 500);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Ignore response body read failures; the status is still actionable.
|
|
168
|
+
}
|
|
169
|
+
return detail
|
|
170
|
+
? `Persistio ${operation} failed: ${res.status} ${detail}`
|
|
171
|
+
: `Persistio ${operation} failed: ${res.status}`;
|
|
172
|
+
}
|