@alfe.ai/openclaw-memory-cloud 0.0.38 → 0.0.39

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.
Files changed (58) hide show
  1. package/README.md +37 -0
  2. package/dist/index.cjs +2 -0
  3. package/dist/index.d.cts +2 -0
  4. package/dist/index.d.ts +2 -60
  5. package/dist/index.js +2 -396
  6. package/dist/plugin.cjs +2 -0
  7. package/dist/plugin.d.cts +59 -0
  8. package/dist/plugin.d.cts.map +1 -0
  9. package/dist/plugin.d.ts +59 -0
  10. package/dist/plugin.d.ts.map +1 -0
  11. package/dist/plugin.js +2 -0
  12. package/dist/plugin2.cjs +1107 -0
  13. package/dist/plugin2.d.cts +2 -0
  14. package/dist/plugin2.d.ts +2 -0
  15. package/dist/plugin2.js +1104 -0
  16. package/dist/plugin2.js.map +1 -0
  17. package/openclaw.plugin.json +6 -5
  18. package/package.json +27 -7
  19. package/.turbo/turbo-build.log +0 -4
  20. package/CHANGELOG.md +0 -320
  21. package/dist/auto-capture.d.ts +0 -45
  22. package/dist/auto-capture.d.ts.map +0 -1
  23. package/dist/auto-capture.js +0 -101
  24. package/dist/auto-capture.js.map +0 -1
  25. package/dist/auto-recall.d.ts +0 -22
  26. package/dist/auto-recall.d.ts.map +0 -1
  27. package/dist/auto-recall.js +0 -63
  28. package/dist/auto-recall.js.map +0 -1
  29. package/dist/formatter.d.ts +0 -7
  30. package/dist/formatter.d.ts.map +0 -1
  31. package/dist/formatter.js +0 -27
  32. package/dist/formatter.js.map +0 -1
  33. package/dist/index.d.ts.map +0 -1
  34. package/dist/index.js.map +0 -1
  35. package/dist/ingest-epoch.d.ts +0 -38
  36. package/dist/ingest-epoch.d.ts.map +0 -1
  37. package/dist/ingest-epoch.js +0 -66
  38. package/dist/ingest-epoch.js.map +0 -1
  39. package/dist/session-backfill.d.ts +0 -54
  40. package/dist/session-backfill.d.ts.map +0 -1
  41. package/dist/session-backfill.js +0 -192
  42. package/dist/session-backfill.js.map +0 -1
  43. package/dist/types.d.ts +0 -113
  44. package/dist/types.d.ts.map +0 -1
  45. package/dist/types.js +0 -2
  46. package/dist/types.js.map +0 -1
  47. package/src/__tests__/auto-capture.test.ts +0 -115
  48. package/src/__tests__/ingest-epoch.test.ts +0 -67
  49. package/src/__tests__/session-backfill.test.ts +0 -289
  50. package/src/auto-capture.ts +0 -108
  51. package/src/auto-recall.ts +0 -66
  52. package/src/formatter.ts +0 -30
  53. package/src/index.ts +0 -464
  54. package/src/ingest-epoch.ts +0 -78
  55. package/src/session-backfill.ts +0 -220
  56. package/src/types.ts +0 -93
  57. package/sst-env.d.ts +0 -10
  58. package/tsconfig.json +0 -20
@@ -1,289 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { mkdtemp, writeFile, rm, mkdir, chmod } from "node:fs/promises";
3
- import { tmpdir } from "node:os";
4
- import { join } from "node:path";
5
- import { runSessionsBackfill } from "../session-backfill.js";
6
- import type { MemoryCloudConfig, MemoryMessage, SessionMetadata } from "../types.js";
7
-
8
- const CONFIG: MemoryCloudConfig = {
9
- autoCapture: true,
10
- autoRecall: true,
11
- captureMaxChars: 500,
12
- idleFlushSeconds: 60,
13
- backfillSessions: true,
14
- backfillMaxSessions: 50,
15
- };
16
-
17
- const logger = { info: vi.fn(), debug: vi.fn(), warn: vi.fn() };
18
-
19
- function makeClient() {
20
- return {
21
- memoryIngest: vi.fn(async (sessionKey: string, messages: MemoryMessage[], metadata?: SessionMetadata) => {
22
- void sessionKey;
23
- void metadata;
24
- return { queued: true, messageCount: messages.length };
25
- }),
26
- memoryBootstrapStatusMark: vi.fn(async (scope?: "files" | "sessions") => {
27
- void scope;
28
- return { synced: true as const, syncedAt: new Date().toISOString() };
29
- }),
30
- };
31
- }
32
-
33
- function sessionFile(sessionId: string, messages: { role: string; content: string; timestamp: number }[], extra?: Record<string, unknown>) {
34
- return JSON.stringify({
35
- sessionId,
36
- agentId: "",
37
- channel: "alfe",
38
- createdAt: new Date(0).toISOString(),
39
- updatedAt: new Date().toISOString(),
40
- messages,
41
- ...extra,
42
- });
43
- }
44
-
45
- describe("runSessionsBackfill", () => {
46
- let dir: string;
47
-
48
- beforeEach(async () => {
49
- dir = await mkdtemp(join(tmpdir(), "memory-backfill-"));
50
- vi.clearAllMocks();
51
- });
52
-
53
- afterEach(async () => {
54
- await rm(dir, { recursive: true, force: true });
55
- });
56
-
57
- it("marks complete immediately when the sessions directory is missing", async () => {
58
- const client = makeClient();
59
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: join(dir, "nope") });
60
- expect(client.memoryIngest).not.toHaveBeenCalled();
61
- expect(client.memoryBootstrapStatusMark).toHaveBeenCalledWith("sessions");
62
- });
63
-
64
- it("marks complete when no eligible sessions exist", async () => {
65
- const client = makeClient();
66
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
67
- expect(client.memoryIngest).not.toHaveBeenCalled();
68
- expect(client.memoryBootstrapStatusMark).toHaveBeenCalledWith("sessions");
69
- });
70
-
71
- it("skips corrupt and shape-mismatched files but ingests valid ones", async () => {
72
- await writeFile(join(dir, "corrupt.json"), "{not json");
73
- await writeFile(join(dir, "no-messages.json"), JSON.stringify({ sessionId: "x" }));
74
- await writeFile(join(dir, "good.json"), sessionFile("alfe:dm:user1", [
75
- { role: "user", content: "hello", timestamp: 1000 },
76
- { role: "assistant", content: "hi there", timestamp: 2000 },
77
- ]));
78
-
79
- const client = makeClient();
80
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
81
-
82
- expect(client.memoryIngest).toHaveBeenCalledTimes(1);
83
- const [sessionKey, messages] = client.memoryIngest.mock.calls[0];
84
- expect(sessionKey).toBe("alfe:dm:user1");
85
- expect(messages).toHaveLength(2);
86
- expect(client.memoryBootstrapStatusMark).toHaveBeenCalledWith("sessions");
87
- });
88
-
89
- it("excludes messages at or after the cutoff (retrofit dedup)", async () => {
90
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", [
91
- { role: "user", content: "before install", timestamp: 1000 },
92
- { role: "user", content: "after install", timestamp: 5000 },
93
- ]));
94
-
95
- const client = makeClient();
96
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: 5000, sessionsDir: dir });
97
-
98
- const [, messages] = client.memoryIngest.mock.calls[0];
99
- expect(messages).toHaveLength(1);
100
- expect((messages as MemoryMessage[])[0].content).toBe("before install");
101
- });
102
-
103
- it("preserves file-array positions as indexes and skips invalid entries without renumbering", async () => {
104
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", [
105
- { role: "user", content: "first", timestamp: 1000 },
106
- { role: "system", content: "not a chat role", timestamp: 1500 },
107
- { role: "assistant", content: "second", timestamp: 2000 },
108
- ]));
109
-
110
- const client = makeClient();
111
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
112
-
113
- const [, messages] = client.memoryIngest.mock.calls[0] as [string, MemoryMessage[]];
114
- expect(messages.map((m) => m.index)).toEqual([0, 2]);
115
- expect(messages[0].timestamp).toBe(new Date(1000).toISOString());
116
- });
117
-
118
- it("truncates message content to captureMaxChars", async () => {
119
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", [
120
- { role: "user", content: "x".repeat(2000), timestamp: 1000 },
121
- ]));
122
-
123
- const client = makeClient();
124
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
125
-
126
- const [, messages] = client.memoryIngest.mock.calls[0] as [string, MemoryMessage[]];
127
- expect(messages[0].content).toHaveLength(500);
128
- });
129
-
130
- it("sends each session as exactly one ingest call (no cross-chunk race)", async () => {
131
- const messages = Array.from({ length: 101 }, (_, i) => ({
132
- role: "user",
133
- content: `msg ${String(i)}`,
134
- timestamp: 1000 + i,
135
- }));
136
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", messages));
137
-
138
- const client = makeClient();
139
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
140
-
141
- expect(client.memoryIngest).toHaveBeenCalledTimes(1);
142
- expect(client.memoryIngest.mock.calls[0][1]).toHaveLength(101);
143
- });
144
-
145
- it("keeps only the most recent 500 messages of a session, preserving original indexes", async () => {
146
- const messages = Array.from({ length: 600 }, (_, i) => ({
147
- role: "user",
148
- content: `msg ${String(i)}`,
149
- timestamp: 1000 + i,
150
- }));
151
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", messages));
152
-
153
- const client = makeClient();
154
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
155
-
156
- expect(client.memoryIngest).toHaveBeenCalledTimes(1);
157
- const sent = client.memoryIngest.mock.calls[0][1] as MemoryMessage[];
158
- expect(sent).toHaveLength(500);
159
- expect(sent[0].index).toBe(100);
160
- expect(sent[sent.length - 1].index).toBe(599);
161
- });
162
-
163
- it("shrinks the window to the most recent messages when the byte budget would overflow", async () => {
164
- // ~40KB of content per message → only 4 fit in the 180KB budget.
165
- const messages = Array.from({ length: 10 }, (_, i) => ({
166
- role: "user",
167
- content: "x".repeat(40_000),
168
- timestamp: 1000 + i,
169
- }));
170
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", messages));
171
-
172
- const client = makeClient();
173
- await runSessionsBackfill(client, { ...CONFIG, captureMaxChars: 50_000 }, logger, { cutoffMs: Date.now(), sessionsDir: dir });
174
-
175
- expect(client.memoryIngest).toHaveBeenCalledTimes(1);
176
- const sent = client.memoryIngest.mock.calls[0][1] as MemoryMessage[];
177
- expect(sent).toHaveLength(4);
178
- expect(sent[sent.length - 1].index).toBe(9);
179
- });
180
-
181
- it("skips sessions whose sessionId exceeds the server's 256-char cap", async () => {
182
- await writeFile(join(dir, "long.json"), sessionFile("x".repeat(300), [
183
- { role: "user", content: "hello", timestamp: 1000 },
184
- ]));
185
-
186
- const client = makeClient();
187
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
188
-
189
- expect(client.memoryIngest).not.toHaveBeenCalled();
190
- expect(client.memoryBootstrapStatusMark).toHaveBeenCalledWith("sessions");
191
- });
192
-
193
- it("does not let post-cutoff sessions starve pre-cutoff sessions out of the cap (retrofit)", async () => {
194
- // Newest session is entirely post-install — emptied by the cutoff. It
195
- // must not occupy the single cap slot.
196
- await writeFile(join(dir, "post-install.json"), sessionFile("alfe:dm:recent", [
197
- { role: "user", content: "captured live already", timestamp: 9000 },
198
- ]));
199
- await writeFile(join(dir, "pre-install.json"), sessionFile("alfe:dm:old", [
200
- { role: "user", content: "history worth keeping", timestamp: 1000 },
201
- ]));
202
-
203
- const client = makeClient();
204
- await runSessionsBackfill(client, { ...CONFIG, backfillMaxSessions: 1 }, logger, { cutoffMs: 5000, sessionsDir: dir });
205
-
206
- expect(client.memoryIngest).toHaveBeenCalledTimes(1);
207
- expect(client.memoryIngest.mock.calls[0][0]).toBe("alfe:dm:old");
208
- });
209
-
210
- it("caps at backfillMaxSessions, keeping the most recent sessions", async () => {
211
- await writeFile(join(dir, "old.json"), sessionFile("alfe:dm:old", [
212
- { role: "user", content: "old", timestamp: 1000 },
213
- ]));
214
- await writeFile(join(dir, "new.json"), sessionFile("alfe:dm:new", [
215
- { role: "user", content: "new", timestamp: 9000 },
216
- ]));
217
-
218
- const client = makeClient();
219
- await runSessionsBackfill(client, { ...CONFIG, backfillMaxSessions: 1 }, logger, { cutoffMs: Date.now(), sessionsDir: dir });
220
-
221
- expect(client.memoryIngest).toHaveBeenCalledTimes(1);
222
- expect(client.memoryIngest.mock.calls[0][0]).toBe("alfe:dm:new");
223
- });
224
-
225
- it("aborts without marking when an ingest call fails", async () => {
226
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", [
227
- { role: "user", content: "hello", timestamp: 1000 },
228
- ]));
229
-
230
- const client = makeClient();
231
- client.memoryIngest.mockRejectedValueOnce(new Error("boom"));
232
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
233
-
234
- expect(client.memoryBootstrapStatusMark).not.toHaveBeenCalled();
235
- expect(logger.warn).toHaveBeenCalled();
236
- });
237
-
238
- it("passes session channel and userId as ingest metadata", async () => {
239
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", [
240
- { role: "user", content: "hello", timestamp: 1000 },
241
- ], { channel: "alfe", userId: "user_123" }));
242
-
243
- const client = makeClient();
244
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
245
-
246
- const metadata = client.memoryIngest.mock.calls[0][2];
247
- expect(metadata).toEqual({ channelId: "alfe", userId: "user_123" });
248
- });
249
-
250
- it("does not warn or fail when the mark call itself fails (retries next startup)", async () => {
251
- await writeFile(join(dir, "s.json"), sessionFile("alfe:dm:u", [
252
- { role: "user", content: "hello", timestamp: 1000 },
253
- ]));
254
-
255
- const client = makeClient();
256
- client.memoryBootstrapStatusMark.mockRejectedValueOnce(new Error("mark failed"));
257
- await expect(
258
- runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir }),
259
- ).resolves.toBeUndefined();
260
- expect(logger.warn).toHaveBeenCalledWith(
261
- "sessions backfill: mark failed; will retry on next startup",
262
- expect.anything(),
263
- );
264
- });
265
-
266
- it("does not mark complete when the directory read fails for a non-ENOENT reason", async () => {
267
- const locked = join(dir, "locked");
268
- await mkdir(locked);
269
- await chmod(locked, 0o000);
270
- try {
271
- const client = makeClient();
272
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: locked });
273
- expect(client.memoryIngest).not.toHaveBeenCalled();
274
- expect(client.memoryBootstrapStatusMark).not.toHaveBeenCalled();
275
- expect(logger.warn).toHaveBeenCalled();
276
- } finally {
277
- await chmod(locked, 0o755);
278
- }
279
- });
280
-
281
- it("ignores non-json files and nested directories", async () => {
282
- await writeFile(join(dir, "notes.txt"), "not a session");
283
- await mkdir(join(dir, "sub.json.d"));
284
- const client = makeClient();
285
- await runSessionsBackfill(client, CONFIG, logger, { cutoffMs: Date.now(), sessionsDir: dir });
286
- expect(client.memoryIngest).not.toHaveBeenCalled();
287
- expect(client.memoryBootstrapStatusMark).toHaveBeenCalledWith("sessions");
288
- });
289
- });
@@ -1,108 +0,0 @@
1
- import type { MemoryApi, MemoryCloudConfig, MemoryMessage, SessionMetadata } from "./types.js";
2
-
3
- /**
4
- * Auto-capture manages the idle debounce timer and flushes
5
- * accumulated messages to the memory service.
6
- *
7
- * Flow:
8
- * 1. on_message_received: track message, reset timer
9
- * 2. Timer fires (60s idle): flush batch to /memory/ingest
10
- * 3. agent_end: final flush of remaining messages
11
- */
12
- export class AutoCapture {
13
- private static readonly MAX_QUEUE_SIZE = 500;
14
- private pendingMessages: MemoryMessage[] = [];
15
- private messageIndex = 0;
16
- private idleTimer: ReturnType<typeof setTimeout> | null = null;
17
- private sessionKey: string | null = null;
18
- private metadata: SessionMetadata = {};
19
-
20
- /**
21
- * Per-boot STRICTLY-monotonic epoch, resolved once at process start by
22
- * `resolveIngestEpoch` (index.ts) as `max(Date.now(), lastEpoch + 1)` from a
23
- * persisted state file — so it never regresses even if the wall clock steps
24
- * backward across a restart. Stamped on every flush so the memory service can
25
- * tell a restarted daemon (whose `messageIndex` counter has reset to 0) from
26
- * a continuing one and reset the server-side high-water mark accordingly —
27
- * otherwise post-restart captures are silently dropped by the surviving
28
- * watermark. Defaults to `Date.now()` only for tests that don't exercise the
29
- * restart path. See ingest-epoch.ts + services/memory/src/lib/ingest-offset.ts.
30
- */
31
- constructor(
32
- private readonly client: MemoryApi,
33
- private readonly config: MemoryCloudConfig,
34
- private readonly logger: { debug: (msg: string, ctx?: Record<string, unknown>) => void; warn: (msg: string, ctx?: Record<string, unknown>) => void },
35
- private readonly ingestEpoch: number = Date.now(),
36
- ) {}
37
-
38
- setSession(sessionKey: string, metadata?: SessionMetadata): void {
39
- this.sessionKey = sessionKey;
40
- if (metadata) this.metadata = metadata;
41
- }
42
-
43
- trackMessage(role: string, content: string): void {
44
- if (!this.config.autoCapture) return;
45
- if (content.length > this.config.captureMaxChars) {
46
- content = content.slice(0, this.config.captureMaxChars);
47
- }
48
-
49
- // Enforce max queue size — drop oldest if exceeded
50
- if (this.pendingMessages.length >= AutoCapture.MAX_QUEUE_SIZE) {
51
- this.logger.warn("Memory queue full — dropping oldest message", { queueSize: this.pendingMessages.length });
52
- this.pendingMessages.shift();
53
- }
54
-
55
- this.pendingMessages.push({
56
- role,
57
- content,
58
- index: this.messageIndex++,
59
- timestamp: new Date().toISOString(),
60
- });
61
-
62
- this.resetIdleTimer();
63
- }
64
-
65
- async flush(): Promise<void> {
66
- if (this.pendingMessages.length === 0 || !this.sessionKey) return;
67
-
68
- const batch = [...this.pendingMessages];
69
- this.pendingMessages = [];
70
-
71
- this.logger.debug("Flushing memory batch", {
72
- sessionKey: this.sessionKey,
73
- messageCount: batch.length,
74
- });
75
-
76
- try {
77
- await this.client.memoryIngest(this.sessionKey, batch, this.metadata, this.ingestEpoch);
78
- } catch (err) {
79
- this.logger.warn("Failed to flush memories", { error: String(err) });
80
- // Re-add failed messages for next flush attempt
81
- this.pendingMessages.unshift(...batch);
82
- }
83
- }
84
-
85
- async onAgentEnd(): Promise<void> {
86
- this.clearIdleTimer();
87
- await this.flush();
88
- }
89
-
90
- destroy(): void {
91
- this.clearIdleTimer();
92
- }
93
-
94
- private resetIdleTimer(): void {
95
- this.clearIdleTimer();
96
- this.idleTimer = setTimeout(() => {
97
- void this.flush();
98
- }, this.config.idleFlushSeconds * 1000);
99
- this.idleTimer.unref();
100
- }
101
-
102
- private clearIdleTimer(): void {
103
- if (this.idleTimer) {
104
- clearTimeout(this.idleTimer);
105
- this.idleTimer = null;
106
- }
107
- }
108
- }
@@ -1,66 +0,0 @@
1
- import type { MemoryApi, MemoryCloudConfig } from "./types.js";
2
-
3
- /**
4
- * Auto-recall loads tiered memory context at session start
5
- * and formats it for injection into the agent's prompt.
6
- *
7
- * Called from the before_agent_start hook.
8
- */
9
- export class AutoRecall {
10
- constructor(
11
- private readonly client: MemoryApi,
12
- private readonly config: MemoryCloudConfig,
13
- private readonly logger: { debug: (msg: string, ctx?: Record<string, unknown>) => void; warn: (msg: string, ctx?: Record<string, unknown>) => void },
14
- ) {}
15
-
16
- /**
17
- * Load memory context and return formatted XML for prompt injection.
18
- * Returns prependContext string for the before_agent_start hook result.
19
- */
20
- async loadForPrompt(userMessage: string): Promise<string | undefined> {
21
- if (!this.config.autoRecall) return undefined;
22
-
23
- try {
24
- // Detect topic from user message (first significant word)
25
- const topicHint = extractTopicHint(userMessage);
26
-
27
- // Load L1 (facts) + L2 (topic context) in one call
28
- const tier = topicHint ? 2 : 1;
29
- const context = await this.client.memoryLoadContext(tier, topicHint);
30
-
31
- if (!context.formatted || context.formatted.length === 0) {
32
- this.logger.debug("No relevant memories found for prompt");
33
- return undefined;
34
- }
35
-
36
- this.logger.debug("Loaded memory context for prompt", {
37
- tier,
38
- topicHint,
39
- factCount: Array.isArray(context.facts) ? context.facts.length : 0,
40
- memoryCount: Array.isArray(context.memories) ? context.memories.length : 0,
41
- tokenEstimate: typeof context.tokenEstimate === "number" ? context.tokenEstimate : 0,
42
- });
43
-
44
- return context.formatted;
45
- } catch (err) {
46
- this.logger.warn("Failed to load memory context", { error: String(err) });
47
- return undefined;
48
- }
49
- }
50
- }
51
-
52
- /**
53
- * Simple topic hint extraction from user message.
54
- * Returns the first capitalized word or proper noun as a potential topic.
55
- */
56
- function extractTopicHint(message: string): string | undefined {
57
- // Look for proper nouns (capitalized words not at sentence start)
58
- const words = message.split(/\s+/);
59
- for (let i = 1; i < words.length; i++) {
60
- const word = words[i];
61
- if (word && word.length > 2 && /^[A-Z]/.test(word) && !/^(The|And|But|For|With|This|That|What|How|Why|When|Where)$/.test(word)) {
62
- return word.toLowerCase();
63
- }
64
- }
65
- return undefined;
66
- }
package/src/formatter.ts DELETED
@@ -1,30 +0,0 @@
1
- import type { MemorySearchResult } from "./types.js";
2
-
3
- /**
4
- * Format memory search results into an XML block for agent context injection.
5
- * KG facts appear first (precise), then vector results (supporting context).
6
- */
7
- export function formatSearchResults(results: MemorySearchResult): string {
8
- const parts: string[] = [];
9
-
10
- if (results.facts.length > 0) {
11
- parts.push("Known facts:");
12
- for (const fact of results.facts) {
13
- const since = typeof fact.since === "string" ? fact.since.slice(0, 10) : "unknown";
14
- parts.push(`- ${fact.subject} ${fact.predicate} ${fact.object} (since ${since}, confidence: ${String(fact.confidence)})`);
15
- }
16
- }
17
-
18
- if (results.memories.length > 0) {
19
- if (parts.length > 0) parts.push("");
20
- parts.push("Related conversations:");
21
- for (const mem of results.memories) {
22
- const truncated = mem.text.length > 200 ? `${mem.text.slice(0, 200)}...` : mem.text;
23
- parts.push(`- [${mem.topic}/${mem.subtopic}] ${truncated}`);
24
- }
25
- }
26
-
27
- if (parts.length === 0) return "";
28
-
29
- return `<relevant-memories>\n${parts.join("\n")}\n</relevant-memories>`;
30
- }