@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.
@@ -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",
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.5",
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(`Persistio ingest failed: ${res.status}`);
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
- type OpenClawMessageRole = 'user' | 'assistant' | 'tool';
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
- chunks.push({ role, content: text, timestamp: ts });
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(() => {