@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 CHANGED
@@ -1,107 +1,121 @@
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 recall a small memory context into prompts and ingest new conversation turns after each run. Registers as an OpenClaw memory provider with compatible `memory_search` and `memory_get` tools, plus optional Persistio management tools under the `persistio_*` namespace.
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.
13
16
 
14
- ```bash
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
- To upgrade an existing install, use the same pinned npm source and restart the Gateway:
19
+ ## Install
22
20
 
23
21
  ```bash
24
- openclaw plugins install npm:@persistio/openclaw-plugin@0.1.8
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
- Then register it in your OpenClaw config:
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
- "tokenBudget": 400,
42
- "recallTopK": 4,
43
- "recallTimeout": 1500,
44
- "recallMinSimilarity": 0.3,
45
- "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,
46
57
  "roles": {
47
58
  "user": "enabled",
48
- "agent": "enabled",
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 | Type | Required | Default | Description |
65
- |---|---|---|---|---|
66
- | `baseURL` | string | | | Base URL of your Persistio instance |
67
- | `apiKey` | string | | | Vault API key |
68
- | `tokenBudget` | number | | `400` | Max tokens to inject into the system prompt |
69
- | `recallTopK` | number | | `4` | Number of memories to retrieve per recall |
70
- | `recallMinSimilarity` | number from `0` to `1` | | Persistio server default | Optional semantic recall quality floor |
71
- | `recallTimeout` | number | | `1500` | HTTP timeout for recall requests (ms) |
72
- | `recallIncludePending` | boolean | | `false` | Include fresh candidate memories in recall results |
73
- | `includeRelatedMemories` | boolean | | `false` | Include graph-related memories in prompt recall bundles |
74
- | `ingest.timeoutMs` | number | | `30000` | HTTP timeout for ingest requests (ms). Timed-out requests are treated as ambiguous and not retried automatically |
75
- | `ingest.maxChunkChars` | number | | `6000` | Maximum characters per chunk sent to Persistio |
76
- | `ingest.maxChunksPerTurn` | number | | `12` | Maximum chunks sent from a single OpenClaw turn |
77
- | `ingest.skipSubagentSessions` | boolean | | `true` | Skip `agent:*` sessions unless they are `agent:main:*` |
78
- | `ingest.user.maxCharsPerMessage` | number | | `24000` | Maximum user-message characters considered for ingest before chunking |
79
- | `ingest.agent.mode` | `"bounded"` or `"raw"` | | `"bounded"` | Assistant ingest shaping mode. `bounded` collapses obvious large noisy blocks before chunking |
80
- | `ingest.agent.maxCharsPerMessage` | number | | `24000` | Maximum assistant-message characters considered after filtering |
81
- | `ingest.agent.maxCharsAfterFiltering` | number | | `9000` | Maximum assistant-message characters retained after deterministic filtering |
82
- | `ingest.agent.maxCharsPerTurn` | number | | `24000` | Maximum assistant-message characters sent from one turn |
83
- | `send.roles.user` | `"enabled"` or `"disabled"` | | `"enabled"` | Send user messages to Persistio ingest |
84
- | `send.roles.agent` | `"enabled"` or `"disabled"` | | `"enabled"` | Send agent/assistant messages to Persistio ingest |
85
- | `send.roles.tool` | `"enabled"` or `"disabled"` | | `"disabled"` | Send tool messages to Persistio ingest |
86
-
87
- 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`.
88
-
89
- Prompt recall intentionally defaults to a small direct semantic bundle. `includeRelatedMemories` and `recallIncludePending` are opt-in because graph expansion and fresh candidates increase context size and tail latency on interactive channels.
90
-
91
- `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.
92
-
93
- 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.
94
-
95
- ## Tools exposed
96
-
97
- | Tool | Description |
98
- |---|---|
99
- | `memory_search` | Required OpenClaw-compatible semantic memory search. Returns bounded structured results with `persistio://memory/<id>` paths |
100
- | `memory_get` | Required OpenClaw-compatible exact memory read for paths returned by `memory_search` |
101
- | `persistio_memory_add` | Optional manual fact store |
102
- | `persistio_memory_delete` | Optional memory deletion by ID |
103
- | `persistio_memory_list` | Optional vault memory listing |
104
-
105
- ## License
106
-
107
- 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,78 +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
- 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
- categories: string[];
28
- confidence: number;
29
- }
30
- export interface GetMemoryOptions {
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: string[];
36
- user_preferences: string[];
37
- task_patterns: string[];
38
- workflows: string[];
39
- project: string[];
40
- constraints: string[];
41
- decisions: string[];
42
- system_facts: string[];
43
- 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[];
44
23
  }
45
24
  export interface RecallBundleResponse {
46
- bundle: RecallBundle;
25
+ bundle?: RecallBundle;
47
26
  related_bundle?: RecallBundle;
48
27
  }
49
- export interface RecallBundleOptions {
50
- includeRelated?: boolean;
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
- private readonly recallTopK;
59
- private readonly recallMinSimilarity?;
60
- private readonly recallTimeout;
61
- private readonly recallIncludePending;
62
- private readonly includeRelatedMemories;
63
- private readonly ingestTimeout;
64
- private readonly writeTimeout;
65
- 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;
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
- headers() {
29
- return {
30
- 'Content-Type': 'application/json',
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(`Persistio recall failed: ${res.status}`);
26
+ throw new Error(await formatHttpError('recall', res));
52
27
  const data = await res.json();
53
- return data.memories ?? [];
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, topK, options = {}) {
57
- return withRequestDeadline('recallBundle', this.recallTimeout, async (signal) => {
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(`Persistio recallBundle failed: ${res.status}`);
75
- const data = await res.json();
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.ingestTimeout, async (signal) => {
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 addMemory(data, subject) {
94
- await withRequestDeadline('addMemory', this.writeTimeout, async (signal) => {
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(`Persistio addMemory failed: ${res.status}`);
73
+ throw new Error(await formatHttpError('memory_store', res));
74
+ return await res.json();
103
75
  });
104
76
  }
105
- async deleteMemory(id) {
106
- await withRequestDeadline('deleteMemory', this.writeTimeout, async (signal) => {
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(`Persistio deleteMemory failed: ${res.status}`);
85
+ throw new Error(await formatHttpError('memory_forget', res));
114
86
  });
115
87
  }
116
- async getMemory(id, options = {}) {
117
- return withRequestDeadline('getMemory', this.recallTimeout, async (signal) => {
118
- const query = options.includePending ? '?include_pending=true' : '';
119
- const res = await fetch(`${this.baseURL}/v1/memories/${id}${query}`, {
120
- headers: this.headers(),
121
- signal,
122
- });
123
- if (res.status === 404)
124
- return null;
125
- if (!res.ok)
126
- throw new Error(`Persistio getMemory failed: ${res.status}`);
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
- async listMemories() {
131
- return withRequestDeadline('listMemories', this.recallTimeout, async (signal) => {
132
- const res = await fetch(`${this.baseURL}/v1/memories`, {
133
- headers: this.headers(),
134
- signal,
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
- if (!(err instanceof Error))
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
- // Ignore response body read failures; the status is still actionable.
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}`