@pentatonic-ai/ai-agent-sdk 0.5.6 → 0.5.8

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.
@@ -0,0 +1,379 @@
1
+ /**
2
+ * Hosted-mode helpers for the Pentatonic memory system.
3
+ *
4
+ * These talk to a remote TES tenant over HTTPS using GraphQL, with a
5
+ * `tes_<clientId>_<rand>` bearer token in the Authorization header.
6
+ * They are deliberately thin wrappers around the GraphQL surface so
7
+ * any caller (the OpenClaw plugin, the LLM proxy worker, a custom
8
+ * integration) gets the same wire shape, the same error handling, and
9
+ * the same operational patterns.
10
+ *
11
+ * No `pg`, no Node-only APIs — Workers-compatible. Pure `fetch`.
12
+ *
13
+ * @example
14
+ * import { hostedSearch, hostedEmitChatTurn } from
15
+ * "@pentatonic-ai/ai-agent-sdk/memory/hosted";
16
+ *
17
+ * const config = {
18
+ * endpoint: "https://acme.api.pentatonic.com",
19
+ * clientId: "acme",
20
+ * apiKey: "tes_acme_xxxxx",
21
+ * };
22
+ *
23
+ * const { memories } = await hostedSearch(config, "What's my name?", {
24
+ * limit: 6, minScore: 0.55, timeoutMs: 800,
25
+ * });
26
+ *
27
+ * await hostedEmitChatTurn(config, {
28
+ * userMessage: "Hi",
29
+ * assistantResponse: "Hello!",
30
+ * model: "gpt-4o-mini",
31
+ * }, { source: "my-product" });
32
+ */
33
+
34
+ const SEMANTIC_SEARCH_QUERY = `
35
+ query SemanticSearchMemories($clientId: String!, $query: String!, $limit: Int, $minScore: Float) {
36
+ semanticSearchMemories(clientId: $clientId, query: $query, limit: $limit, minScore: $minScore) {
37
+ id
38
+ content
39
+ similarity
40
+ }
41
+ }
42
+ `;
43
+
44
+ const CREATE_MODULE_EVENT_MUTATION = `
45
+ mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
46
+ createModuleEvent(moduleId: $moduleId, input: $input) { success eventId }
47
+ }
48
+ `;
49
+
50
+ const DEFAULT_SEARCH_TIMEOUT_MS = 5000;
51
+ const DEFAULT_EMIT_TIMEOUT_MS = 10000;
52
+ const DEFAULT_SEARCH_LIMIT = 6;
53
+ const DEFAULT_SEARCH_MIN_SCORE = 0.55;
54
+
55
+ /**
56
+ * Normalise a config object — accepts both modern (`endpoint/clientId/apiKey`)
57
+ * and legacy openclaw-style (`tes_endpoint/tes_client_id/tes_api_key`) keys.
58
+ *
59
+ * @param {object} config
60
+ * @returns {{endpoint: string, clientId: string, apiKey: string}}
61
+ */
62
+ function normalizeConfig(config) {
63
+ if (!config) throw new Error("hosted: config is required");
64
+ const endpoint = config.endpoint || config.tes_endpoint;
65
+ const clientId = config.clientId || config.tes_client_id;
66
+ const apiKey = config.apiKey || config.tes_api_key;
67
+ if (!endpoint || !clientId || !apiKey) {
68
+ throw new Error(
69
+ "hosted: config requires { endpoint, clientId, apiKey } (or legacy tes_* equivalents)"
70
+ );
71
+ }
72
+ return { endpoint, clientId, apiKey };
73
+ }
74
+
75
+ /**
76
+ * Build the request headers TES expects for hosted-mode calls.
77
+ * Bearer auth if the apiKey starts with `tes_`; otherwise treated as a
78
+ * service key (for internal callers).
79
+ */
80
+ export function buildHostedHeaders(config) {
81
+ const { clientId, apiKey } = normalizeConfig(config);
82
+ const headers = {
83
+ "Content-Type": "application/json",
84
+ "x-client-id": clientId,
85
+ };
86
+ if (apiKey.startsWith("tes_")) {
87
+ headers["Authorization"] = `Bearer ${apiKey}`;
88
+ } else {
89
+ headers["x-service-key"] = apiKey;
90
+ }
91
+ return headers;
92
+ }
93
+
94
+ /**
95
+ * Run a semantic memory search against a remote TES tenant.
96
+ *
97
+ * @param {object} config — { endpoint, clientId, apiKey }
98
+ * @param {string} query — natural-language query
99
+ * @param {object} [opts]
100
+ * @param {number} [opts.limit=6]
101
+ * @param {number} [opts.minScore=0.55]
102
+ * @param {number} [opts.timeoutMs=5000]
103
+ * @returns {Promise<{
104
+ * memories: Array<{id: string, content: string, similarity: number}>,
105
+ * skipped?: string,
106
+ * }>}
107
+ *
108
+ * Failure mode: any error returns `{ memories: [], skipped: <reason> }`.
109
+ * Callers (e.g. the LLM proxy) inspect `skipped` to set `X-TES-Skipped`
110
+ * on their response, then forward unmodified. We never throw — the
111
+ * fail-soft contract means a hosted-search call never breaks the
112
+ * caller's primary user-facing flow.
113
+ */
114
+ export async function hostedSearch(config, query, opts = {}) {
115
+ if (!query) return { memories: [], skipped: "no_query" };
116
+
117
+ let cfg;
118
+ try {
119
+ cfg = normalizeConfig(config);
120
+ } catch (err) {
121
+ return { memories: [], skipped: `config_error:${err.message}` };
122
+ }
123
+
124
+ const limit = opts.limit ?? DEFAULT_SEARCH_LIMIT;
125
+ const minScore = opts.minScore ?? DEFAULT_SEARCH_MIN_SCORE;
126
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
127
+
128
+ const controller = new AbortController();
129
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
130
+
131
+ let response;
132
+ try {
133
+ response = await fetch(`${cfg.endpoint}/api/graphql`, {
134
+ method: "POST",
135
+ headers: buildHostedHeaders(cfg),
136
+ body: JSON.stringify({
137
+ query: SEMANTIC_SEARCH_QUERY,
138
+ variables: { clientId: cfg.clientId, query, limit, minScore },
139
+ }),
140
+ signal: controller.signal,
141
+ });
142
+ } catch (err) {
143
+ clearTimeout(timer);
144
+ return {
145
+ memories: [],
146
+ skipped: err.name === "AbortError" ? "tes_timeout" : "tes_unreachable",
147
+ };
148
+ }
149
+ clearTimeout(timer);
150
+
151
+ if (!response.ok) {
152
+ return { memories: [], skipped: `tes_http_${response.status}` };
153
+ }
154
+
155
+ let payload;
156
+ try {
157
+ payload = await response.json();
158
+ } catch {
159
+ return { memories: [], skipped: "tes_invalid_json" };
160
+ }
161
+
162
+ if (payload.errors?.length) {
163
+ const reason = payload.errors[0].message || "tes_graphql_error";
164
+ return { memories: [], skipped: `tes_graphql:${shortenReason(reason)}` };
165
+ }
166
+
167
+ return { memories: payload.data?.semanticSearchMemories || [] };
168
+ }
169
+
170
+ /**
171
+ * Emit a CHAT_TURN event to the conversation-analytics module of a
172
+ * remote TES tenant. The deep-memory consumer also subscribes to
173
+ * CHAT_TURN, so a single emit lands in both pipelines via consumer
174
+ * fan-out at the queue layer.
175
+ *
176
+ * @param {object} config — { endpoint, clientId, apiKey }
177
+ * @param {object} payload
178
+ * @param {string} [payload.userMessage]
179
+ * @param {string} [payload.assistantResponse]
180
+ * @param {string} [payload.model]
181
+ * @param {object} [payload.usage]
182
+ * @param {Array} [payload.toolCalls]
183
+ * @param {number} [payload.turnNumber]
184
+ * @param {string} [payload.systemPrompt]
185
+ * @param {string} [payload.sessionId]
186
+ * @param {string} [payload.userId]
187
+ * @param {object} [payload.extra] — additional attributes merged onto the event
188
+ * @param {object} [opts]
189
+ * @param {string} [opts.source="tes-sdk"] — attribution string written into attributes.source
190
+ * @param {number} [opts.timeoutMs=10000]
191
+ * @returns {Promise<{ ok: boolean, eventId?: string, skipped?: string }>}
192
+ */
193
+ export async function hostedEmitChatTurn(config, payload, opts = {}) {
194
+ if (!payload) return { ok: false, skipped: "no_payload" };
195
+ if (!payload.userMessage && !payload.assistantResponse) {
196
+ return { ok: false, skipped: "empty_turn" };
197
+ }
198
+
199
+ let cfg;
200
+ try {
201
+ cfg = normalizeConfig(config);
202
+ } catch (err) {
203
+ return { ok: false, skipped: `config_error:${err.message}` };
204
+ }
205
+
206
+ const source = opts.source || "tes-sdk";
207
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_EMIT_TIMEOUT_MS;
208
+
209
+ const attributes = { source };
210
+ if (payload.userMessage !== undefined)
211
+ attributes.user_message = payload.userMessage;
212
+ if (payload.assistantResponse !== undefined)
213
+ attributes.assistant_response = payload.assistantResponse;
214
+ if (payload.model) attributes.model = payload.model;
215
+ if (payload.usage) attributes.usage = payload.usage;
216
+ if (payload.toolCalls?.length) attributes.tool_calls = payload.toolCalls;
217
+ if (payload.turnNumber !== undefined)
218
+ attributes.turn_number = payload.turnNumber;
219
+ if (payload.systemPrompt) attributes.system_prompt = payload.systemPrompt;
220
+ if (payload.userId) attributes.user_id = payload.userId;
221
+ if (payload.extra && typeof payload.extra === "object") {
222
+ Object.assign(attributes, payload.extra);
223
+ }
224
+
225
+ const data = { attributes };
226
+ if (payload.sessionId) data.entity_id = payload.sessionId;
227
+
228
+ const input = { eventType: "CHAT_TURN", data };
229
+
230
+ const controller = new AbortController();
231
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
232
+
233
+ let response;
234
+ try {
235
+ response = await fetch(`${cfg.endpoint}/api/graphql`, {
236
+ method: "POST",
237
+ headers: buildHostedHeaders(cfg),
238
+ body: JSON.stringify({
239
+ query: CREATE_MODULE_EVENT_MUTATION,
240
+ variables: { moduleId: "conversation-analytics", input },
241
+ }),
242
+ signal: controller.signal,
243
+ });
244
+ } catch (err) {
245
+ clearTimeout(timer);
246
+ return {
247
+ ok: false,
248
+ skipped: err.name === "AbortError" ? "tes_timeout" : "tes_unreachable",
249
+ };
250
+ }
251
+ clearTimeout(timer);
252
+
253
+ if (!response.ok) {
254
+ return { ok: false, skipped: `tes_http_${response.status}` };
255
+ }
256
+
257
+ let body;
258
+ try {
259
+ body = await response.json();
260
+ } catch {
261
+ return { ok: false, skipped: "tes_invalid_json" };
262
+ }
263
+
264
+ if (body.errors?.length) {
265
+ return {
266
+ ok: false,
267
+ skipped: `tes_graphql:${shortenReason(body.errors[0].message)}`,
268
+ };
269
+ }
270
+
271
+ return {
272
+ ok: !!body.data?.createModuleEvent?.success,
273
+ eventId: body.data?.createModuleEvent?.eventId,
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Emit a STORE_MEMORY event against the deep-memory module. Used by the
279
+ * OpenClaw plugin for explicit memory-write tools.
280
+ *
281
+ * @param {object} config
282
+ * @param {string} content
283
+ * @param {object} [metadata]
284
+ * @param {object} [opts]
285
+ * @param {string} [opts.source="tes-sdk"]
286
+ * @param {number} [opts.timeoutMs=10000]
287
+ * @returns {Promise<{ ok: boolean, eventId?: string, skipped?: string }>}
288
+ */
289
+ export async function hostedStoreMemory(
290
+ config,
291
+ content,
292
+ metadata = {},
293
+ opts = {}
294
+ ) {
295
+ if (!content) return { ok: false, skipped: "no_content" };
296
+
297
+ let cfg;
298
+ try {
299
+ cfg = normalizeConfig(config);
300
+ } catch (err) {
301
+ return { ok: false, skipped: `config_error:${err.message}` };
302
+ }
303
+
304
+ const source = opts.source || "tes-sdk";
305
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_EMIT_TIMEOUT_MS;
306
+
307
+ const data = {
308
+ entity_id: metadata.session_id || metadata.sessionId || source,
309
+ attributes: {
310
+ ...metadata,
311
+ content,
312
+ source,
313
+ },
314
+ };
315
+
316
+ const controller = new AbortController();
317
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
318
+
319
+ let response;
320
+ try {
321
+ response = await fetch(`${cfg.endpoint}/api/graphql`, {
322
+ method: "POST",
323
+ headers: buildHostedHeaders(cfg),
324
+ body: JSON.stringify({
325
+ query: CREATE_MODULE_EVENT_MUTATION,
326
+ variables: {
327
+ moduleId: "deep-memory",
328
+ input: { eventType: "STORE_MEMORY", data },
329
+ },
330
+ }),
331
+ signal: controller.signal,
332
+ });
333
+ } catch (err) {
334
+ clearTimeout(timer);
335
+ return {
336
+ ok: false,
337
+ skipped: err.name === "AbortError" ? "tes_timeout" : "tes_unreachable",
338
+ };
339
+ }
340
+ clearTimeout(timer);
341
+
342
+ if (!response.ok) {
343
+ return { ok: false, skipped: `tes_http_${response.status}` };
344
+ }
345
+
346
+ let body;
347
+ try {
348
+ body = await response.json();
349
+ } catch {
350
+ return { ok: false, skipped: "tes_invalid_json" };
351
+ }
352
+
353
+ if (body.errors?.length) {
354
+ return {
355
+ ok: false,
356
+ skipped: `tes_graphql:${shortenReason(body.errors[0].message)}`,
357
+ };
358
+ }
359
+
360
+ return {
361
+ ok: !!body.data?.createModuleEvent?.success,
362
+ eventId: body.data?.createModuleEvent?.eventId,
363
+ };
364
+ }
365
+
366
+ function shortenReason(msg) {
367
+ if (typeof msg !== "string") return "unknown";
368
+ return msg
369
+ .toLowerCase()
370
+ .replace(/[^a-z0-9]+/g, "_")
371
+ .slice(0, 60);
372
+ }
373
+
374
+ // Re-export the system-message injector so callers that import the
375
+ // hosted module get the full memory-augmentation surface in one place.
376
+ // Keeping the implementation in `./inject.js` lets non-hosted consumers
377
+ // (e.g. a future "augment a request body" helper that doesn't talk to
378
+ // TES) reuse it without pulling in the GraphQL surface.
379
+ export { injectMemories } from "./inject.js";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Memory injection — formats retrieved memories as a system-message preamble
3
+ * and merges them into the upstream request body.
4
+ *
5
+ * Why a preamble (not a separate user-turn or tool-result):
6
+ * - Customer's existing system prompt is preserved verbatim, just appended.
7
+ * - Anthropic and OpenAI both treat system content as cache-friendly.
8
+ * - No conversation-history mutation — replays remain reproducible.
9
+ *
10
+ * Format:
11
+ * <tes:context>
12
+ * [1] (similarity 0.82) memory text...
13
+ * [2] (similarity 0.71) memory text...
14
+ * </tes:context>
15
+ *
16
+ * The XML-ish wrapper makes it trivial for the model to ignore on demand
17
+ * and trivial for an evaluator to strip when measuring quality deltas.
18
+ */
19
+
20
+ const MAX_CHARS_PER_MEMORY = 1200;
21
+
22
+ /**
23
+ * @param {object} body — upstream request body, mutated copy returned
24
+ * @param {Array<{id, content, similarity}>} memories
25
+ * @param {"anthropic"|"openai"} provider
26
+ * @returns {object} new body
27
+ */
28
+ export function injectMemories(body, memories, provider) {
29
+ if (!memories || memories.length === 0) return body;
30
+
31
+ const preamble = formatPreamble(memories);
32
+
33
+ if (provider === "anthropic") {
34
+ return injectAnthropic(body, preamble);
35
+ }
36
+ return injectOpenAI(body, preamble);
37
+ }
38
+
39
+ function formatPreamble(memories) {
40
+ const lines = ["<tes:context>"];
41
+ memories.forEach((m, i) => {
42
+ const sim =
43
+ typeof m.similarity === "number" ? m.similarity.toFixed(2) : "?";
44
+ const content = (m.content || "").slice(0, MAX_CHARS_PER_MEMORY);
45
+ lines.push(`[${i + 1}] (similarity ${sim}) ${content}`);
46
+ });
47
+ lines.push("</tes:context>");
48
+ return lines.join("\n");
49
+ }
50
+
51
+ function injectAnthropic(body, preamble) {
52
+ // Anthropic accepts `system` as either a string OR an array of content
53
+ // blocks. Preserve whichever shape the customer sent.
54
+ const next = { ...body };
55
+ if (typeof body.system === "string") {
56
+ next.system = `${preamble}\n\n${body.system}`;
57
+ } else if (Array.isArray(body.system)) {
58
+ next.system = [{ type: "text", text: preamble }, ...body.system];
59
+ } else {
60
+ next.system = preamble;
61
+ }
62
+ return next;
63
+ }
64
+
65
+ function injectOpenAI(body, preamble) {
66
+ // OpenAI carries the system prompt as the first message with role:'system'.
67
+ // If one exists we prepend; otherwise we insert a fresh one at index 0.
68
+ const messages = Array.isArray(body.messages) ? [...body.messages] : [];
69
+ if (messages.length > 0 && messages[0].role === "system") {
70
+ const existing = messages[0];
71
+ const existingContent =
72
+ typeof existing.content === "string"
73
+ ? existing.content
74
+ : JSON.stringify(existing.content);
75
+ messages[0] = {
76
+ ...existing,
77
+ content: `${preamble}\n\n${existingContent}`,
78
+ };
79
+ } else {
80
+ messages.unshift({ role: "system", content: preamble });
81
+ }
82
+ return { ...body, messages };
83
+ }
@@ -38,6 +38,40 @@
38
38
  import pg from "pg";
39
39
  import { createMemorySystem } from "../index.js";
40
40
  import { createContextEngine } from "./context-engine.js";
41
+ import { sanitizeMemoryContent } from "../sanitize.js";
42
+ import {
43
+ hostedSearch as _hostedSearch,
44
+ hostedEmitChatTurn as _hostedEmitChatTurn,
45
+ hostedStoreMemory as _hostedStoreMemory,
46
+ } from "../hosted.js";
47
+
48
+ // --- Hosted-mode adapters ---
49
+ //
50
+ // The OpenClaw plugin predates the public hosted-helper API (`packages/
51
+ // memory/src/hosted.js`). The wrappers below adapt the plugin's existing
52
+ // call shape to the public API so other consumers (the LLM proxy worker,
53
+ // custom integrations) hit the same code path. Adapters are tiny — they
54
+ // translate args and unwrap the result envelope. New code should import
55
+ // from `@pentatonic-ai/ai-agent-sdk/memory/hosted` directly.
56
+
57
+ async function hostedSearch(config, query, limit = 5, minScore = 0.3) {
58
+ const { memories } = await _hostedSearch(config, query, { limit, minScore });
59
+ return memories;
60
+ }
61
+
62
+ async function hostedEmitChatTurn(config, sessionId, turn) {
63
+ return _hostedEmitChatTurn(
64
+ config,
65
+ { ...turn, sessionId },
66
+ { source: "openclaw-plugin" }
67
+ );
68
+ }
69
+
70
+ async function hostedStore(config, content, metadata = {}) {
71
+ return _hostedStoreMemory(config, content, metadata, {
72
+ source: metadata.source || "openclaw-plugin",
73
+ });
74
+ }
41
75
 
42
76
  const { Pool } = pg;
43
77
 
@@ -74,139 +108,6 @@ function getLocalMemory(config) {
74
108
  return memory;
75
109
  }
76
110
 
77
- // --- Hosted mode helpers ---
78
-
79
- function tesHeaders(config) {
80
- const headers = {
81
- "Content-Type": "application/json",
82
- "x-client-id": config.tes_client_id,
83
- };
84
- if (config.tes_api_key?.startsWith("tes_")) {
85
- headers["Authorization"] = `Bearer ${config.tes_api_key}`;
86
- } else {
87
- headers["x-service-key"] = config.tes_api_key;
88
- }
89
- return headers;
90
- }
91
-
92
- async function hostedSearch(config, query, limit = 5, minScore = 0.3) {
93
- try {
94
- const response = await fetch(`${config.tes_endpoint}/api/graphql`, {
95
- method: "POST",
96
- headers: tesHeaders(config),
97
- body: JSON.stringify({
98
- query: `query($clientId: String!, $query: String!, $limit: Int, $minScore: Float) {
99
- semanticSearchMemories(clientId: $clientId, query: $query, limit: $limit, minScore: $minScore) {
100
- id content similarity
101
- }
102
- }`,
103
- variables: {
104
- clientId: config.tes_client_id,
105
- query,
106
- limit,
107
- minScore,
108
- },
109
- }),
110
- signal: AbortSignal.timeout(5000),
111
- });
112
- if (!response.ok) return [];
113
- const json = await response.json();
114
- return json.data?.semanticSearchMemories || [];
115
- } catch {
116
- return [];
117
- }
118
- }
119
-
120
- /**
121
- * Emit a CHAT_TURN event to TES so the conversation-analytics dashboard
122
- * (Token Universe + Tools tabs) can render. Without this, the dashboard
123
- * filters on eventType=CHAT_TURN and shows nothing for OpenClaw users
124
- * because the only events emitted are STORE_MEMORY.
125
- *
126
- * Anything missing from the message metadata is omitted rather than
127
- * defaulted to zero — that way the dashboard can distinguish "no data"
128
- * from "zero usage".
129
- */
130
- async function hostedEmitChatTurn(config, sessionId, turn) {
131
- const attributes = {
132
- source: "openclaw-plugin",
133
- user_message: turn.userMessage,
134
- assistant_response: turn.assistantResponse,
135
- };
136
- if (turn.model) attributes.model = turn.model;
137
- if (turn.usage) attributes.usage = turn.usage;
138
- if (turn.toolCalls?.length) attributes.tool_calls = turn.toolCalls;
139
- if (turn.turnNumber !== undefined) attributes.turn_number = turn.turnNumber;
140
- if (turn.systemPrompt) attributes.system_prompt = turn.systemPrompt;
141
-
142
- try {
143
- const response = await fetch(`${config.tes_endpoint}/api/graphql`, {
144
- method: "POST",
145
- headers: tesHeaders(config),
146
- // Route through createModuleEvent on the conversation-analytics
147
- // module rather than the top-level emitEvent. The latter requires
148
- // a permission most client API keys don't have ("Access denied:
149
- // You don't have permission to update emitEvent"), but the
150
- // module's manifest declares CHAT_TURN as a registered event
151
- // type, so the module-scoped path is both authorised and
152
- // consistent with how STORE_MEMORY is emitted.
153
- body: JSON.stringify({
154
- query: `mutation Cme($moduleId: String!, $input: ModuleEventInput!) {
155
- createModuleEvent(moduleId: $moduleId, input: $input) { success eventId }
156
- }`,
157
- variables: {
158
- moduleId: "conversation-analytics",
159
- input: {
160
- eventType: "CHAT_TURN",
161
- data: {
162
- entity_id: sessionId,
163
- attributes,
164
- },
165
- },
166
- },
167
- }),
168
- signal: AbortSignal.timeout(10000),
169
- });
170
- if (!response.ok) return null;
171
- return response.json();
172
- } catch {
173
- return null;
174
- }
175
- }
176
-
177
- async function hostedStore(config, content, metadata = {}) {
178
- try {
179
- const response = await fetch(`${config.tes_endpoint}/api/graphql`, {
180
- method: "POST",
181
- headers: tesHeaders(config),
182
- body: JSON.stringify({
183
- query: `mutation CreateModuleEvent($moduleId: String!, $input: ModuleEventInput!) {
184
- createModuleEvent(moduleId: $moduleId, input: $input) { success eventId }
185
- }`,
186
- variables: {
187
- moduleId: "deep-memory",
188
- input: {
189
- eventType: "STORE_MEMORY",
190
- data: {
191
- entity_id: metadata.session_id || "openclaw",
192
- attributes: {
193
- ...metadata,
194
- content,
195
- source: "openclaw-plugin",
196
- },
197
- },
198
- },
199
- },
200
- }),
201
- signal: AbortSignal.timeout(10000),
202
- });
203
- if (!response.ok) return null;
204
- return response.json();
205
- } catch {
206
- return null;
207
- }
208
- }
209
-
210
111
  // --- Hosted context engine ---
211
112
 
212
113
  // Per-session turn buffer. Holds the user message until the matching
@@ -440,7 +341,7 @@ function createHostedContextEngine(config, opts = {}) {
440
341
  const memoryText = results
441
342
  .map(
442
343
  (m) =>
443
- `- [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`
344
+ `- [${Math.round((m.similarity || 0) * 100)}%] ${sanitizeMemoryContent(m.content)}`
444
345
  )
445
346
  .join("\n");
446
347
 
@@ -638,7 +539,7 @@ Tell the user to run step 1 first, then help them fill in the config with the cr
638
539
  return results
639
540
  .map(
640
541
  (m, i) =>
641
- `${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`
542
+ `${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${sanitizeMemoryContent(m.content)}`
642
543
  )
643
544
  .join("\n\n");
644
545
  },
@@ -705,7 +606,7 @@ Tell the user to run step 1 first, then help them fill in the config with the cr
705
606
  return results
706
607
  .map(
707
608
  (m, i) =>
708
- `${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${m.content}`
609
+ `${i + 1}. [${Math.round((m.similarity || 0) * 100)}%] ${sanitizeMemoryContent(m.content)}`
709
610
  )
710
611
  .join("\n\n");
711
612
  },