@evermind-ai/openclaw-plugin 1.1.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.
@@ -0,0 +1,283 @@
1
+ /**
2
+ * EverMemOS ContextEngine Implementation for OpenClaw 3.8+
3
+ * Implements full-lifecycle memory management: bootstrap, assemble, afterTurn, compact, subagent
4
+ */
5
+
6
+ import { resolveConfig } from "./config.js";
7
+ import { toText, isSessionResetPrompt, collectMessages } from "./message-utils.js";
8
+ import { ContextAssembler } from "./assembler.js";
9
+ import { LifecycleManager } from "./lifecycle.js";
10
+ import { CompactionHandler } from "./compaction.js";
11
+ import { SubagentTracker } from "./subagent.js";
12
+
13
+ /**
14
+ * @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
15
+ * @typedef {import("./types.js").Logger} Logger
16
+ * @typedef {import("./types.js").BootstrapContext} BootstrapContext
17
+ * @typedef {import("./types.js").AssembleContext} AssembleContext
18
+ * @typedef {import("./types.js").AssembleResult} AssembleResult
19
+ * @typedef {import("./types.js").AfterTurnContext} AfterTurnContext
20
+ * @typedef {import("./types.js").CompactContext} CompactContext
21
+ * @typedef {import("./types.js").CompactResult} CompactResult
22
+ * @typedef {import("./types.js").PrepareSubagentContext} PrepareSubagentContext
23
+ * @typedef {import("./types.js").PrepareSubagentResult} PrepareSubagentResult
24
+ * @typedef {import("./types.js").SubagentEndedContext} SubagentEndedContext
25
+ */
26
+
27
+ /**
28
+ * EverMemOS ContextEngine implementation for OpenClaw 3.8+
29
+ * Implements full-lifecycle memory management: bootstrap, assemble, afterTurn, compact, subagent
30
+ */
31
+ export class EverMemOSContextEngine {
32
+ /**
33
+ * @param {EverMemOSConfig} config
34
+ * @param {Logger} logger
35
+ */
36
+ constructor(config, logger = console) {
37
+ this.cfg = resolveConfig(config);
38
+ this.log = logger;
39
+
40
+ // Session state tracking
41
+ this.turnCount = 0;
42
+ this.lastAssembleTime = 0;
43
+ this.pendingFlush = false;
44
+
45
+ // Submodules
46
+ this.assembler = new ContextAssembler(this.cfg, this.log);
47
+ this.lifecycle = new LifecycleManager(this.cfg, this.log);
48
+ this.compaction = new CompactionHandler(this.cfg, this.log);
49
+ this.subagentTracker = new SubagentTracker(this.cfg, this.log);
50
+ }
51
+
52
+ /**
53
+ * Bootstrap: Called once when plugin loads. Initialize connection and verify backend health.
54
+ * @param {BootstrapContext} ctx
55
+ * @returns {Promise<void>}
56
+ */
57
+ async bootstrap(ctx) {
58
+ this.log("[evermemos] bootstrap: initializing EverMemOS connection");
59
+ try {
60
+ const result = await this._healthCheck();
61
+ if (result?.status === "ok") {
62
+ this.log("[evermemos] bootstrap: backend healthy");
63
+ } else {
64
+ this.log.warn("[evermemos] bootstrap: backend unhealthy, will degrade gracefully");
65
+ }
66
+ } catch (err) {
67
+ this.log.warn(`[evermemos] bootstrap: health check failed: ${err.message}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Assemble: Build context from memories before agent processes the prompt.
73
+ * This replaces the old before_agent_start hook with query-aware assembly.
74
+ * @param {AssembleContext} ctx
75
+ * @returns {Promise<AssembleResult>}
76
+ */
77
+ async assemble(ctx) {
78
+ const { prompt, messages } = ctx;
79
+ const query = toText(prompt);
80
+
81
+ // Detect session reset (/new, /reset)
82
+ if (isSessionResetPrompt(query)) {
83
+ this.log("[evermemos] assemble: session reset detected, skipping search");
84
+ this.pendingFlush = true;
85
+ return { context: "", metadata: { skipped: "session_reset" } };
86
+ }
87
+
88
+ if (!query || query.length < 3) {
89
+ return { context: "", metadata: { skipped: "empty_query" } };
90
+ }
91
+
92
+ try {
93
+ this.lastAssembleTime = Date.now();
94
+ const result = await this.assembler.assemble(query, messages, this.turnCount);
95
+ this.log(`[evermemos] assemble: retrieved ${result.memoryCount} memories`);
96
+ return {
97
+ context: result.context,
98
+ metadata: {
99
+ memoryCount: result.memoryCount,
100
+ retrieveMethod: this.cfg.retrieveMethod,
101
+ turnCount: this.turnCount,
102
+ },
103
+ };
104
+ } catch (err) {
105
+ this.log.warn(`[evermemos] assemble: ${err.message}`);
106
+ return { context: "", metadata: { error: err.message } };
107
+ }
108
+ }
109
+
110
+ /**
111
+ * AfterTurn: Called after each agent turn. Extract memories promptly.
112
+ * This enables timely extraction instead of waiting for agent_end.
113
+ * @param {AfterTurnContext} ctx
114
+ * @returns {Promise<void>}
115
+ */
116
+ async afterTurn(ctx) {
117
+ const { messages, success } = ctx;
118
+ this.turnCount++;
119
+
120
+ if (!success || !messages?.length) {
121
+ this.log(`[evermemos] afterTurn: turn ${this.turnCount} skipped (no success/messages)`);
122
+ return;
123
+ }
124
+
125
+ try {
126
+ const collected = collectMessages(messages);
127
+ if (!collected.length) {
128
+ this.log(`[evermemos] afterTurn: turn ${this.turnCount} - no messages to save`);
129
+ return;
130
+ }
131
+
132
+ await this.lifecycle.ingestTurn({
133
+ userId: this.cfg.userId,
134
+ groupId: this.cfg.groupId,
135
+ messages: collected,
136
+ turnCount: this.turnCount,
137
+ flush: this.pendingFlush,
138
+ });
139
+
140
+ this.log(`[evermemos] afterTurn: turn ${this.turnCount} - saved ${collected.length} messages`);
141
+
142
+ // Consume flush flag after successful save
143
+ if (this.pendingFlush) {
144
+ this.pendingFlush = false;
145
+ this.log("[evermemos] afterTurn: flush flag consumed");
146
+ }
147
+ } catch (err) {
148
+ this.log.warn(`[evermemos] afterTurn: ${err.message}`);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Compact: Participate in session compaction decision and memory consolidation.
154
+ * @param {CompactContext} ctx
155
+ * @returns {Promise<CompactResult>}
156
+ */
157
+ async compact(ctx) {
158
+ const { messages, tokenCount } = ctx;
159
+
160
+ this.log(`[evermemos] compact: evaluating compaction (tokens: ${tokenCount}, turns: ${this.turnCount})`);
161
+
162
+ try {
163
+ const decision = await this.compaction.evaluate({
164
+ messages,
165
+ tokenCount,
166
+ turnCount: this.turnCount,
167
+ lastAssembleTime: this.lastAssembleTime,
168
+ });
169
+
170
+ if (decision.shouldCompact) {
171
+ this.log(`[evermemos] compact: compaction recommended - ${decision.reason}`);
172
+ return {
173
+ shouldCompact: true,
174
+ reason: decision.reason,
175
+ metadata: {
176
+ turnCount: this.turnCount,
177
+ memoryStrategy: decision.memoryStrategy,
178
+ },
179
+ };
180
+ }
181
+
182
+ this.log("[evermemos] compact: no compaction needed");
183
+ return { shouldCompact: false, reason: decision.reason };
184
+ } catch (err) {
185
+ this.log.warn(`[evermemos] compact: ${err.message}`);
186
+ return { shouldCompact: false, reason: "error evaluating compaction" };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * PrepareSubagentSpawn: Called before spawning a subagent.
192
+ * @param {PrepareSubagentContext} ctx
193
+ * @returns {Promise<PrepareSubagentResult>}
194
+ */
195
+ async prepareSubagentSpawn(ctx) {
196
+ const { subagentId, parentMessages } = ctx;
197
+
198
+ this.log(`[evermemos] prepareSubagentSpawn: tracking subagent ${subagentId}`);
199
+
200
+ // Register the subagent
201
+ this.subagentTracker.register(subagentId, {
202
+ subagentType: ctx.subagentType,
203
+ parentTurnCount: this.turnCount,
204
+ });
205
+
206
+ try {
207
+ // Collect relevant memories for the subagent
208
+ const query = toText(ctx.prompt);
209
+ const context = query
210
+ ? await this.assembler.assembleForSubagent(query)
211
+ : "";
212
+
213
+ return {
214
+ prependContext: context || "",
215
+ metadata: {
216
+ subagentId,
217
+ parentTurnCount: this.turnCount,
218
+ },
219
+ };
220
+ } catch (err) {
221
+ this.log.warn(`[evermemos] prepareSubagentSpawn: ${err.message}`);
222
+ return { prependContext: "", metadata: {} };
223
+ }
224
+ }
225
+
226
+ /**
227
+ * OnSubagentEnded: Called after a subagent completes.
228
+ * @param {SubagentEndedContext} ctx
229
+ * @returns {Promise<void>}
230
+ */
231
+ async onSubagentEnded(ctx) {
232
+ const { subagentId, messages, success } = ctx;
233
+
234
+ this.log(`[evermemos] onSubagentEnded: subagent ${subagentId} ended, success=${success}`);
235
+
236
+ // Unregister the subagent
237
+ this.subagentTracker.unregister(subagentId);
238
+
239
+ try {
240
+ if (success && messages?.length) {
241
+ const collected = collectMessages(messages);
242
+ if (collected.length) {
243
+ await this.lifecycle.ingestTurn({
244
+ userId: this.cfg.userId,
245
+ groupId: this.cfg.groupId,
246
+ messages: collected,
247
+ turnCount: this.turnCount,
248
+ flush: false,
249
+ });
250
+ this.log(`[evermemos] onSubagentEnded: saved ${collected.length} messages from subagent`);
251
+ }
252
+ }
253
+ } catch (err) {
254
+ this.log.warn(`[evermemos] onSubagentEnded: ${err.message}`);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Internal: Verify backend health
260
+ * @private
261
+ * @returns {Promise<{status: string}|null>}
262
+ */
263
+ async _healthCheck() {
264
+ const { serverUrl } = this.cfg;
265
+ try {
266
+ const controller = new AbortController();
267
+ const timeout = setTimeout(() => controller.abort(), 5000);
268
+
269
+ const response = await fetch(`${serverUrl}/api/v0/health`, {
270
+ method: "GET",
271
+ signal: controller.signal,
272
+ });
273
+ clearTimeout(timeout);
274
+
275
+ if (response.ok) {
276
+ return await response.json();
277
+ }
278
+ return { status: "error", httpStatus: response.status };
279
+ } catch {
280
+ return { status: "error" };
281
+ }
282
+ }
283
+ }
@@ -0,0 +1,152 @@
1
+ export const CONTEXT_BOUNDARY = "user\u200b原\u200b始\u200bquery\u200b:\u200b\u200b\u200b\u200b";
2
+
3
+ function timestampToLabel(ts) {
4
+ if (ts == null || ts === "") return "";
5
+
6
+ if (typeof ts === "number") {
7
+ const d = new Date(ts);
8
+ if (Number.isNaN(d.getTime())) return "";
9
+ const p = (n) => `${n}`.padStart(2, "0");
10
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
11
+ }
12
+
13
+ if (typeof ts === "string") {
14
+ const s = ts.trim();
15
+ if (!s) return "";
16
+ // Unix epoch as string
17
+ if (/^\d{10,13}$/.test(s)) return timestampToLabel(Number(s));
18
+ // ISO 8601: extract date and HH:MM
19
+ const dateEnd = s.indexOf("T");
20
+ if (dateEnd === 10 && s.length > 15) return `${s.slice(0, 10)} ${s.slice(11, 16)}`;
21
+ return s;
22
+ }
23
+
24
+ return "";
25
+ }
26
+
27
+ export function parseSearchResponse(raw) {
28
+ if (raw?.status !== "ok" || !raw?.result) return null;
29
+
30
+ const allMemories = raw.result.memories ?? [];
31
+
32
+ // episodic memories
33
+ const episodic = allMemories
34
+ .filter((m) => m.memory_type === "episodic_memory" && (m.score ?? 0) >= 0.1)
35
+ .map((m) => {
36
+ const body = m.summary || m.episode || m.content || "";
37
+ const subject = m.subject || "";
38
+ return {
39
+ text: subject ? `${subject}: ${body}` : body,
40
+ timestamp: m.timestamp ?? null,
41
+ };
42
+ });
43
+
44
+ const traits = (raw.result.profiles ?? []).filter((p) => (p.score ?? 0) >= 0.1).map((p) => {
45
+ const label = p.category || p.trait_name || "";
46
+ let kind = p.item_type || "";
47
+ if (kind === "explicit_info") kind = "explicit";
48
+ else if (kind === "implicit_trait") kind = "implicit";
49
+ return {
50
+ text: label ? `[${label}] ${p.description || ""}` : (p.description || ""),
51
+ kind,
52
+ };
53
+ });
54
+
55
+ // agent_case: top 1 by score (min 0.01)
56
+ const topCase = allMemories
57
+ .filter((m) => m.memory_type === "agent_case" && (m.score ?? 0) >= 0.01)
58
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))[0] || null;
59
+
60
+ // agent_skill: top 1 by score (min 0.01)
61
+ const topSkill = allMemories
62
+ .filter((m) => m.memory_type === "agent_skill" && (m.score ?? 0) >= 0.01)
63
+ .sort((a, b) => (b.score ?? 0) - (a.score ?? 0))[0] || null;
64
+
65
+ return { episodic, traits, case: topCase, skill: topSkill };
66
+ }
67
+
68
+ function oneLiner(text) {
69
+ return text == null ? "" : String(text).replace(/[\r\n]+/g, " ").trim();
70
+ }
71
+
72
+ function factLine(fact) {
73
+ const t = oneLiner(fact.text);
74
+ if (!t) return "";
75
+ const when = timestampToLabel(fact.timestamp);
76
+ return when ? ` - [${when}] ${t}` : ` - ${t}`;
77
+ }
78
+
79
+ function traitLine(trait) {
80
+ const t = oneLiner(trait.text);
81
+ if (!t) return "";
82
+ const k = trait.kind?.toLowerCase() ?? "";
83
+ const badge = k.includes("explicit") ? " [Explicit]"
84
+ : k.includes("implicit") ? " [Implicit]"
85
+ : trait.kind ? ` [${trait.kind.replace(/[_-]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())}]`
86
+ : "";
87
+ return ` -${badge} ${t}`;
88
+ }
89
+
90
+ function caseBlock(c) {
91
+ if (!c) return [];
92
+ const intent = oneLiner(c.task_intent || "");
93
+ const approach = c.approach || "";
94
+ if (!intent && !approach) return [];
95
+ const when = timestampToLabel(c.timestamp ?? c.created_at ?? null);
96
+ return [
97
+ " <case>",
98
+ ...(when ? [` - time: ${when}`] : []),
99
+ ...(intent ? [` - intent: ${intent}`] : []),
100
+ ...(approach ? [` - approach: ${approach}`] : []),
101
+ " </case>",
102
+ ];
103
+ }
104
+
105
+ function skillBlock(s) {
106
+ if (!s) return [];
107
+ const name = oneLiner(s.name || "");
108
+ const desc = oneLiner(s.description || "");
109
+ const content = s.content || "";
110
+ if (!name && !content) return [];
111
+ return [
112
+ " <skill>",
113
+ ...(name ? [` - name: ${name}`] : []),
114
+ ...(desc ? [` - description: ${desc}`] : []),
115
+ ...(content ? [` - content: ${content}`] : []),
116
+ " </skill>",
117
+ ];
118
+ }
119
+
120
+ export function buildMemoryPrompt(parsed, opts = {}) {
121
+ if (!parsed) return "";
122
+
123
+ const episodicLines = parsed.episodic.map(factLine).filter(Boolean);
124
+ const traitLines = parsed.traits.map(traitLine).filter(Boolean);
125
+ const caseLines = caseBlock(parsed.case);
126
+ const skillLines = skillBlock(parsed.skill);
127
+
128
+ if (!episodicLines.length && !traitLines.length && !caseLines.length && !skillLines.length) return "";
129
+
130
+ const xmlBlock = [
131
+ "<memory>",
132
+ ...(episodicLines.length ? [" <episodic>", ...episodicLines, " </episodic>"] : []),
133
+ ...(traitLines.length ? [" <trait>", ...traitLines, " </trait>"] : []),
134
+ ...(caseLines.length ? [" <!-- Similar past case. Use as reference if applicable to the current task. -->", ...caseLines] : []),
135
+ ...(skillLines.length ? [" <!-- Relevant skill. Use as reference if applicable to the current task. -->", ...skillLines] : []),
136
+ "</memory>",
137
+ ];
138
+
139
+ const memSection = opts.wrapInCodeBlock ? ["```text", ...xmlBlock, "```"] : xmlBlock;
140
+ const nowLabel = timestampToLabel(Date.now());
141
+
142
+ return [
143
+ "Note: Reference memory below. Build on past successes; avoid repeating failed approaches.",
144
+ ...(nowLabel ? [`- Time: ${nowLabel}`] : []),
145
+ "",
146
+ ...memSection,
147
+ "",
148
+ "**Note**: for memory, please not read from or write to local `MEMORY.md` or `memory/*` files as they are provided above.",
149
+ "",
150
+ CONTEXT_BOUNDARY,
151
+ ].join("\n");
152
+ }
@@ -0,0 +1,46 @@
1
+ import { setTimeout as sleep } from "node:timers/promises";
2
+ import { TIMEOUT_MS } from "./config.js";
3
+
4
+ function buildUrl(base, path, params) {
5
+ const qs = new URLSearchParams();
6
+ for (const [k, v] of Object.entries(params ?? {})) {
7
+ if (v == null || v === "") continue;
8
+ if (Array.isArray(v)) {
9
+ v.filter((x) => x != null && x !== "").forEach((x) => qs.append(k, String(x)));
10
+ } else {
11
+ qs.set(k, String(v));
12
+ }
13
+ }
14
+ const q = qs.toString();
15
+ return q ? `${base}${path}?${q}` : `${base}${path}`;
16
+ }
17
+
18
+ async function send(url, method, headers, body, timeoutMs) {
19
+ const ac = new AbortController();
20
+ const t = setTimeout(() => ac.abort(new Error(`timed out after ${timeoutMs}ms`)), timeoutMs);
21
+ try {
22
+ const res = await fetch(url, { method, headers, body, signal: ac.signal });
23
+ if (!res.ok) {
24
+ const msg = await res.text().catch(() => "");
25
+ throw new Error(`HTTP ${res.status}${msg ? ` – ${msg.slice(0, 200)}` : ""}`);
26
+ }
27
+ return res.json();
28
+ } finally {
29
+ clearTimeout(t);
30
+ }
31
+ }
32
+
33
+ export async function request(cfg, method, path, params) {
34
+ const headers = { "Content-Type": "application/json" };
35
+ // GET and DELETE use query string, POST uses body
36
+ const url = (method === "GET" || method === "DELETE") ? buildUrl(cfg.serverUrl, path, params) : `${cfg.serverUrl}${path}`;
37
+ const body = method === "POST" && params ? JSON.stringify(params) : undefined;
38
+ const ms = TIMEOUT_MS;
39
+
40
+ try {
41
+ return await send(url, method, headers, body, ms);
42
+ } catch {
43
+ await sleep(150);
44
+ return send(url, method, headers, body, ms);
45
+ }
46
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Lifecycle Manager Module
3
+ * Handles turn-level memory ingestion
4
+ */
5
+
6
+ import { saveMemories } from "./memory-api.js";
7
+
8
+ /**
9
+ * @typedef {import("./types.js").EverMemOSConfig} EverMemOSConfig
10
+ * @typedef {import("./types.js").Logger} Logger
11
+ */
12
+
13
+ /**
14
+ * Turn ingestion parameters
15
+ * @typedef {Object} IngestTurnParams
16
+ * @property {string} userId
17
+ * @property {string} groupId
18
+ * @property {Array} messages
19
+ * @property {number} turnCount
20
+ * @property {boolean} flush
21
+ */
22
+
23
+ /**
24
+ * Handles turn-level memory ingestion
25
+ * Extracts and saves memories after each conversation turn
26
+ */
27
+ export class LifecycleManager {
28
+ /**
29
+ * @param {EverMemOSConfig} cfg
30
+ * @param {Logger} logger
31
+ */
32
+ constructor(cfg, logger) {
33
+ this.cfg = cfg;
34
+ this.log = logger;
35
+ }
36
+
37
+ /**
38
+ * Ingest messages from a single turn
39
+ * @param {IngestTurnParams} params
40
+ * @returns {Promise<void>}
41
+ */
42
+ async ingestTurn({ userId, groupId, messages, turnCount, flush }) {
43
+ await saveMemories(this.cfg, {
44
+ userId,
45
+ groupId,
46
+ messages,
47
+ flush,
48
+ });
49
+ }
50
+
51
+ /**
52
+ * Flush pending memories to storage
53
+ * Called when session ends or reset is detected
54
+ * @returns {Promise<void>}
55
+ */
56
+ async flush() {
57
+ // Force save with flush=true
58
+ await saveMemories(this.cfg, {
59
+ userId: this.cfg.userId,
60
+ groupId: this.cfg.groupId,
61
+ messages: [],
62
+ flush: true,
63
+ });
64
+ }
65
+ }
@@ -0,0 +1,77 @@
1
+ import { request } from "./http-client.js";
2
+
3
+ export async function searchMemories(cfg, params) {
4
+ const { memory_types, ...baseParams } = params;
5
+
6
+ const episodicTypes = (memory_types ?? []).filter((t) => t === "episodic_memory" || t === "profile");
7
+ const caseTypes = (memory_types ?? []).filter((t) => t === "agent_case" || t === "agent_skill");
8
+
9
+ const searches = [];
10
+ if (episodicTypes.length) searches.push({ label: "episodic+profile", types: episodicTypes });
11
+ if (caseTypes.length) searches.push({ label: "case+skill", types: caseTypes });
12
+
13
+ const results = await Promise.all(
14
+ searches.map(async ({ label, types }) => {
15
+ const p = { ...baseParams, memory_types: types };
16
+ console.log("[memory-api] GET /api/v1/memories/search", label, JSON.stringify(p));
17
+ const r = await request(cfg, "GET", "/api/v1/memories/search", p);
18
+ console.log("[memory-api] GET response", label, JSON.stringify(r));
19
+ return r;
20
+ }),
21
+ );
22
+
23
+ // merge into a single response in order: episodic/profile first, then case/skill
24
+ const merged = {
25
+ status: "ok",
26
+ result: {
27
+ profiles: [],
28
+ memories: [],
29
+ },
30
+ };
31
+ for (const r of results) {
32
+ if (r?.result?.profiles?.length) merged.result.profiles.push(...r.result.profiles);
33
+ if (r?.result?.memories?.length) merged.result.memories.push(...r.result.memories);
34
+ }
35
+ return merged;
36
+ }
37
+
38
+ export async function saveMemories(cfg, { userId, groupId, messages = [], flush = false }) {
39
+ if (!messages.length) return;
40
+ const stamp = Date.now();
41
+ for (let i = 0; i < messages.length; i++) {
42
+ const { role = "user", content = "", tool_calls, tool_call_id } = messages[i];
43
+ const sender = role === "assistant" ? role : (role === "tool" ? "tool" : userId);
44
+ const isLast = i === messages.length - 1;
45
+
46
+ const payload = {
47
+ message_id: `em_${stamp}_${i}`,
48
+ create_time: new Date().toISOString(),
49
+ role,
50
+ sender,
51
+ sender_name: sender,
52
+ content,
53
+ group_id: groupId,
54
+ group_name: groupId,
55
+ scene: "assistant",
56
+ raw_data_type: "AgentConversation",
57
+ ...(tool_calls && { tool_calls }),
58
+ ...(tool_call_id && { tool_call_id }),
59
+ ...(flush && isLast && { flush: true }),
60
+ };
61
+ console.log("[memory-api] POST /api/v1/memories", JSON.stringify(payload));
62
+ const result = await request(cfg, "POST", "/api/v1/memories", payload);
63
+ console.log("[memory-api] POST response", JSON.stringify(result));
64
+ }
65
+ }
66
+
67
+ export async function deleteMemories(cfg, { memoryId, userId, groupId }) {
68
+ const payload = {};
69
+ if (memoryId) payload.memory_id = memoryId;
70
+ if (userId) payload.user_id = userId;
71
+ if (groupId) payload.group_id = groupId;
72
+
73
+ console.log("[memory-api] DELETE /api/v1/memories", JSON.stringify(payload));
74
+ const result = await request(cfg, "DELETE", "/api/v1/memories", payload);
75
+ console.log("[memory-api] DELETE response", JSON.stringify(result));
76
+ return result;
77
+ }