@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/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,13 +54,27 @@ export interface RecallBundleResponse {
52
54
  related_bundle?: RecallBundle;
53
55
  }
54
56
 
57
+ export interface RecallBundleOptions {
58
+ includeRelated?: boolean;
59
+ }
60
+
61
+ export class PersistioTimeoutError extends Error {
62
+ constructor(operation: string, timeoutMs: number) {
63
+ super(`Persistio ${operation} timed out after ${timeoutMs}ms`);
64
+ this.name = 'TimeoutError';
65
+ }
66
+ }
67
+
55
68
  export class PersistioClient {
56
69
  private readonly baseURL: string;
57
70
  private readonly apiKey: string;
58
71
  private readonly recallTopK: number;
59
72
  private readonly recallMinSimilarity?: number;
60
73
  private readonly recallTimeout: number;
74
+ private readonly recallIncludePending: boolean;
75
+ private readonly includeRelatedMemories: boolean;
61
76
  private readonly ingestTimeout: number;
77
+ private readonly writeTimeout: number;
62
78
 
63
79
  constructor(config: PersistioConfig) {
64
80
  this.baseURL = config.baseURL.replace(/\/$/, '');
@@ -66,7 +82,10 @@ export class PersistioClient {
66
82
  this.recallTopK = config.recallTopK;
67
83
  this.recallMinSimilarity = config.recallMinSimilarity;
68
84
  this.recallTimeout = config.recallTimeout;
85
+ this.recallIncludePending = config.recallIncludePending;
86
+ this.includeRelatedMemories = config.includeRelatedMemories;
69
87
  this.ingestTimeout = config.ingest.timeoutMs;
88
+ this.writeTimeout = config.ingest.timeoutMs;
70
89
  }
71
90
 
72
91
  private headers(): Record<string, string> {
@@ -77,87 +96,150 @@ export class PersistioClient {
77
96
  }
78
97
 
79
98
  async recall(query: string): Promise<PersistioMemory[]> {
80
- const body: Record<string, unknown> = { query, top_k: this.recallTopK, include_pending: true };
81
- if (typeof this.recallMinSimilarity === 'number') {
82
- body.min_similarity = this.recallMinSimilarity;
83
- }
84
-
85
- const res = await fetch(`${this.baseURL}/v1/recall`, {
86
- method: 'POST',
87
- headers: this.headers(),
88
- body: JSON.stringify(body),
89
- signal: AbortSignal.timeout(this.recallTimeout),
99
+ return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
100
+ const body: Record<string, unknown> = {
101
+ query,
102
+ top_k: this.recallTopK,
103
+ include_pending: this.recallIncludePending
104
+ };
105
+ if (typeof this.recallMinSimilarity === 'number') {
106
+ body.min_similarity = this.recallMinSimilarity;
107
+ }
108
+
109
+ const res = await fetch(`${this.baseURL}/v1/recall`, {
110
+ method: 'POST',
111
+ headers: this.headers(),
112
+ body: JSON.stringify(body),
113
+ signal,
114
+ });
115
+ if (!res.ok) throw new Error(`Persistio recall failed: ${res.status}`);
116
+ const data = await res.json() as { memories: PersistioMemory[] };
117
+ return data.memories ?? [];
90
118
  });
91
- if (!res.ok) throw new Error(`Persistio recall failed: ${res.status}`);
92
- const data = await res.json() as { memories: PersistioMemory[] };
93
- return data.memories ?? [];
94
119
  }
95
120
 
96
- async recallBundle(query: string, topK?: number): Promise<RecallBundleResponse> {
97
- const body: Record<string, unknown> = { query, top_k: topK ?? this.recallTopK, include_pending: true };
98
- if (typeof this.recallMinSimilarity === 'number') {
99
- body.min_similarity = this.recallMinSimilarity;
100
- }
101
-
102
- const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
103
- method: 'POST',
104
- headers: this.headers(),
105
- body: JSON.stringify(body),
106
- signal: AbortSignal.timeout(this.recallTimeout),
121
+ async recallBundle(query: string, topK?: number, options: RecallBundleOptions = {}): Promise<RecallBundleResponse> {
122
+ return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
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
+ };
129
+ if (typeof this.recallMinSimilarity === 'number') {
130
+ body.min_similarity = this.recallMinSimilarity;
131
+ }
132
+
133
+ const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
134
+ method: 'POST',
135
+ headers: this.headers(),
136
+ body: JSON.stringify(body),
137
+ signal,
138
+ });
139
+ if (!res.ok) throw new Error(`Persistio recallBundle failed: ${res.status}`);
140
+ const data = await res.json() as RecallBundleResponse;
141
+ return data;
107
142
  });
108
- if (!res.ok) throw new Error(`Persistio recallBundle failed: ${res.status}`);
109
- const data = await res.json() as RecallBundleResponse;
110
- return data;
111
143
  }
112
144
 
113
145
  async ingest(sessionId: string, chunks: Array<{ role: string; content: string; timestamp: string }>): Promise<void> {
114
146
  if (chunks.length === 0) return;
115
- const res = await fetch(`${this.baseURL}/v1/ingest`, {
116
- method: 'POST',
117
- headers: this.headers(),
118
- body: JSON.stringify({ session_id: sessionId, chunks }),
119
- signal: AbortSignal.timeout(this.ingestTimeout),
147
+ await withRequestDeadline('ingest', this.ingestTimeout, async (signal) => {
148
+ const res = await fetch(`${this.baseURL}/v1/ingest`, {
149
+ method: 'POST',
150
+ headers: this.headers(),
151
+ body: JSON.stringify({ session_id: sessionId, chunks }),
152
+ signal,
153
+ });
154
+ if (!res.ok) throw new Error(await formatHttpError('ingest', res));
120
155
  });
121
- if (!res.ok) throw new Error(await formatHttpError('ingest', res));
122
156
  }
123
157
 
124
158
  async addMemory(data: string, subject: string): Promise<void> {
125
- const res = await fetch(`${this.baseURL}/v1/memories`, {
126
- method: 'POST',
127
- headers: this.headers(),
128
- body: JSON.stringify({ data, subject }),
159
+ await withRequestDeadline('addMemory', this.writeTimeout, async (signal) => {
160
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
161
+ method: 'POST',
162
+ headers: this.headers(),
163
+ body: JSON.stringify({ data, subject }),
164
+ signal,
165
+ });
166
+ if (!res.ok) throw new Error(`Persistio addMemory failed: ${res.status}`);
129
167
  });
130
- if (!res.ok) throw new Error(`Persistio addMemory failed: ${res.status}`);
131
168
  }
132
169
 
133
170
  async deleteMemory(id: string): Promise<void> {
134
- const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
135
- method: 'DELETE',
136
- headers: this.headers(),
171
+ await withRequestDeadline('deleteMemory', this.writeTimeout, async (signal) => {
172
+ const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
173
+ method: 'DELETE',
174
+ headers: this.headers(),
175
+ signal,
176
+ });
177
+ if (!res.ok) throw new Error(`Persistio deleteMemory failed: ${res.status}`);
137
178
  });
138
- if (!res.ok) throw new Error(`Persistio deleteMemory failed: ${res.status}`);
139
179
  }
140
180
 
141
181
  async getMemory(id: string, options: GetMemoryOptions = {}): Promise<PersistioMemory | null> {
142
- const query = options.includePending ? '?include_pending=true' : '';
143
- const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
144
- headers: this.headers(),
182
+ return withRequestDeadline('getMemory', this.recallTimeout, async (signal) => {
183
+ const query = options.includePending ? '?include_pending=true' : '';
184
+ const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
185
+ headers: this.headers(),
186
+ signal,
187
+ });
188
+ if (res.status === 404) return null;
189
+ if (!res.ok) throw new Error(`Persistio getMemory failed: ${res.status}`);
190
+ return await res.json() as PersistioMemory;
145
191
  });
146
- if (res.status === 404) return null;
147
- if (!res.ok) throw new Error(`Persistio getMemory failed: ${res.status}`);
148
- return await res.json() as PersistioMemory;
149
192
  }
150
193
 
151
194
  async listMemories(): Promise<PersistioMemory[]> {
152
- const res = await fetch(`${this.baseURL}/v1/memories`, {
153
- headers: this.headers(),
195
+ return withRequestDeadline('listMemories', this.recallTimeout, async (signal) => {
196
+ const res = await fetch(`${this.baseURL}/v1/memories`, {
197
+ headers: this.headers(),
198
+ signal,
199
+ });
200
+ if (!res.ok) throw new Error(`Persistio listMemories failed: ${res.status}`);
201
+ const data = await res.json() as { items: PersistioMemory[] };
202
+ return data.items ?? [];
154
203
  });
155
- if (!res.ok) throw new Error(`Persistio listMemories failed: ${res.status}`);
156
- const data = await res.json() as { items: PersistioMemory[] };
157
- return data.items ?? [];
158
204
  }
159
205
  }
160
206
 
207
+ async function withRequestDeadline<T>(
208
+ operation: string,
209
+ timeoutMs: number,
210
+ run: (signal: AbortSignal) => Promise<T>,
211
+ ): Promise<T> {
212
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
213
+ return run(new AbortController().signal);
214
+ }
215
+
216
+ const controller = new AbortController();
217
+ let timeout: ReturnType<typeof setTimeout> | undefined;
218
+
219
+ const deadline = new Promise<never>((_resolve, reject) => {
220
+ timeout = setTimeout(() => {
221
+ controller.abort();
222
+ reject(new PersistioTimeoutError(operation, timeoutMs));
223
+ }, timeoutMs);
224
+ });
225
+
226
+ try {
227
+ return await Promise.race([run(controller.signal), deadline]);
228
+ } catch (err) {
229
+ if (controller.signal.aborted && isAbortLikeError(err)) {
230
+ throw new PersistioTimeoutError(operation, timeoutMs);
231
+ }
232
+ throw err;
233
+ } finally {
234
+ if (timeout) clearTimeout(timeout);
235
+ }
236
+ }
237
+
238
+ function isAbortLikeError(err: unknown): boolean {
239
+ if (!(err instanceof Error)) return false;
240
+ return err.name === 'AbortError' || err.name === 'TimeoutError';
241
+ }
242
+
161
243
  async function formatHttpError(operation: string, res: Response): Promise<string> {
162
244
  let detail = '';
163
245
  try {