@persistio/openclaw-plugin 0.1.5 → 0.1.6
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 +11 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.js +16 -1
- package/dist/index.js +67 -1
- package/dist/ingest-policy.d.ts +48 -0
- package/dist/ingest-policy.js +380 -0
- package/openclaw.plugin.json +59 -1
- package/package.json +2 -2
- package/src/client.ts +20 -1
- package/src/index.ts +77 -3
- package/src/ingest-policy.ts +508 -0
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.6",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": {
|
|
8
8
|
"onStartup": true
|
|
@@ -39,6 +39,64 @@
|
|
|
39
39
|
"recallTimeout": {
|
|
40
40
|
"type": "number"
|
|
41
41
|
},
|
|
42
|
+
"ingest": {
|
|
43
|
+
"type": "object",
|
|
44
|
+
"additionalProperties": false,
|
|
45
|
+
"properties": {
|
|
46
|
+
"timeoutMs": {
|
|
47
|
+
"type": "number"
|
|
48
|
+
},
|
|
49
|
+
"maxChunkChars": {
|
|
50
|
+
"type": "number"
|
|
51
|
+
},
|
|
52
|
+
"maxChunksPerTurn": {
|
|
53
|
+
"type": "number"
|
|
54
|
+
},
|
|
55
|
+
"skipSubagentSessions": {
|
|
56
|
+
"type": "boolean"
|
|
57
|
+
},
|
|
58
|
+
"user": {
|
|
59
|
+
"type": "object",
|
|
60
|
+
"additionalProperties": false,
|
|
61
|
+
"properties": {
|
|
62
|
+
"maxCharsPerMessage": {
|
|
63
|
+
"type": "number"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"agent": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"properties": {
|
|
71
|
+
"mode": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"enum": [
|
|
74
|
+
"bounded",
|
|
75
|
+
"raw"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"maxCharsPerMessage": {
|
|
79
|
+
"type": "number"
|
|
80
|
+
},
|
|
81
|
+
"maxCharsAfterFiltering": {
|
|
82
|
+
"type": "number"
|
|
83
|
+
},
|
|
84
|
+
"maxCharsPerTurn": {
|
|
85
|
+
"type": "number"
|
|
86
|
+
},
|
|
87
|
+
"largeBlockThresholdChars": {
|
|
88
|
+
"type": "number"
|
|
89
|
+
},
|
|
90
|
+
"largeBlockThresholdLines": {
|
|
91
|
+
"type": "number"
|
|
92
|
+
},
|
|
93
|
+
"maxTableRows": {
|
|
94
|
+
"type": "number"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
42
100
|
"send": {
|
|
43
101
|
"type": "object",
|
|
44
102
|
"additionalProperties": false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@persistio/openclaw-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "OpenClaw plugin for Persistio \u2014 persistent semantic memory for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsc",
|
|
45
|
-
"test": "node test/config-schema.test.mjs"
|
|
45
|
+
"test": "npm run build && node test/config-schema.test.mjs && node test/ingest-policy.test.mjs"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@sinclair/typebox": "^0.34.0"
|
package/src/client.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PersistioIngestPolicy } from './ingest-policy.js';
|
|
2
|
+
|
|
1
3
|
export interface PersistioConfig {
|
|
2
4
|
baseURL: string;
|
|
3
5
|
apiKey: string;
|
|
@@ -5,6 +7,7 @@ export interface PersistioConfig {
|
|
|
5
7
|
recallTopK: number;
|
|
6
8
|
recallMinSimilarity?: number;
|
|
7
9
|
recallTimeout: number;
|
|
10
|
+
ingest: PersistioIngestPolicy;
|
|
8
11
|
send: PersistioSendConfig;
|
|
9
12
|
}
|
|
10
13
|
|
|
@@ -55,6 +58,7 @@ export class PersistioClient {
|
|
|
55
58
|
private readonly recallTopK: number;
|
|
56
59
|
private readonly recallMinSimilarity?: number;
|
|
57
60
|
private readonly recallTimeout: number;
|
|
61
|
+
private readonly ingestTimeout: number;
|
|
58
62
|
|
|
59
63
|
constructor(config: PersistioConfig) {
|
|
60
64
|
this.baseURL = config.baseURL.replace(/\/$/, '');
|
|
@@ -62,6 +66,7 @@ export class PersistioClient {
|
|
|
62
66
|
this.recallTopK = config.recallTopK;
|
|
63
67
|
this.recallMinSimilarity = config.recallMinSimilarity;
|
|
64
68
|
this.recallTimeout = config.recallTimeout;
|
|
69
|
+
this.ingestTimeout = config.ingest.timeoutMs;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
private headers(): Record<string, string> {
|
|
@@ -111,8 +116,9 @@ export class PersistioClient {
|
|
|
111
116
|
method: 'POST',
|
|
112
117
|
headers: this.headers(),
|
|
113
118
|
body: JSON.stringify({ session_id: sessionId, chunks }),
|
|
119
|
+
signal: AbortSignal.timeout(this.ingestTimeout),
|
|
114
120
|
});
|
|
115
|
-
if (!res.ok) throw new Error(
|
|
121
|
+
if (!res.ok) throw new Error(await formatHttpError('ingest', res));
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
async addMemory(data: string, subject: string): Promise<void> {
|
|
@@ -151,3 +157,16 @@ export class PersistioClient {
|
|
|
151
157
|
return data.items ?? [];
|
|
152
158
|
}
|
|
153
159
|
}
|
|
160
|
+
|
|
161
|
+
async function formatHttpError(operation: string, res: Response): Promise<string> {
|
|
162
|
+
let detail = '';
|
|
163
|
+
try {
|
|
164
|
+
detail = (await res.text()).trim().slice(0, 500);
|
|
165
|
+
} catch {
|
|
166
|
+
// Ignore response body read failures; the status is still actionable.
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return detail
|
|
170
|
+
? `Persistio ${operation} failed: ${res.status} ${detail}`
|
|
171
|
+
: `Persistio ${operation} failed: ${res.status}`;
|
|
172
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,8 +7,13 @@ import type {
|
|
|
7
7
|
} from 'openclaw/plugin-sdk/memory-core-host-engine-storage';
|
|
8
8
|
import { Type } from '@sinclair/typebox';
|
|
9
9
|
import { PersistioClient, type PersistioConfig, type PersistioMemory, type RecallBundle } from './client.js';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import {
|
|
11
|
+
prepareMessageForIngest,
|
|
12
|
+
resolveIngestPolicy,
|
|
13
|
+
shouldIngestSession,
|
|
14
|
+
type OpenClawMessageRole,
|
|
15
|
+
type OmissionSummary,
|
|
16
|
+
} from './ingest-policy.js';
|
|
12
17
|
|
|
13
18
|
interface SessionMessageKeyStore {
|
|
14
19
|
keys: Set<string>;
|
|
@@ -58,6 +63,7 @@ function resolveConfig(raw: unknown): PersistioConfig {
|
|
|
58
63
|
recallTopK: typeof c['recallTopK'] === 'number' ? c['recallTopK'] : 10,
|
|
59
64
|
recallMinSimilarity: resolveRecallMinSimilarity(c['recallMinSimilarity']),
|
|
60
65
|
recallTimeout: typeof c['recallTimeout'] === 'number' ? c['recallTimeout'] : 5000,
|
|
66
|
+
ingest: resolveIngestPolicy(c['ingest']),
|
|
61
67
|
send: resolveSendConfig(c),
|
|
62
68
|
};
|
|
63
69
|
}
|
|
@@ -303,6 +309,26 @@ function forgetKeys(target: Set<string>, keys: string[]): void {
|
|
|
303
309
|
for (const key of keys) target.delete(key);
|
|
304
310
|
}
|
|
305
311
|
|
|
312
|
+
function summarizeOmissions(omissions: OmissionSummary[]): string {
|
|
313
|
+
if (omissions.length === 0) return 'none';
|
|
314
|
+
const counts = new Map<string, number>();
|
|
315
|
+
for (const omission of omissions) {
|
|
316
|
+
counts.set(omission.label, (counts.get(omission.label) ?? 0) + 1);
|
|
317
|
+
}
|
|
318
|
+
return [...counts.entries()]
|
|
319
|
+
.map(([label, count]) => `${label}:${count}`)
|
|
320
|
+
.join(',');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isTimeoutLikeError(err: unknown): boolean {
|
|
324
|
+
if (typeof err !== 'object' || err === null) return false;
|
|
325
|
+
const record = err as Record<string, unknown>;
|
|
326
|
+
const name = typeof record['name'] === 'string' ? record['name'] : '';
|
|
327
|
+
if (name === 'TimeoutError' || name === 'AbortError') return true;
|
|
328
|
+
const message = typeof record['message'] === 'string' ? record['message'].toLowerCase() : '';
|
|
329
|
+
return message.includes('timeout') || message.includes('aborted');
|
|
330
|
+
}
|
|
331
|
+
|
|
306
332
|
const PERSISTIO_MEMORY_PATH_PREFIX = 'persistio://memory/';
|
|
307
333
|
|
|
308
334
|
function createClient(config: PersistioConfig, recallTopK = config.recallTopK): PersistioClient {
|
|
@@ -503,8 +529,18 @@ export default definePluginEntry({
|
|
|
503
529
|
try {
|
|
504
530
|
const sessionId = context?.sessionId ?? event.runId ?? 'unknown-session';
|
|
505
531
|
if (sessionId.startsWith('announce:')) return;
|
|
532
|
+
if (!shouldIngestSession(sessionId, cfg.ingest)) {
|
|
533
|
+
api.logger?.debug?.(`openclaw-persistio: ingest skipped non-main session: ${sessionId}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
506
536
|
const chunks: Array<{ role: string; content: string; timestamp: string }> = [];
|
|
507
537
|
const chunkKeys: string[] = [];
|
|
538
|
+
let agentCharsSent = 0;
|
|
539
|
+
let originalChars = 0;
|
|
540
|
+
let preparedChars = 0;
|
|
541
|
+
let truncatedMessages = 0;
|
|
542
|
+
let skippedMessages = 0;
|
|
543
|
+
const omissions: OmissionSummary[] = [];
|
|
508
544
|
const now = Date.now();
|
|
509
545
|
const sentKeys = getSessionKeyStore(sentMessageKeysBySession, sessionId, now);
|
|
510
546
|
const pendingKeys = getSessionKeyStore(pendingMessageKeysBySession, sessionId, now);
|
|
@@ -520,17 +556,55 @@ export default definePluginEntry({
|
|
|
520
556
|
if (sentKeys.has(key) || pendingKeys.has(key)) continue;
|
|
521
557
|
|
|
522
558
|
const ts = resolveMessageTimestamp(m) ?? new Date().toISOString();
|
|
559
|
+
const prepared = prepareMessageForIngest({
|
|
560
|
+
role,
|
|
561
|
+
text,
|
|
562
|
+
policy: cfg.ingest,
|
|
563
|
+
remainingAgentChars: Math.max(0, cfg.ingest.agent.maxCharsPerTurn - agentCharsSent),
|
|
564
|
+
remainingChunks: Math.max(0, cfg.ingest.maxChunksPerTurn - chunks.length),
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
originalChars += prepared.originalChars;
|
|
568
|
+
preparedChars += prepared.preparedChars;
|
|
569
|
+
omissions.push(...prepared.omissions);
|
|
570
|
+
if (prepared.truncated) truncatedMessages += 1;
|
|
571
|
+
if (prepared.chunks.length === 0) {
|
|
572
|
+
skippedMessages += 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
|
|
523
576
|
chunkKeys.push(key);
|
|
524
|
-
|
|
577
|
+
if (role === 'assistant') {
|
|
578
|
+
agentCharsSent += prepared.preparedChars;
|
|
579
|
+
}
|
|
580
|
+
chunks.push(...prepared.chunks.map((content) => ({ role, content, timestamp: ts })));
|
|
581
|
+
|
|
582
|
+
if (chunks.length >= cfg.ingest.maxChunksPerTurn) break;
|
|
525
583
|
}
|
|
526
584
|
|
|
527
585
|
if (chunks.length === 0) return;
|
|
586
|
+
if (truncatedMessages > 0 || omissions.length > 0 || skippedMessages > 0) {
|
|
587
|
+
api.logger?.info?.(
|
|
588
|
+
`openclaw-persistio: ingest planned session=${sessionId} chunks=${chunks.length} `
|
|
589
|
+
+ `originalChars=${originalChars} preparedChars=${preparedChars} `
|
|
590
|
+
+ `truncatedMessages=${truncatedMessages} skippedMessages=${skippedMessages} `
|
|
591
|
+
+ `omissions=${summarizeOmissions(omissions)}`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
528
594
|
rememberKeys(pendingKeys, chunkKeys);
|
|
529
595
|
client.ingest(sessionId, chunks)
|
|
530
596
|
.then(() => {
|
|
531
597
|
rememberKeys(sentKeys, chunkKeys, MAX_SENT_KEYS_PER_SESSION);
|
|
532
598
|
})
|
|
533
599
|
.catch((err: unknown) => {
|
|
600
|
+
if (isTimeoutLikeError(err)) {
|
|
601
|
+
rememberKeys(sentKeys, chunkKeys, MAX_SENT_KEYS_PER_SESSION);
|
|
602
|
+
api.logger?.warn?.(
|
|
603
|
+
`openclaw-persistio: ingest timeout after ${cfg.ingest.timeoutMs}ms; `
|
|
604
|
+
+ `outcome is ambiguous, suppressing retry for ${chunkKeys.length} messages in session=${sessionId}`,
|
|
605
|
+
);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
534
608
|
api.logger?.warn?.(`openclaw-persistio: ingest error: ${String(err)}`);
|
|
535
609
|
})
|
|
536
610
|
.finally(() => {
|