@persistio/openclaw-plugin 0.1.7 → 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 CHANGED
@@ -1,40 +1,62 @@
1
1
  # @persistio/openclaw-plugin
2
2
 
3
- OpenClaw plugin for [Persistio](https://persistio.ai) — persistent semantic memory for AI agents.
3
+ OpenClaw-native long-term memory powered by Persistio.
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
+ 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
- ## Requirements
7
+ ## Design
8
8
 
9
- - A running [Persistio](https://github.com/chriscoveyduck/persistio) instance (`api.persistio.ai` or self-hosted)
10
- - OpenClaw `>=2026.3.24-beta.2`
9
+ Persistio v2 separates the memory surfaces:
11
10
 
12
- ## Installation
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.
16
+
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.
18
+
19
+ ## Install
13
20
 
14
21
  ```bash
15
- openclaw plugins install npm:@persistio/openclaw-plugin
16
- openclaw plugins enable openclaw-persistio
22
+ openclaw plugins install npm:@persistio/openclaw-plugin@0.2.0
23
+ openclaw plugins enable openclaw-persistio-v2
17
24
  openclaw gateway restart
18
- openclaw plugins inspect openclaw-persistio --runtime --json
19
25
  ```
20
26
 
21
- Then register it in your OpenClaw config:
27
+ To test it as the active memory slot:
22
28
 
23
29
  ```json
24
30
  {
25
31
  "plugins": {
32
+ "slots": {
33
+ "memory": "openclaw-persistio-v2"
34
+ },
26
35
  "entries": {
27
- "openclaw-persistio": {
36
+ "openclaw-persistio-v2": {
28
37
  "enabled": true,
29
38
  "package": "@persistio/openclaw-plugin",
39
+ "hooks": {
40
+ "allowConversationAccess": true
41
+ },
30
42
  "config": {
31
43
  "baseURL": "https://api.persistio.ai",
32
44
  "apiKey": "your-vault-api-key",
33
- "recallMinSimilarity": 0.3,
34
- "send": {
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,
35
57
  "roles": {
36
58
  "user": "enabled",
37
- "agent": "enabled",
59
+ "assistant": "bounded",
38
60
  "tool": "disabled"
39
61
  }
40
62
  }
@@ -45,44 +67,55 @@ Then register it in your OpenClaw config:
45
67
  }
46
68
  ```
47
69
 
70
+ `hooks.allowConversationAccess` is required when `autoCapture` is enabled because the plugin reads the completed conversation snapshot from `agent_end`.
71
+
48
72
  ## Configuration
49
73
 
50
- | Option | Type | Required | Default | Description |
51
- |---|---|---|---|---|
52
- | `baseURL` | string | | | Base URL of your Persistio instance |
53
- | `apiKey` | string | | | Vault API key |
54
- | `tokenBudget` | number | | `2000` | Max tokens to inject into the system prompt |
55
- | `recallTopK` | number | | `10` | Number of memories to retrieve per recall |
56
- | `recallMinSimilarity` | number from `0` to `1` | | Persistio server default | Optional semantic recall quality floor |
57
- | `recallTimeout` | number | | `5000` | HTTP timeout for recall requests (ms) |
58
- | `ingest.timeoutMs` | number | | `30000` | HTTP timeout for ingest requests (ms). Timed-out requests are treated as ambiguous and not retried automatically |
59
- | `ingest.maxChunkChars` | number | | `6000` | Maximum characters per chunk sent to Persistio |
60
- | `ingest.maxChunksPerTurn` | number | | `12` | Maximum chunks sent from a single OpenClaw turn |
61
- | `ingest.skipSubagentSessions` | boolean | | `true` | Skip `agent:*` sessions unless they are `agent:main:*` |
62
- | `ingest.user.maxCharsPerMessage` | number | | `24000` | Maximum user-message characters considered for ingest before chunking |
63
- | `ingest.agent.mode` | `"bounded"` or `"raw"` | | `"bounded"` | Assistant ingest shaping mode. `bounded` collapses obvious large noisy blocks before chunking |
64
- | `ingest.agent.maxCharsPerMessage` | number | | `24000` | Maximum assistant-message characters considered after filtering |
65
- | `ingest.agent.maxCharsAfterFiltering` | number | | `9000` | Maximum assistant-message characters retained after deterministic filtering |
66
- | `ingest.agent.maxCharsPerTurn` | number | | `24000` | Maximum assistant-message characters sent from one turn |
67
- | `send.roles.user` | `"enabled"` or `"disabled"` | | `"enabled"` | Send user messages to Persistio ingest |
68
- | `send.roles.agent` | `"enabled"` or `"disabled"` | | `"enabled"` | Send agent/assistant messages to Persistio ingest |
69
- | `send.roles.tool` | `"enabled"` or `"disabled"` | | `"disabled"` | Send tool messages to Persistio ingest |
70
-
71
- 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`.
72
-
73
- `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.
74
-
75
- 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.
76
-
77
- ## Tools exposed
78
-
79
- | Tool | Description |
80
- |---|---|
81
- | `memory_search` | Search memories by semantic query |
82
- | `memory_add` | Manually store a fact |
83
- | `memory_delete` | Delete a memory by ID |
84
- | `memory_list` | List all memories in the vault |
85
-
86
- ## License
87
-
88
- MIT
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;
@@ -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,71 +1,55 @@
1
- import type { PersistioIngestPolicy } from './ingest-policy.js';
2
- export interface PersistioConfig {
3
- baseURL: string;
4
- apiKey: string;
5
- tokenBudget: number;
6
- recallTopK: number;
7
- recallMinSimilarity?: number;
8
- recallTimeout: number;
9
- ingest: PersistioIngestPolicy;
10
- send: PersistioSendConfig;
11
- }
12
- export type PersistioSendRoleStatus = 'enabled' | 'disabled';
13
- export interface PersistioSendConfig {
14
- roles: {
15
- user: PersistioSendRoleStatus;
16
- agent: PersistioSendRoleStatus;
17
- tool: PersistioSendRoleStatus;
18
- };
19
- }
1
+ import type { PersistioV2Config } from './config.js';
20
2
  export interface PersistioMemory {
21
3
  id: string;
22
4
  data: string;
23
5
  subject: string;
24
6
  similarity?: number;
25
- categories: string[];
26
- confidence: number;
27
- }
28
- export interface GetMemoryOptions {
29
- includePending?: boolean;
7
+ confidence?: number;
8
+ categories?: string[];
9
+ source?: string;
10
+ edge_type?: string | null;
30
11
  }
31
12
  export interface RecallBundle {
32
13
  global_user_rules?: string[];
33
- user_rules: string[];
34
- user_preferences: string[];
35
- task_patterns: string[];
36
- workflows: string[];
37
- project: string[];
38
- constraints: string[];
39
- decisions: string[];
40
- system_facts: string[];
41
- domain_knowledge: string[];
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[];
42
23
  }
43
24
  export interface RecallBundleResponse {
44
- bundle: RecallBundle;
25
+ bundle?: RecallBundle;
45
26
  related_bundle?: RecallBundle;
46
27
  }
28
+ export interface RecallResult {
29
+ memories: PersistioMemory[];
30
+ relatedMemories: PersistioMemory[];
31
+ }
32
+ export interface IngestChunk {
33
+ role: string;
34
+ content: string;
35
+ timestamp: string;
36
+ }
47
37
  export declare class PersistioTimeoutError extends Error {
48
38
  constructor(operation: string, timeoutMs: number);
49
39
  }
50
40
  export declare class PersistioClient {
41
+ private readonly config;
51
42
  private readonly baseURL;
52
43
  private readonly apiKey;
53
- private readonly recallTopK;
54
- private readonly recallMinSimilarity?;
55
- private readonly recallTimeout;
56
- private readonly ingestTimeout;
57
- private readonly writeTimeout;
58
- constructor(config: PersistioConfig);
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;
59
53
  private headers;
60
- recall(query: string): Promise<PersistioMemory[]>;
61
- recallBundle(query: string, topK?: number): Promise<RecallBundleResponse>;
62
- ingest(sessionId: string, chunks: Array<{
63
- role: string;
64
- content: string;
65
- timestamp: string;
66
- }>): Promise<void>;
67
- addMemory(data: string, subject: string): Promise<void>;
68
- deleteMemory(id: string): Promise<void>;
69
- getMemory(id: string, options?: GetMemoryOptions): Promise<PersistioMemory | null>;
70
- listMemories(): Promise<PersistioMemory[]>;
71
54
  }
55
+ export declare function withRequestDeadline<T>(operation: string, timeoutMs: number, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
package/dist/client.js CHANGED
@@ -5,34 +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
- ingestTimeout;
14
- writeTimeout;
15
11
  constructor(config) {
12
+ this.config = config;
16
13
  this.baseURL = config.baseURL.replace(/\/$/, '');
17
14
  this.apiKey = config.apiKey;
18
- this.recallTopK = config.recallTopK;
19
- this.recallMinSimilarity = config.recallMinSimilarity;
20
- this.recallTimeout = config.recallTimeout;
21
- this.ingestTimeout = config.ingest.timeoutMs;
22
- this.writeTimeout = config.ingest.timeoutMs;
23
15
  }
24
- headers() {
25
- return {
26
- 'Content-Type': 'application/json',
27
- 'Authorization': `Bearer ${this.apiKey}`,
28
- };
29
- }
30
- async recall(query) {
31
- return withRequestDeadline('recall', this.recallTimeout, async (signal) => {
32
- const body = { query, top_k: this.recallTopK, include_pending: true };
33
- if (typeof this.recallMinSimilarity === 'number') {
34
- body.min_similarity = this.recallMinSimilarity;
35
- }
16
+ async recall(query, options = {}) {
17
+ return withRequestDeadline('recall', this.config.recall.timeoutMs, async (signal) => {
18
+ const body = this.buildRecallBody(query, options.maxResults);
36
19
  const res = await fetch(`${this.baseURL}/v1/recall`, {
37
20
  method: 'POST',
38
21
  headers: this.headers(),
@@ -40,17 +23,19 @@ export class PersistioClient {
40
23
  signal,
41
24
  });
42
25
  if (!res.ok)
43
- throw new Error(`Persistio recall failed: ${res.status}`);
26
+ throw new Error(await formatHttpError('recall', res));
44
27
  const data = await res.json();
45
- return data.memories ?? [];
28
+ return {
29
+ memories: Array.isArray(data.memories) ? data.memories : [],
30
+ relatedMemories: Array.isArray(data.related_memories) ? data.related_memories : [],
31
+ };
46
32
  });
47
33
  }
48
- async recallBundle(query, topK) {
49
- return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
50
- const body = { query, top_k: topK ?? this.recallTopK, include_pending: true };
51
- if (typeof this.recallMinSimilarity === 'number') {
52
- body.min_similarity = this.recallMinSimilarity;
53
- }
34
+ async recallBundle(query) {
35
+ return withRequestDeadline('recallBundle', this.config.recall.timeoutMs, async (signal) => {
36
+ const body = {
37
+ ...this.buildRecallBody(query, this.config.recall.maxResults),
38
+ };
54
39
  const res = await fetch(`${this.baseURL}/v1/recall?format=bundle`, {
55
40
  method: 'POST',
56
41
  headers: this.headers(),
@@ -58,15 +43,14 @@ export class PersistioClient {
58
43
  signal,
59
44
  });
60
45
  if (!res.ok)
61
- throw new Error(`Persistio recallBundle failed: ${res.status}`);
62
- const data = await res.json();
63
- return data;
46
+ throw new Error(await formatHttpError('recallBundle', res));
47
+ return await res.json();
64
48
  });
65
49
  }
66
50
  async ingest(sessionId, chunks) {
67
51
  if (chunks.length === 0)
68
52
  return;
69
- await withRequestDeadline('ingest', this.ingestTimeout, async (signal) => {
53
+ await withRequestDeadline('ingest', this.config.capture.timeoutMs, async (signal) => {
70
54
  const res = await fetch(`${this.baseURL}/v1/ingest`, {
71
55
  method: 'POST',
72
56
  headers: this.headers(),
@@ -77,8 +61,8 @@ export class PersistioClient {
77
61
  throw new Error(await formatHttpError('ingest', res));
78
62
  });
79
63
  }
80
- async addMemory(data, subject) {
81
- await withRequestDeadline('addMemory', this.writeTimeout, async (signal) => {
64
+ async storeMemory(data, subject) {
65
+ return withRequestDeadline('memory_store', this.config.capture.timeoutMs, async (signal) => {
82
66
  const res = await fetch(`${this.baseURL}/v1/memories`, {
83
67
  method: 'POST',
84
68
  headers: this.headers(),
@@ -86,48 +70,41 @@ export class PersistioClient {
86
70
  signal,
87
71
  });
88
72
  if (!res.ok)
89
- throw new Error(`Persistio addMemory failed: ${res.status}`);
73
+ throw new Error(await formatHttpError('memory_store', res));
74
+ return await res.json();
90
75
  });
91
76
  }
92
- async deleteMemory(id) {
93
- await withRequestDeadline('deleteMemory', this.writeTimeout, async (signal) => {
94
- 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)}`, {
95
80
  method: 'DELETE',
96
81
  headers: this.headers(),
97
82
  signal,
98
83
  });
99
84
  if (!res.ok)
100
- throw new Error(`Persistio deleteMemory failed: ${res.status}`);
85
+ throw new Error(await formatHttpError('memory_forget', res));
101
86
  });
102
87
  }
103
- async getMemory(id, options = {}) {
104
- return withRequestDeadline('getMemory', this.recallTimeout, async (signal) => {
105
- const query = options.includePending ? '?include_pending=true' : '';
106
- const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
107
- headers: this.headers(),
108
- signal,
109
- });
110
- if (res.status === 404)
111
- return null;
112
- if (!res.ok)
113
- throw new Error(`Persistio getMemory failed: ${res.status}`);
114
- return await res.json();
115
- });
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;
116
99
  }
117
- async listMemories() {
118
- return withRequestDeadline('listMemories', this.recallTimeout, async (signal) => {
119
- const res = await fetch(`${this.baseURL}/v1/memories`, {
120
- headers: this.headers(),
121
- signal,
122
- });
123
- if (!res.ok)
124
- throw new Error(`Persistio listMemories failed: ${res.status}`);
125
- const data = await res.json();
126
- return data.items ?? [];
127
- });
100
+ headers() {
101
+ return {
102
+ 'Content-Type': 'application/json',
103
+ 'Authorization': `Bearer ${this.apiKey}`,
104
+ };
128
105
  }
129
106
  }
130
- async function withRequestDeadline(operation, timeoutMs, run) {
107
+ export async function withRequestDeadline(operation, timeoutMs, run) {
131
108
  if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
132
109
  return run(new AbortController().signal);
133
110
  }
@@ -154,9 +131,7 @@ async function withRequestDeadline(operation, timeoutMs, run) {
154
131
  }
155
132
  }
156
133
  function isAbortLikeError(err) {
157
- if (!(err instanceof Error))
158
- return false;
159
- return err.name === 'AbortError' || err.name === 'TimeoutError';
134
+ return err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError');
160
135
  }
161
136
  async function formatHttpError(operation, res) {
162
137
  let detail = '';
@@ -164,7 +139,7 @@ async function formatHttpError(operation, res) {
164
139
  detail = (await res.text()).trim().slice(0, 500);
165
140
  }
166
141
  catch {
167
- // Ignore response body read failures; the status is still actionable.
142
+ // Status code is still useful if the body cannot be read.
168
143
  }
169
144
  return detail
170
145
  ? `Persistio ${operation} failed: ${res.status} ${detail}`
@@ -0,0 +1,29 @@
1
+ export type PersistioCaptureRoleStatus = 'enabled' | 'bounded' | 'disabled';
2
+ export interface PersistioV2Config {
3
+ baseURL: string;
4
+ apiKey: string;
5
+ autoRecall: boolean;
6
+ autoCapture: boolean;
7
+ recall: {
8
+ timeoutMs: number;
9
+ maxResults: number;
10
+ tokenBudget: number;
11
+ minSimilarity?: number;
12
+ includePending: boolean;
13
+ includeRelated: boolean;
14
+ queryMaxChars: number;
15
+ };
16
+ capture: {
17
+ timeoutMs: number;
18
+ maxCharsPerTurn: number;
19
+ maxCharsPerMessage: number;
20
+ maxChunksPerTurn: number;
21
+ maxChunkChars: number;
22
+ roles: {
23
+ user: 'enabled' | 'disabled';
24
+ assistant: PersistioCaptureRoleStatus;
25
+ tool: 'enabled' | 'disabled';
26
+ };
27
+ };
28
+ }
29
+ export declare function resolveConfig(raw: unknown): PersistioV2Config;