@persistio/openclaw-plugin 0.1.8 → 0.2.0
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 +84 -70
- package/dist/capture.d.ts +17 -0
- package/dist/capture.js +112 -0
- package/dist/client.d.ts +34 -57
- package/dist/client.js +43 -81
- package/dist/config.d.ts +29 -0
- package/dist/config.js +86 -0
- package/dist/index.js +293 -742
- package/dist/memory-format.d.ts +8 -0
- package/dist/memory-format.js +121 -0
- package/openclaw.plugin.json +67 -103
- package/package.json +10 -11
- package/src/capture.ts +132 -0
- package/src/client.ts +70 -128
- package/src/config.ts +125 -0
- package/src/index.ts +301 -860
- package/src/memory-format.ts +127 -0
- package/dist/ingest-policy.d.ts +0 -48
- package/dist/ingest-policy.js +0 -380
- package/src/ingest-policy.ts +0 -508
package/README.md
CHANGED
|
@@ -1,107 +1,121 @@
|
|
|
1
1
|
# @persistio/openclaw-plugin
|
|
2
2
|
|
|
3
|
-
OpenClaw
|
|
3
|
+
OpenClaw-native long-term memory powered by Persistio.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This is the production Persistio plugin for OpenClaw. Version `0.2.x` promotes the OpenClaw-native memory-slot architecture that was tested separately as `openclaw-persistio-v2`.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Design
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- OpenClaw `>=2026.3.24-beta.2`
|
|
9
|
+
Persistio v2 separates the memory surfaces:
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
- `memory_recall` lets the model explicitly retrieve durable Persistio memory.
|
|
12
|
+
- `memory_store` stores a deliberate durable fact, preference, decision, or project note.
|
|
13
|
+
- `memory_forget` deletes a known memory id or returns candidate memories for a query.
|
|
14
|
+
- `autoRecall` optionally injects a small bounded memory block before a turn.
|
|
15
|
+
- `autoCapture` optionally captures bounded post-turn messages without awaiting Persistio from the OpenClaw hook.
|
|
13
16
|
|
|
14
|
-
|
|
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
|
-
```
|
|
17
|
+
The plugin registers as an OpenClaw memory plugin and provides prompt guidance. It does not replace OpenClaw's generic `memory_search` / `memory_get` tools in this first v2 package.
|
|
20
18
|
|
|
21
|
-
|
|
19
|
+
## Install
|
|
22
20
|
|
|
23
21
|
```bash
|
|
24
|
-
openclaw plugins install npm:@persistio/openclaw-plugin@0.
|
|
22
|
+
openclaw plugins install npm:@persistio/openclaw-plugin@0.2.0
|
|
23
|
+
openclaw plugins enable openclaw-persistio-v2
|
|
25
24
|
openclaw gateway restart
|
|
26
|
-
openclaw plugins inspect openclaw-persistio --runtime --json
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
To test it as the active memory slot:
|
|
30
28
|
|
|
31
29
|
```json
|
|
32
30
|
{
|
|
33
31
|
"plugins": {
|
|
32
|
+
"slots": {
|
|
33
|
+
"memory": "openclaw-persistio-v2"
|
|
34
|
+
},
|
|
34
35
|
"entries": {
|
|
35
|
-
"openclaw-persistio": {
|
|
36
|
+
"openclaw-persistio-v2": {
|
|
36
37
|
"enabled": true,
|
|
37
38
|
"package": "@persistio/openclaw-plugin",
|
|
39
|
+
"hooks": {
|
|
40
|
+
"allowConversationAccess": true
|
|
41
|
+
},
|
|
38
42
|
"config": {
|
|
39
43
|
"baseURL": "https://api.persistio.ai",
|
|
40
44
|
"apiKey": "your-vault-api-key",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
"autoRecall": true,
|
|
46
|
+
"autoCapture": true,
|
|
47
|
+
"recall": {
|
|
48
|
+
"timeoutMs": 1200,
|
|
49
|
+
"maxResults": 4,
|
|
50
|
+
"tokenBudget": 400,
|
|
51
|
+
"minSimilarity": 0.45,
|
|
52
|
+
"includePending": false,
|
|
53
|
+
"includeRelated": false
|
|
54
|
+
},
|
|
55
|
+
"capture": {
|
|
56
|
+
"timeoutMs": 10000,
|
|
46
57
|
"roles": {
|
|
47
58
|
"user": "enabled",
|
|
48
|
-
"
|
|
59
|
+
"assistant": "bounded",
|
|
49
60
|
"tool": "disabled"
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
}
|
|
54
|
-
},
|
|
55
|
-
"slots": {
|
|
56
|
-
"memory": "openclaw-persistio"
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
68
|
```
|
|
61
69
|
|
|
70
|
+
`hooks.allowConversationAccess` is required when `autoCapture` is enabled because the plugin reads the completed conversation snapshot from `agent_end`.
|
|
71
|
+
|
|
62
72
|
## Configuration
|
|
63
73
|
|
|
64
|
-
| Option |
|
|
65
|
-
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
74
|
+
| Option | Default | Description |
|
|
75
|
+
|---|---:|---|
|
|
76
|
+
| `autoRecall` | `true` | Inject a small fail-open memory block before the model turn |
|
|
77
|
+
| `autoCapture` | `true` | Capture bounded post-turn messages asynchronously |
|
|
78
|
+
| `recall.timeoutMs` | `1200` | HTTP timeout for recall and recall-bundle calls |
|
|
79
|
+
| `recall.maxResults` | `4` | Maximum memories returned by recall |
|
|
80
|
+
| `recall.tokenBudget` | `400` | Approximate prompt-token budget for auto-recall |
|
|
81
|
+
| `recall.minSimilarity` | unset | Optional Persistio similarity floor |
|
|
82
|
+
| `recall.includePending` | `false` | Include pending candidate memories in hot-path recall |
|
|
83
|
+
| `recall.includeRelated` | `false` | Include graph-related memories in hot-path recall |
|
|
84
|
+
| `recall.queryMaxChars` | `1200` | Maximum latest-user query characters embedded for recall |
|
|
85
|
+
| `capture.timeoutMs` | `10000` | HTTP timeout for post-turn ingest and manual writes |
|
|
86
|
+
| `capture.maxCharsPerTurn` | `6000` | Maximum captured characters per turn |
|
|
87
|
+
| `capture.maxCharsPerMessage` | `3000` | Maximum captured characters per message |
|
|
88
|
+
| `capture.maxChunksPerTurn` | `4` | Maximum chunks sent to Persistio per turn |
|
|
89
|
+
| `capture.maxChunkChars` | `2000` | Maximum characters per capture chunk |
|
|
90
|
+
| `capture.roles.user` | `enabled` | Capture user messages |
|
|
91
|
+
| `capture.roles.assistant` | `bounded` | Capture assistant messages after deterministic noise filtering |
|
|
92
|
+
| `capture.roles.tool` | `disabled` | Capture tool messages |
|
|
93
|
+
|
|
94
|
+
## Upgrade from 0.1.x
|
|
95
|
+
|
|
96
|
+
Install the `0.2.x` package, configure `openclaw-persistio-v2`, and point the OpenClaw memory slot at the new plugin id:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"plugins": {
|
|
101
|
+
"slots": {
|
|
102
|
+
"memory": "openclaw-persistio-v2"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Keep the old `openclaw-persistio` entry disabled or remove it after confirming the new slot behaves correctly. The v2 id is intentionally distinct so operators opt into the new memory-slot behavior instead of silently changing an existing v1 install.
|
|
109
|
+
|
|
110
|
+
## Benchmark Posture
|
|
111
|
+
|
|
112
|
+
For behavioral benchmark work, leave `autoRecall=true` and `autoCapture=true`, keep recall under a tight timeout, and keep `includePending` / `includeRelated` off unless the specific benchmark requires them.
|
|
113
|
+
|
|
114
|
+
The expected turn shape is:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
OpenClaw turn
|
|
118
|
+
-> Persistio autoRecall, bounded and fail-open
|
|
119
|
+
-> model answers with a tiny memory block
|
|
120
|
+
-> Persistio autoCapture, async and non-blocking
|
|
121
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { IngestChunk } from './client.js';
|
|
2
|
+
import type { PersistioV2Config } from './config.js';
|
|
3
|
+
export interface PreparedCapture {
|
|
4
|
+
chunks: IngestChunk[];
|
|
5
|
+
keys: string[];
|
|
6
|
+
items: CaptureItem[];
|
|
7
|
+
}
|
|
8
|
+
export interface CaptureItem {
|
|
9
|
+
key: string;
|
|
10
|
+
chunks: IngestChunk[];
|
|
11
|
+
}
|
|
12
|
+
export interface PrepareCaptureOptions {
|
|
13
|
+
shouldIncludeKey?: (key: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function prepareCapture(event: {
|
|
16
|
+
messages?: unknown[];
|
|
17
|
+
}, cfg: PersistioV2Config, options?: PrepareCaptureOptions): PreparedCapture;
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { extractTextFromMessage } from './memory-format.js';
|
|
2
|
+
export function prepareCapture(event, cfg, options = {}) {
|
|
3
|
+
if (!Array.isArray(event.messages))
|
|
4
|
+
return { chunks: [], keys: [], items: [] };
|
|
5
|
+
const chunks = [];
|
|
6
|
+
const keys = [];
|
|
7
|
+
const items = [];
|
|
8
|
+
let turnChars = 0;
|
|
9
|
+
for (const [index, message] of event.messages.entries()) {
|
|
10
|
+
const role = normalizeRole(message);
|
|
11
|
+
if (!role || !shouldCaptureRole(role, cfg))
|
|
12
|
+
continue;
|
|
13
|
+
const rawText = extractTextFromMessage(message);
|
|
14
|
+
const key = messageKey(message, role, rawText, index);
|
|
15
|
+
if (options.shouldIncludeKey && !options.shouldIncludeKey(key))
|
|
16
|
+
continue;
|
|
17
|
+
if (chunks.length >= cfg.capture.maxChunksPerTurn)
|
|
18
|
+
break;
|
|
19
|
+
const preparedText = prepareTextForRole(rawText, role, cfg);
|
|
20
|
+
if (!preparedText)
|
|
21
|
+
continue;
|
|
22
|
+
const remainingTurnChars = cfg.capture.maxCharsPerTurn - turnChars;
|
|
23
|
+
if (remainingTurnChars <= 0)
|
|
24
|
+
break;
|
|
25
|
+
const boundedText = truncate(preparedText, Math.min(cfg.capture.maxCharsPerMessage, remainingTurnChars));
|
|
26
|
+
const itemChunks = [];
|
|
27
|
+
for (const chunk of chunkText(boundedText, cfg.capture.maxChunkChars)) {
|
|
28
|
+
if (chunks.length >= cfg.capture.maxChunksPerTurn)
|
|
29
|
+
break;
|
|
30
|
+
const ingestChunk = {
|
|
31
|
+
role,
|
|
32
|
+
content: chunk,
|
|
33
|
+
timestamp: resolveTimestamp(message) ?? new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
chunks.push(ingestChunk);
|
|
36
|
+
itemChunks.push(ingestChunk);
|
|
37
|
+
turnChars += chunk.length;
|
|
38
|
+
}
|
|
39
|
+
if (itemChunks.length > 0) {
|
|
40
|
+
keys.push(key);
|
|
41
|
+
items.push({ key, chunks: itemChunks });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { chunks, keys, items };
|
|
45
|
+
}
|
|
46
|
+
function normalizeRole(message) {
|
|
47
|
+
if (typeof message !== 'object' || message === null)
|
|
48
|
+
return null;
|
|
49
|
+
const role = message['role'];
|
|
50
|
+
return role === 'user' || role === 'assistant' || role === 'tool' ? role : null;
|
|
51
|
+
}
|
|
52
|
+
function shouldCaptureRole(role, cfg) {
|
|
53
|
+
if (role === 'user')
|
|
54
|
+
return cfg.capture.roles.user === 'enabled';
|
|
55
|
+
if (role === 'assistant')
|
|
56
|
+
return cfg.capture.roles.assistant !== 'disabled';
|
|
57
|
+
return cfg.capture.roles.tool === 'enabled';
|
|
58
|
+
}
|
|
59
|
+
function prepareTextForRole(text, role, cfg) {
|
|
60
|
+
const normalized = normalizeText(text);
|
|
61
|
+
if (!normalized)
|
|
62
|
+
return '';
|
|
63
|
+
if (role !== 'assistant' || cfg.capture.roles.assistant !== 'bounded') {
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
return normalized
|
|
67
|
+
.replace(/```[\s\S]*?```/g, '[Code block omitted from memory capture]')
|
|
68
|
+
.replace(/\n(?:[|].*[|]\n){8,}/g, '\n[Large table omitted from memory capture]\n')
|
|
69
|
+
.replace(/\n(?:[-+].*\n){20,}/g, '\n[Large diff/log omitted from memory capture]\n')
|
|
70
|
+
.trim();
|
|
71
|
+
}
|
|
72
|
+
function normalizeText(text) {
|
|
73
|
+
return text
|
|
74
|
+
.replace(/\r\n?/g, '\n')
|
|
75
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
76
|
+
.replace(/\n{4,}/g, '\n\n\n')
|
|
77
|
+
.trim();
|
|
78
|
+
}
|
|
79
|
+
function chunkText(text, maxChars) {
|
|
80
|
+
if (text.length <= maxChars)
|
|
81
|
+
return [text];
|
|
82
|
+
const chunks = [];
|
|
83
|
+
for (let start = 0; start < text.length; start += maxChars) {
|
|
84
|
+
const chunk = text.slice(start, start + maxChars).trim();
|
|
85
|
+
if (chunk)
|
|
86
|
+
chunks.push(chunk);
|
|
87
|
+
}
|
|
88
|
+
return chunks;
|
|
89
|
+
}
|
|
90
|
+
function truncate(text, maxChars) {
|
|
91
|
+
if (text.length <= maxChars)
|
|
92
|
+
return text;
|
|
93
|
+
return `${text.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
|
|
94
|
+
}
|
|
95
|
+
function resolveTimestamp(message) {
|
|
96
|
+
if (typeof message !== 'object' || message === null)
|
|
97
|
+
return undefined;
|
|
98
|
+
const value = message['timestamp'];
|
|
99
|
+
if (typeof value === 'string')
|
|
100
|
+
return value;
|
|
101
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
102
|
+
return new Date(value).toISOString();
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
function messageKey(message, role, text, index) {
|
|
106
|
+
if (typeof message === 'object' && message !== null) {
|
|
107
|
+
const id = message['id'];
|
|
108
|
+
if (typeof id === 'string' && id)
|
|
109
|
+
return `${role}:${id}`;
|
|
110
|
+
}
|
|
111
|
+
return `${role}:${index}:${text.slice(0, 300)}`;
|
|
112
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,78 +1,55 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export interface PersistioConfig {
|
|
3
|
-
baseURL: string;
|
|
4
|
-
apiKey: string;
|
|
5
|
-
tokenBudget: number;
|
|
6
|
-
recallTopK: number;
|
|
7
|
-
recallMinSimilarity?: number;
|
|
8
|
-
recallTimeout: number;
|
|
9
|
-
recallIncludePending: boolean;
|
|
10
|
-
includeRelatedMemories: boolean;
|
|
11
|
-
ingest: PersistioIngestPolicy;
|
|
12
|
-
send: PersistioSendConfig;
|
|
13
|
-
}
|
|
14
|
-
export type PersistioSendRoleStatus = 'enabled' | 'disabled';
|
|
15
|
-
export interface PersistioSendConfig {
|
|
16
|
-
roles: {
|
|
17
|
-
user: PersistioSendRoleStatus;
|
|
18
|
-
agent: PersistioSendRoleStatus;
|
|
19
|
-
tool: PersistioSendRoleStatus;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
1
|
+
import type { PersistioV2Config } from './config.js';
|
|
22
2
|
export interface PersistioMemory {
|
|
23
3
|
id: string;
|
|
24
4
|
data: string;
|
|
25
5
|
subject: string;
|
|
26
6
|
similarity?: number;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
includePending?: boolean;
|
|
7
|
+
confidence?: number;
|
|
8
|
+
categories?: string[];
|
|
9
|
+
source?: string;
|
|
10
|
+
edge_type?: string | null;
|
|
32
11
|
}
|
|
33
12
|
export interface RecallBundle {
|
|
34
13
|
global_user_rules?: string[];
|
|
35
|
-
user_rules
|
|
36
|
-
user_preferences
|
|
37
|
-
task_patterns
|
|
38
|
-
workflows
|
|
39
|
-
project
|
|
40
|
-
constraints
|
|
41
|
-
decisions
|
|
42
|
-
system_facts
|
|
43
|
-
domain_knowledge
|
|
14
|
+
user_rules?: string[];
|
|
15
|
+
user_preferences?: string[];
|
|
16
|
+
task_patterns?: string[];
|
|
17
|
+
workflows?: string[];
|
|
18
|
+
project?: string[];
|
|
19
|
+
constraints?: string[];
|
|
20
|
+
decisions?: string[];
|
|
21
|
+
system_facts?: string[];
|
|
22
|
+
domain_knowledge?: string[];
|
|
44
23
|
}
|
|
45
24
|
export interface RecallBundleResponse {
|
|
46
|
-
bundle
|
|
25
|
+
bundle?: RecallBundle;
|
|
47
26
|
related_bundle?: RecallBundle;
|
|
48
27
|
}
|
|
49
|
-
export interface
|
|
50
|
-
|
|
28
|
+
export interface RecallResult {
|
|
29
|
+
memories: PersistioMemory[];
|
|
30
|
+
relatedMemories: PersistioMemory[];
|
|
31
|
+
}
|
|
32
|
+
export interface IngestChunk {
|
|
33
|
+
role: string;
|
|
34
|
+
content: string;
|
|
35
|
+
timestamp: string;
|
|
51
36
|
}
|
|
52
37
|
export declare class PersistioTimeoutError extends Error {
|
|
53
38
|
constructor(operation: string, timeoutMs: number);
|
|
54
39
|
}
|
|
55
40
|
export declare class PersistioClient {
|
|
41
|
+
private readonly config;
|
|
56
42
|
private readonly baseURL;
|
|
57
43
|
private readonly apiKey;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
44
|
+
constructor(config: PersistioV2Config);
|
|
45
|
+
recall(query: string, options?: {
|
|
46
|
+
maxResults?: number;
|
|
47
|
+
}): Promise<RecallResult>;
|
|
48
|
+
recallBundle(query: string): Promise<RecallBundleResponse>;
|
|
49
|
+
ingest(sessionId: string, chunks: IngestChunk[]): Promise<void>;
|
|
50
|
+
storeMemory(data: string, subject: string): Promise<PersistioMemory>;
|
|
51
|
+
forgetMemory(id: string): Promise<void>;
|
|
52
|
+
private buildRecallBody;
|
|
66
53
|
private headers;
|
|
67
|
-
recall(query: string): Promise<PersistioMemory[]>;
|
|
68
|
-
recallBundle(query: string, topK?: number, options?: RecallBundleOptions): Promise<RecallBundleResponse>;
|
|
69
|
-
ingest(sessionId: string, chunks: Array<{
|
|
70
|
-
role: string;
|
|
71
|
-
content: string;
|
|
72
|
-
timestamp: string;
|
|
73
|
-
}>): Promise<void>;
|
|
74
|
-
addMemory(data: string, subject: string): Promise<void>;
|
|
75
|
-
deleteMemory(id: string): Promise<void>;
|
|
76
|
-
getMemory(id: string, options?: GetMemoryOptions): Promise<PersistioMemory | null>;
|
|
77
|
-
listMemories(): Promise<PersistioMemory[]>;
|
|
78
54
|
}
|
|
55
|
+
export declare function withRequestDeadline<T>(operation: string, timeoutMs: number, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
package/dist/client.js
CHANGED
|
@@ -5,42 +5,17 @@ export class PersistioTimeoutError extends Error {
|
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
7
|
export class PersistioClient {
|
|
8
|
+
config;
|
|
8
9
|
baseURL;
|
|
9
10
|
apiKey;
|
|
10
|
-
recallTopK;
|
|
11
|
-
recallMinSimilarity;
|
|
12
|
-
recallTimeout;
|
|
13
|
-
recallIncludePending;
|
|
14
|
-
includeRelatedMemories;
|
|
15
|
-
ingestTimeout;
|
|
16
|
-
writeTimeout;
|
|
17
11
|
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
18
13
|
this.baseURL = config.baseURL.replace(/\/$/, '');
|
|
19
14
|
this.apiKey = config.apiKey;
|
|
20
|
-
this.recallTopK = config.recallTopK;
|
|
21
|
-
this.recallMinSimilarity = config.recallMinSimilarity;
|
|
22
|
-
this.recallTimeout = config.recallTimeout;
|
|
23
|
-
this.recallIncludePending = config.recallIncludePending;
|
|
24
|
-
this.includeRelatedMemories = config.includeRelatedMemories;
|
|
25
|
-
this.ingestTimeout = config.ingest.timeoutMs;
|
|
26
|
-
this.writeTimeout = config.ingest.timeoutMs;
|
|
27
15
|
}
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
|
|
31
|
-
'Authorization': `Bearer ${this.apiKey}`,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
async recall(query) {
|
|
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
|
-
}
|
|
16
|
+
async recall(query, options = {}) {
|
|
17
|
+
return withRequestDeadline('recall', this.config.recall.timeoutMs, async (signal) => {
|
|
18
|
+
const body = this.buildRecallBody(query, options.maxResults);
|
|
44
19
|
const res = await fetch(`${this.baseURL}/v1/recall`, {
|
|
45
20
|
method: 'POST',
|
|
46
21
|
headers: this.headers(),
|
|
@@ -48,22 +23,19 @@ export class PersistioClient {
|
|
|
48
23
|
signal,
|
|
49
24
|
});
|
|
50
25
|
if (!res.ok)
|
|
51
|
-
throw new Error(
|
|
26
|
+
throw new Error(await formatHttpError('recall', res));
|
|
52
27
|
const data = await res.json();
|
|
53
|
-
return
|
|
28
|
+
return {
|
|
29
|
+
memories: Array.isArray(data.memories) ? data.memories : [],
|
|
30
|
+
relatedMemories: Array.isArray(data.related_memories) ? data.related_memories : [],
|
|
31
|
+
};
|
|
54
32
|
});
|
|
55
33
|
}
|
|
56
|
-
async recallBundle(query
|
|
57
|
-
return withRequestDeadline('recallBundle', this.
|
|
34
|
+
async recallBundle(query) {
|
|
35
|
+
return withRequestDeadline('recallBundle', this.config.recall.timeoutMs, async (signal) => {
|
|
58
36
|
const body = {
|
|
59
|
-
query,
|
|
60
|
-
top_k: topK ?? this.recallTopK,
|
|
61
|
-
include_pending: this.recallIncludePending,
|
|
62
|
-
include_related: options.includeRelated ?? this.includeRelatedMemories
|
|
37
|
+
...this.buildRecallBody(query, this.config.recall.maxResults),
|
|
63
38
|
};
|
|
64
|
-
if (typeof this.recallMinSimilarity === 'number') {
|
|
65
|
-
body.min_similarity = this.recallMinSimilarity;
|
|
66
|
-
}
|
|
67
39
|
const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
|
|
68
40
|
method: 'POST',
|
|
69
41
|
headers: this.headers(),
|
|
@@ -71,15 +43,14 @@ export class PersistioClient {
|
|
|
71
43
|
signal,
|
|
72
44
|
});
|
|
73
45
|
if (!res.ok)
|
|
74
|
-
throw new Error(
|
|
75
|
-
|
|
76
|
-
return data;
|
|
46
|
+
throw new Error(await formatHttpError('recallBundle', res));
|
|
47
|
+
return await res.json();
|
|
77
48
|
});
|
|
78
49
|
}
|
|
79
50
|
async ingest(sessionId, chunks) {
|
|
80
51
|
if (chunks.length === 0)
|
|
81
52
|
return;
|
|
82
|
-
await withRequestDeadline('ingest', this.
|
|
53
|
+
await withRequestDeadline('ingest', this.config.capture.timeoutMs, async (signal) => {
|
|
83
54
|
const res = await fetch(`${this.baseURL}/v1/ingest`, {
|
|
84
55
|
method: 'POST',
|
|
85
56
|
headers: this.headers(),
|
|
@@ -90,8 +61,8 @@ export class PersistioClient {
|
|
|
90
61
|
throw new Error(await formatHttpError('ingest', res));
|
|
91
62
|
});
|
|
92
63
|
}
|
|
93
|
-
async
|
|
94
|
-
|
|
64
|
+
async storeMemory(data, subject) {
|
|
65
|
+
return withRequestDeadline('memory_store', this.config.capture.timeoutMs, async (signal) => {
|
|
95
66
|
const res = await fetch(`${this.baseURL}/v1/memories`, {
|
|
96
67
|
method: 'POST',
|
|
97
68
|
headers: this.headers(),
|
|
@@ -99,48 +70,41 @@ export class PersistioClient {
|
|
|
99
70
|
signal,
|
|
100
71
|
});
|
|
101
72
|
if (!res.ok)
|
|
102
|
-
throw new Error(
|
|
73
|
+
throw new Error(await formatHttpError('memory_store', res));
|
|
74
|
+
return await res.json();
|
|
103
75
|
});
|
|
104
76
|
}
|
|
105
|
-
async
|
|
106
|
-
await withRequestDeadline('
|
|
107
|
-
const res = await fetch(`${this.baseURL}/v1/memories/${id}`, {
|
|
77
|
+
async forgetMemory(id) {
|
|
78
|
+
await withRequestDeadline('memory_forget', this.config.capture.timeoutMs, async (signal) => {
|
|
79
|
+
const res = await fetch(`${this.baseURL}/v1/memories/${encodeURIComponent(id)}`, {
|
|
108
80
|
method: 'DELETE',
|
|
109
81
|
headers: this.headers(),
|
|
110
82
|
signal,
|
|
111
83
|
});
|
|
112
84
|
if (!res.ok)
|
|
113
|
-
throw new Error(
|
|
85
|
+
throw new Error(await formatHttpError('memory_forget', res));
|
|
114
86
|
});
|
|
115
87
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return await res.json();
|
|
128
|
-
});
|
|
88
|
+
buildRecallBody(query, maxResults = this.config.recall.maxResults) {
|
|
89
|
+
const body = {
|
|
90
|
+
query,
|
|
91
|
+
top_k: maxResults,
|
|
92
|
+
include_pending: this.config.recall.includePending,
|
|
93
|
+
include_related: this.config.recall.includeRelated,
|
|
94
|
+
};
|
|
95
|
+
if (typeof this.config.recall.minSimilarity === 'number') {
|
|
96
|
+
body.min_similarity = this.config.recall.minSimilarity;
|
|
97
|
+
}
|
|
98
|
+
return body;
|
|
129
99
|
}
|
|
130
|
-
|
|
131
|
-
return
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
if (!res.ok)
|
|
137
|
-
throw new Error(`Persistio listMemories failed: ${res.status}`);
|
|
138
|
-
const data = await res.json();
|
|
139
|
-
return data.items ?? [];
|
|
140
|
-
});
|
|
100
|
+
headers() {
|
|
101
|
+
return {
|
|
102
|
+
'Content-Type': 'application/json',
|
|
103
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
104
|
+
};
|
|
141
105
|
}
|
|
142
106
|
}
|
|
143
|
-
async function withRequestDeadline(operation, timeoutMs, run) {
|
|
107
|
+
export async function withRequestDeadline(operation, timeoutMs, run) {
|
|
144
108
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
145
109
|
return run(new AbortController().signal);
|
|
146
110
|
}
|
|
@@ -167,9 +131,7 @@ async function withRequestDeadline(operation, timeoutMs, run) {
|
|
|
167
131
|
}
|
|
168
132
|
}
|
|
169
133
|
function isAbortLikeError(err) {
|
|
170
|
-
|
|
171
|
-
return false;
|
|
172
|
-
return err.name === 'AbortError' || err.name === 'TimeoutError';
|
|
134
|
+
return err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError');
|
|
173
135
|
}
|
|
174
136
|
async function formatHttpError(operation, res) {
|
|
175
137
|
let detail = '';
|
|
@@ -177,7 +139,7 @@ async function formatHttpError(operation, res) {
|
|
|
177
139
|
detail = (await res.text()).trim().slice(0, 500);
|
|
178
140
|
}
|
|
179
141
|
catch {
|
|
180
|
-
//
|
|
142
|
+
// Status code is still useful if the body cannot be read.
|
|
181
143
|
}
|
|
182
144
|
return detail
|
|
183
145
|
? `Persistio ${operation} failed: ${res.status} ${detail}`
|