@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 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 automatically recall relevant memories into every prompt and ingest new conversation turns after each run. Exposes `memory_search`, `memory_add`, `memory_delete`, and `memory_list` as agent tools.
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
 
@@ -12,7 +12,18 @@ Hooks into OpenClaw's `before_prompt_build` and `agent_end` events to automatica
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- npm install -g @persistio/openclaw-plugin
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
19
+ ```
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
16
27
  ```
17
28
 
18
29
  Then register it in your OpenClaw config:
@@ -21,11 +32,15 @@ Then register it in your OpenClaw config:
21
32
  {
22
33
  "plugins": {
23
34
  "entries": {
24
- "persistio": {
35
+ "openclaw-persistio": {
36
+ "enabled": true,
25
37
  "package": "@persistio/openclaw-plugin",
26
38
  "config": {
27
39
  "baseURL": "https://api.persistio.ai",
28
40
  "apiKey": "your-vault-api-key",
41
+ "tokenBudget": 400,
42
+ "recallTopK": 4,
43
+ "recallTimeout": 1500,
29
44
  "recallMinSimilarity": 0.3,
30
45
  "send": {
31
46
  "roles": {
@@ -36,6 +51,9 @@ Then register it in your OpenClaw config:
36
51
  }
37
52
  }
38
53
  }
54
+ },
55
+ "slots": {
56
+ "memory": "openclaw-persistio"
39
57
  }
40
58
  }
41
59
  }
@@ -47,10 +65,12 @@ Then register it in your OpenClaw config:
47
65
  |---|---|---|---|---|
48
66
  | `baseURL` | string | ✅ | — | Base URL of your Persistio instance |
49
67
  | `apiKey` | string | ✅ | — | Vault API key |
50
- | `tokenBudget` | number | | `2000` | Max tokens to inject into the system prompt |
51
- | `recallTopK` | number | | `10` | Number of memories to retrieve per recall |
68
+ | `tokenBudget` | number | | `400` | Max tokens to inject into the system prompt |
69
+ | `recallTopK` | number | | `4` | Number of memories to retrieve per recall |
52
70
  | `recallMinSimilarity` | number from `0` to `1` | | Persistio server default | Optional semantic recall quality floor |
53
- | `recallTimeout` | number | | `5000` | HTTP timeout for recall requests (ms) |
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 |
54
74
  | `ingest.timeoutMs` | number | | `30000` | HTTP timeout for ingest requests (ms). Timed-out requests are treated as ambiguous and not retried automatically |
55
75
  | `ingest.maxChunkChars` | number | | `6000` | Maximum characters per chunk sent to Persistio |
56
76
  | `ingest.maxChunksPerTurn` | number | | `12` | Maximum chunks sent from a single OpenClaw turn |
@@ -64,6 +84,10 @@ Then register it in your OpenClaw config:
64
84
  | `send.roles.agent` | `"enabled"` or `"disabled"` | | `"enabled"` | Send agent/assistant messages to Persistio ingest |
65
85
  | `send.roles.tool` | `"enabled"` or `"disabled"` | | `"disabled"` | Send tool messages to Persistio ingest |
66
86
 
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`.
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
+
67
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.
68
92
 
69
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.
@@ -72,10 +96,11 @@ Assistant ingest is bounded before any network call. By default the plugin skips
72
96
 
73
97
  | Tool | Description |
74
98
  |---|---|
75
- | `memory_search` | Search memories by semantic query |
76
- | `memory_add` | Manually store a fact |
77
- | `memory_delete` | Delete a memory by ID |
78
- | `memory_list` | List all memories in the vault |
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 |
79
104
 
80
105
  ## License
81
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,17 +46,26 @@ export interface RecallBundleResponse {
44
46
  bundle: RecallBundle;
45
47
  related_bundle?: RecallBundle;
46
48
  }
49
+ export interface RecallBundleOptions {
50
+ includeRelated?: boolean;
51
+ }
52
+ export declare class PersistioTimeoutError extends Error {
53
+ constructor(operation: string, timeoutMs: number);
54
+ }
47
55
  export declare class PersistioClient {
48
56
  private readonly baseURL;
49
57
  private readonly apiKey;
50
58
  private readonly recallTopK;
51
59
  private readonly recallMinSimilarity?;
52
60
  private readonly recallTimeout;
61
+ private readonly recallIncludePending;
62
+ private readonly includeRelatedMemories;
53
63
  private readonly ingestTimeout;
64
+ private readonly writeTimeout;
54
65
  constructor(config: PersistioConfig);
55
66
  private headers;
56
67
  recall(query: string): Promise<PersistioMemory[]>;
57
- recallBundle(query: string, topK?: number): Promise<RecallBundleResponse>;
68
+ recallBundle(query: string, topK?: number, options?: RecallBundleOptions): Promise<RecallBundleResponse>;
58
69
  ingest(sessionId: string, chunks: Array<{
59
70
  role: string;
60
71
  content: string;
package/dist/client.js CHANGED
@@ -1,17 +1,29 @@
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
+ recallIncludePending;
14
+ includeRelatedMemories;
7
15
  ingestTimeout;
16
+ writeTimeout;
8
17
  constructor(config) {
9
18
  this.baseURL = config.baseURL.replace(/\/$/, '');
10
19
  this.apiKey = config.apiKey;
11
20
  this.recallTopK = config.recallTopK;
12
21
  this.recallMinSimilarity = config.recallMinSimilarity;
13
22
  this.recallTimeout = config.recallTimeout;
23
+ this.recallIncludePending = config.recallIncludePending;
24
+ this.includeRelatedMemories = config.includeRelatedMemories;
14
25
  this.ingestTimeout = config.ingest.timeoutMs;
26
+ this.writeTimeout = config.ingest.timeoutMs;
15
27
  }
16
28
  headers() {
17
29
  return {
@@ -20,87 +32,145 @@ export class PersistioClient {
20
32
  };
21
33
  }
22
34
  async recall(query) {
23
- const body = { query, top_k: this.recallTopK, include_pending: true };
24
- if (typeof this.recallMinSimilarity === 'number') {
25
- body.min_similarity = this.recallMinSimilarity;
26
- }
27
- const res = await fetch(`${this.baseURL}/v1/recall`, {
28
- method: 'POST',
29
- headers: this.headers(),
30
- body: JSON.stringify(body),
31
- signal: AbortSignal.timeout(this.recallTimeout),
35
+ return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
36
+ const body = {
37
+ query,
38
+ top_k: this.recallTopK,
39
+ include_pending: this.recallIncludePending
40
+ };
41
+ if (typeof this.recallMinSimilarity === 'number') {
42
+ body.min_similarity = this.recallMinSimilarity;
43
+ }
44
+ const res = await fetch(`${this.baseURL}/v1/recall`, {
45
+ method: 'POST',
46
+ headers: this.headers(),
47
+ body: JSON.stringify(body),
48
+ signal,
49
+ });
50
+ if (!res.ok)
51
+ throw new Error(`Persistio recall failed: ${res.status}`);
52
+ const data = await res.json();
53
+ return data.memories ?? [];
32
54
  });
33
- if (!res.ok)
34
- throw new Error(`Persistio recall failed: ${res.status}`);
35
- const data = await res.json();
36
- return data.memories ?? [];
37
- }
38
- async recallBundle(query, topK) {
39
- const body = { query, top_k: topK ?? this.recallTopK, include_pending: true };
40
- if (typeof this.recallMinSimilarity === 'number') {
41
- body.min_similarity = this.recallMinSimilarity;
42
- }
43
- const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
44
- method: 'POST',
45
- headers: this.headers(),
46
- body: JSON.stringify(body),
47
- signal: AbortSignal.timeout(this.recallTimeout),
55
+ }
56
+ async recallBundle(query, topK, options = {}) {
57
+ return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
58
+ const body = {
59
+ query,
60
+ top_k: topK ?? this.recallTopK,
61
+ include_pending: this.recallIncludePending,
62
+ include_related: options.includeRelated ?? this.includeRelatedMemories
63
+ };
64
+ if (typeof this.recallMinSimilarity === 'number') {
65
+ body.min_similarity = this.recallMinSimilarity;
66
+ }
67
+ const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
68
+ method: 'POST',
69
+ headers: this.headers(),
70
+ body: JSON.stringify(body),
71
+ signal,
72
+ });
73
+ if (!res.ok)
74
+ throw new Error(`Persistio recallBundle failed: ${res.status}`);
75
+ const data = await res.json();
76
+ return data;
48
77
  });
49
- if (!res.ok)
50
- throw new Error(`Persistio recallBundle failed: ${res.status}`);
51
- const data = await res.json();
52
- return data;
53
78
  }
54
79
  async ingest(sessionId, chunks) {
55
80
  if (chunks.length === 0)
56
81
  return;
57
- const res = await fetch(`${this.baseURL}/v1/ingest`, {
58
- method: 'POST',
59
- headers: this.headers(),
60
- body: JSON.stringify({ session_id: sessionId, chunks }),
61
- signal: AbortSignal.timeout(this.ingestTimeout),
82
+ await withRequestDeadline('ingest', this.ingestTimeout, async (signal) => {
83
+ const res = await fetch(`${this.baseURL}/v1/ingest`, {
84
+ method: 'POST',
85
+ headers: this.headers(),
86
+ body: JSON.stringify({ session_id: sessionId, chunks }),
87
+ signal,
88
+ });
89
+ if (!res.ok)
90
+ throw new Error(await formatHttpError('ingest', res));
62
91
  });
63
- if (!res.ok)
64
- throw new Error(await formatHttpError('ingest', res));
65
92
  }
66
93
  async addMemory(data, subject) {
67
- const res = await fetch(`${this.baseURL}/v1/memories`, {
68
- method: 'POST',
69
- headers: this.headers(),
70
- body: JSON.stringify({ data, subject }),
94
+ await withRequestDeadline('addMemory', this.writeTimeout, async (signal) => {
95
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
96
+ method: 'POST',
97
+ headers: this.headers(),
98
+ body: JSON.stringify({ data, subject }),
99
+ signal,
100
+ });
101
+ if (!res.ok)
102
+ throw new Error(`Persistio addMemory failed: ${res.status}`);
71
103
  });
72
- if (!res.ok)
73
- throw new Error(`Persistio addMemory failed: ${res.status}`);
74
104
  }
75
105
  async deleteMemory(id) {
76
- const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
77
- method: 'DELETE',
78
- headers: this.headers(),
106
+ await withRequestDeadline('deleteMemory', this.writeTimeout, async (signal) => {
107
+ const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
108
+ method: 'DELETE',
109
+ headers: this.headers(),
110
+ signal,
111
+ });
112
+ if (!res.ok)
113
+ throw new Error(`Persistio deleteMemory failed: ${res.status}`);
79
114
  });
80
- if (!res.ok)
81
- throw new Error(`Persistio deleteMemory failed: ${res.status}`);
82
115
  }
83
116
  async getMemory(id, options = {}) {
84
- const query = options.includePending ? '?include_pending=true' : '';
85
- const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
86
- headers: this.headers(),
117
+ return withRequestDeadline('getMemory', this.recallTimeout, async (signal) => {
118
+ const query = options.includePending ? '?include_pending=true' : '';
119
+ const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
120
+ headers: this.headers(),
121
+ signal,
122
+ });
123
+ if (res.status === 404)
124
+ return null;
125
+ if (!res.ok)
126
+ throw new Error(`Persistio getMemory failed: ${res.status}`);
127
+ return await res.json();
87
128
  });
88
- if (res.status === 404)
89
- return null;
90
- if (!res.ok)
91
- throw new Error(`Persistio getMemory failed: ${res.status}`);
92
- return await res.json();
93
129
  }
94
130
  async listMemories() {
95
- const res = await fetch(`${this.baseURL}/v1/memories`, {
96
- headers: this.headers(),
131
+ return withRequestDeadline('listMemories', this.recallTimeout, async (signal) => {
132
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
133
+ headers: this.headers(),
134
+ signal,
135
+ });
136
+ if (!res.ok)
137
+ throw new Error(`Persistio listMemories failed: ${res.status}`);
138
+ const data = await res.json();
139
+ return data.items ?? [];
97
140
  });
98
- if (!res.ok)
99
- throw new Error(`Persistio listMemories failed: ${res.status}`);
100
- const data = await res.json();
101
- return data.items ?? [];
102
141
  }
103
142
  }
143
+ async function withRequestDeadline(operation, timeoutMs, run) {
144
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
145
+ return run(new AbortController().signal);
146
+ }
147
+ const controller = new AbortController();
148
+ let timeout;
149
+ const deadline = new Promise((_resolve, reject) => {
150
+ timeout = setTimeout(() => {
151
+ controller.abort();
152
+ reject(new PersistioTimeoutError(operation, timeoutMs));
153
+ }, timeoutMs);
154
+ });
155
+ try {
156
+ return await Promise.race([run(controller.signal), deadline]);
157
+ }
158
+ catch (err) {
159
+ if (controller.signal.aborted && isAbortLikeError(err)) {
160
+ throw new PersistioTimeoutError(operation, timeoutMs);
161
+ }
162
+ throw err;
163
+ }
164
+ finally {
165
+ if (timeout)
166
+ clearTimeout(timeout);
167
+ }
168
+ }
169
+ function isAbortLikeError(err) {
170
+ if (!(err instanceof Error))
171
+ return false;
172
+ return err.name === 'AbortError' || err.name === 'TimeoutError';
173
+ }
104
174
  async function formatHttpError(operation, res) {
105
175
  let detail = '';
106
176
  try {