@alfe.ai/openclaw-memory-cloud 0.0.1

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/src/index.ts ADDED
@@ -0,0 +1,286 @@
1
+ /**
2
+ * memory-cloud — OpenClaw memory extension
3
+ *
4
+ * Replaces the builtin LanceDB memory extension with cloud-backed storage:
5
+ * - Turbopuffer for vector storage (via services/memory Lambda proxy)
6
+ * - DynamoDB for knowledge graph
7
+ * - Haiku for classification + entity extraction
8
+ *
9
+ * Registers:
10
+ * - Memory tools: memory_recall, memory_store, memory_forget, memory_navigate, memory_graph, memory_stats
11
+ * - Lifecycle hooks: before_agent_start (auto-recall), agent_end (auto-capture), message_received (idle debounce)
12
+ * - Memory runtime: replaces builtin SQLite backend with cloud backend
13
+ */
14
+
15
+ import { MemoryClient } from "./memory-client.js";
16
+ import { AutoCapture } from "./auto-capture.js";
17
+ import { AutoRecall } from "./auto-recall.js";
18
+ import { formatSearchResults } from "./formatter.js";
19
+ import type { MemoryCloudConfig } from "./types.js";
20
+
21
+ // OpenClaw plugin types — these match the OpenClaw plugin API contract
22
+ interface PluginApi {
23
+ pluginConfig?: Record<string, unknown>;
24
+ config: Record<string, unknown>;
25
+ logger: {
26
+ info: (msg: string, ctx?: Record<string, unknown>) => void;
27
+ debug: (msg: string, ctx?: Record<string, unknown>) => void;
28
+ warn: (msg: string, ctx?: Record<string, unknown>) => void;
29
+ error: (msg: string, ctx?: Record<string, unknown>) => void;
30
+ };
31
+ registerTool: (factory: (ctx: ToolContext) => Tool, opts?: { names?: string[] }) => void;
32
+ registerHook: (events: string | string[], handler: (...args: unknown[]) => unknown, opts?: { name?: string; description?: string }) => void;
33
+ on: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }) => void;
34
+ registerMemoryPromptSection: (builder: (params: { availableTools: Set<string> }) => string[]) => void;
35
+ registerMemoryRuntime: (runtime: unknown) => void;
36
+ }
37
+
38
+ interface ToolContext {
39
+ agentId?: string;
40
+ sessionKey?: string;
41
+ sessionId?: string;
42
+ messageChannel?: string;
43
+ }
44
+
45
+ interface Tool {
46
+ name: string;
47
+ label: string;
48
+ description: string;
49
+ parameters: Record<string, unknown>;
50
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
51
+ }
52
+
53
+
54
+
55
+ const DEFAULT_CONFIG: MemoryCloudConfig = {
56
+ autoCapture: true,
57
+ autoRecall: true,
58
+ captureMaxChars: 500,
59
+ idleFlushSeconds: 60,
60
+ };
61
+
62
+ function resolveConfig(pluginConfig?: Record<string, unknown>): MemoryCloudConfig {
63
+ return {
64
+ autoCapture: typeof pluginConfig?.autoCapture === "boolean" ? pluginConfig.autoCapture : DEFAULT_CONFIG.autoCapture,
65
+ autoRecall: typeof pluginConfig?.autoRecall === "boolean" ? pluginConfig.autoRecall : DEFAULT_CONFIG.autoRecall,
66
+ captureMaxChars: typeof pluginConfig?.captureMaxChars === "number" ? pluginConfig.captureMaxChars : DEFAULT_CONFIG.captureMaxChars,
67
+ idleFlushSeconds: typeof pluginConfig?.idleFlushSeconds === "number" ? pluginConfig.idleFlushSeconds : DEFAULT_CONFIG.idleFlushSeconds,
68
+ };
69
+ }
70
+
71
+ export default {
72
+ id: "memory-cloud",
73
+ name: "Cloud Memory",
74
+ description: "Persistent agent memory backed by Turbopuffer + DynamoDB knowledge graph",
75
+ version: "0.1.0",
76
+ kind: "memory" as const,
77
+
78
+ register(api: PluginApi): void {
79
+ const config = resolveConfig(api.pluginConfig);
80
+ const logger = api.logger;
81
+
82
+ // Memory service URL from integration DESIRED_STATE config
83
+ const cfgUrl = api.pluginConfig?.memoryServiceUrl;
84
+ const cfgKey = api.pluginConfig?.agentApiKey;
85
+ const cfgAgent = api.pluginConfig?.agentId;
86
+ const memoryServiceUrl = typeof cfgUrl === "string" ? cfgUrl : "";
87
+ const agentApiKey = typeof cfgKey === "string" ? cfgKey : "";
88
+ const agentId = typeof cfgAgent === "string" ? cfgAgent : "";
89
+
90
+ if (!memoryServiceUrl) {
91
+ throw new Error("memory-cloud: memoryServiceUrl not configured — cannot register memory extension");
92
+ }
93
+
94
+ const client = new MemoryClient(memoryServiceUrl, agentApiKey, agentId);
95
+ const autoCapture = new AutoCapture(client, config, logger);
96
+ const autoRecall = new AutoRecall(client, config, logger);
97
+
98
+ // ─── Memory prompt section ──────────────────────────────────
99
+ api.registerMemoryPromptSection(({ availableTools }) => {
100
+ const lines: string[] = ["## Memory"];
101
+ if (availableTools.has("memory_recall")) {
102
+ lines.push("Use memory_recall to search your conversation history and knowledge graph.");
103
+ lines.push("Results include structured facts (from knowledge graph) and relevant conversation excerpts.");
104
+ }
105
+ if (availableTools.has("memory_store")) {
106
+ lines.push("Use memory_store to explicitly save important information for future reference.");
107
+ }
108
+ if (availableTools.has("memory_graph")) {
109
+ lines.push("Use memory_graph to look up what you know about a specific entity.");
110
+ }
111
+ return lines;
112
+ });
113
+
114
+ // ─── Tools ──────────────────────────────────────────────────
115
+ api.registerTool(() => ({
116
+ name: "memory_recall",
117
+ label: "Memory Recall",
118
+ description: "Search conversation memory and knowledge graph. Returns structured facts and relevant conversation excerpts.",
119
+ parameters: {
120
+ type: "object",
121
+ properties: {
122
+ query: { type: "string", description: "What to search for" },
123
+ limit: { type: "number", description: "Maximum results (default 10)" },
124
+ topic: { type: "string", description: "Filter by topic" },
125
+ tag: { type: "string", description: "Filter by tag (fact/decision/preference/event/discovery)" },
126
+ },
127
+ required: ["query"],
128
+ },
129
+ execute: async (_toolCallId: string, params: Record<string, unknown>) => {
130
+ const results = await client.search(
131
+ params.query as string,
132
+ {
133
+ limit: params.limit as number | undefined,
134
+ topic: params.topic as string | undefined,
135
+ tag: params.tag as string | undefined,
136
+ },
137
+ );
138
+ return formatSearchResults(results);
139
+ },
140
+ }), { names: ["memory_recall", "memory_search"] });
141
+
142
+ api.registerTool(() => ({
143
+ name: "memory_store",
144
+ label: "Memory Store",
145
+ description: "Explicitly save a piece of information to long-term memory.",
146
+ parameters: {
147
+ type: "object",
148
+ properties: {
149
+ text: { type: "string", description: "The information to remember" },
150
+ topic: { type: "string", description: "Topic category (e.g., person name, project)" },
151
+ tag: { type: "string", description: "Memory type: fact, decision, preference, event, discovery" },
152
+ importance: { type: "number", description: "Importance 0-1 (default 0.7)" },
153
+ },
154
+ required: ["text"],
155
+ },
156
+ execute: async (_toolCallId: string, params: Record<string, unknown>) => {
157
+ const result = await client.store(
158
+ params.text as string,
159
+ {
160
+ topic: params.topic as string | undefined,
161
+ tag: params.tag as string | undefined,
162
+ importance: params.importance as number | undefined,
163
+ },
164
+ );
165
+ return `Stored memory: ${result.memoryId}`;
166
+ },
167
+ }), { names: ["memory_store"] });
168
+
169
+ api.registerTool(() => ({
170
+ name: "memory_forget",
171
+ label: "Memory Forget",
172
+ description: "Search for and delete memories matching a query.",
173
+ parameters: {
174
+ type: "object",
175
+ properties: {
176
+ query: { type: "string", description: "Search for memories to delete" },
177
+ },
178
+ required: ["query"],
179
+ },
180
+ execute: async (_toolCallId: string, params: Record<string, unknown>) => {
181
+ const results = await client.search(params.query as string, { limit: 5 });
182
+ if (results.memories.length === 0) return "No matching memories found.";
183
+
184
+ const deleted: string[] = [];
185
+ for (const mem of results.memories) {
186
+ try {
187
+ await client.deleteMemory(mem.id);
188
+ deleted.push(mem.id);
189
+ } catch {
190
+ logger.warn("Failed to delete memory", { memoryId: mem.id });
191
+ }
192
+ }
193
+ return `Deleted ${String(deleted.length)} of ${String(results.memories.length)} matching memories.`;
194
+ },
195
+ }), { names: ["memory_forget"] });
196
+
197
+ api.registerTool(() => ({
198
+ name: "memory_graph",
199
+ label: "Knowledge Graph",
200
+ description: "Look up what you know about a specific entity from the knowledge graph.",
201
+ parameters: {
202
+ type: "object",
203
+ properties: {
204
+ entity: { type: "string", description: "The entity to look up (person, concept, system)" },
205
+ },
206
+ required: ["entity"],
207
+ },
208
+ execute: async (_toolCallId: string, params: Record<string, unknown>) => {
209
+ const result = await client.lookupEntity(params.entity as string);
210
+ if (result.triples.length === 0) return `No knowledge found about "${params.entity as string}".`;
211
+ const facts = result.triples.map((t) => `- ${result.subject} ${t.predicate} ${t.object} (since ${t.validFrom.slice(0, 10)})`);
212
+ return `Known facts about ${result.subject}:\n${facts.join("\n")}`;
213
+ },
214
+ }), { names: ["memory_graph"] });
215
+
216
+ api.registerTool(() => ({
217
+ name: "memory_stats",
218
+ label: "Memory Stats",
219
+ description: "Get memory storage statistics.",
220
+ parameters: { type: "object", properties: {} },
221
+ execute: async () => {
222
+ const s = await client.stats();
223
+ return `Memories: ${String(s.vectorCount)}, Knowledge facts: ${String(s.tripleCount)}, Storage: ${String(Math.round(s.storageEstimateBytes / 1024))} KB`;
224
+ },
225
+ }), { names: ["memory_stats"] });
226
+
227
+ api.registerTool(() => ({
228
+ name: "memory_navigate",
229
+ label: "Memory Navigate",
230
+ description: "Browse your memory palace structure — list topics, subtopics, and memory counts.",
231
+ parameters: { type: "object", properties: {} },
232
+ execute: async () => {
233
+ const nav = await client.navigate() as { topics?: { name: string; tripleCount: number; subtopics: string[] }[] } | null;
234
+ const topics = nav?.topics ?? [];
235
+ if (topics.length === 0) return "No memories stored yet.";
236
+ const lines = topics.map((t) => `- ${t.name} (${String(t.tripleCount)} facts, subtopics: ${t.subtopics.join(", ") || "none"})`);
237
+ return `Memory topics:\n${lines.join("\n")}`;
238
+ },
239
+ }), { names: ["memory_navigate"] });
240
+
241
+ // ─── Lifecycle hooks ────────────────────────────────────────
242
+
243
+ // Auto-recall: inject relevant memories at session start
244
+ api.on("before_agent_start", async (event: unknown, ctx: unknown) => {
245
+ const startEvent = event as Record<string, unknown>;
246
+ const agentCtx = ctx as Record<string, unknown>;
247
+ const prompt = typeof startEvent.prompt === "string" ? startEvent.prompt : "";
248
+ const sessionKey = typeof agentCtx.sessionKey === "string" ? agentCtx.sessionKey : undefined;
249
+ const channelId = typeof agentCtx.channelId === "string" ? agentCtx.channelId : undefined;
250
+
251
+ if (sessionKey) {
252
+ autoCapture.setSession(sessionKey, { channelId });
253
+ }
254
+
255
+ const contextXml = await autoRecall.loadForPrompt(prompt);
256
+ if (contextXml) {
257
+ return { prependContext: contextXml };
258
+ }
259
+ return undefined;
260
+ }, { priority: 10 });
261
+
262
+ // Track incoming messages for idle debounce
263
+ api.on("message_received", (event: unknown) => {
264
+ const msgEvent = event as Record<string, unknown>;
265
+ const content = typeof msgEvent.content === "string" ? msgEvent.content : "";
266
+ autoCapture.trackMessage("user", content);
267
+ });
268
+
269
+ // Track outgoing messages
270
+ api.on("message_sending", (event: unknown) => {
271
+ const msgEvent = event as Record<string, unknown>;
272
+ const content = typeof msgEvent.content === "string" ? msgEvent.content : "";
273
+ autoCapture.trackMessage("assistant", content);
274
+ });
275
+
276
+ // Final flush on session end
277
+ api.on("agent_end", async (event: unknown) => {
278
+ const endEvent = event as Record<string, unknown>;
279
+ if (endEvent.success !== false) {
280
+ await autoCapture.onAgentEnd();
281
+ }
282
+ });
283
+
284
+ logger.info("memory-cloud extension registered", { autoCapture: config.autoCapture, autoRecall: config.autoRecall });
285
+ },
286
+ };
@@ -0,0 +1,132 @@
1
+ import type { MemorySearchResult, MemoryContext, MemoryMessage, SessionMetadata } from "./types.js";
2
+
3
+ /**
4
+ * HTTP client for services/memory.
5
+ * All memory operations go through the Lambda service — no direct Turbopuffer/DynamoDB access.
6
+ */
7
+ export class MemoryClient {
8
+ constructor(
9
+ private readonly baseUrl: string,
10
+ private readonly agentApiKey: string,
11
+ private readonly agentId: string,
12
+ ) {
13
+ if (!baseUrl) throw new Error("MemoryClient: baseUrl is required");
14
+ }
15
+
16
+ async search(query: string, opts?: {
17
+ limit?: number;
18
+ topic?: string;
19
+ subtopic?: string;
20
+ tag?: string;
21
+ includeKnowledge?: boolean;
22
+ }): Promise<MemorySearchResult> {
23
+ const resp = await this.post<{ data: MemorySearchResult }>("/memory/search", {
24
+ query,
25
+ limit: opts?.limit ?? 10,
26
+ topic: opts?.topic,
27
+ subtopic: opts?.subtopic,
28
+ tag: opts?.tag,
29
+ includeKnowledge: opts?.includeKnowledge ?? true,
30
+ });
31
+ return resp.data;
32
+ }
33
+
34
+ async store(text: string, opts?: {
35
+ topic?: string;
36
+ subtopic?: string;
37
+ tag?: string;
38
+ importance?: number;
39
+ }): Promise<{ memoryId: string }> {
40
+ const resp = await this.post<{ data: { memoryId: string } }>("/memory/store", {
41
+ text,
42
+ topic: opts?.topic ?? "general",
43
+ subtopic: opts?.subtopic ?? "general",
44
+ tag: opts?.tag ?? "fact",
45
+ importance: opts?.importance ?? 0.7,
46
+ });
47
+ return resp.data;
48
+ }
49
+
50
+ async ingest(sessionKey: string, messages: MemoryMessage[], metadata?: SessionMetadata): Promise<void> {
51
+ await this.post("/memory/ingest", {
52
+ agentId: this.agentId,
53
+ sessionKey,
54
+ lastProcessedIndex: messages.length > 0 ? messages[messages.length - 1].index : -1,
55
+ messages,
56
+ metadata,
57
+ });
58
+ }
59
+
60
+ async loadContext(tier: number, topicHint?: string): Promise<MemoryContext> {
61
+ const params = new URLSearchParams({ tier: String(tier) });
62
+ if (topicHint) params.set("topicHint", topicHint);
63
+ const resp = await this.get<{ data: MemoryContext }>(`/memory/context?${params.toString()}`);
64
+ return resp.data;
65
+ }
66
+
67
+ async lookupEntity(subject: string): Promise<{
68
+ subject: string;
69
+ triples: { tripleId: string; predicate: string; object: string; validFrom: string; validTo?: string; confidence: number }[];
70
+ }> {
71
+ const resp = await this.get<{ data: { subject: string; triples: { tripleId: string; predicate: string; object: string; validFrom: string; validTo?: string; confidence: number }[] } }>(
72
+ `/memory/knowledge/entities?subject=${encodeURIComponent(subject)}`,
73
+ );
74
+ return resp.data;
75
+ }
76
+
77
+ async navigate(): Promise<unknown> {
78
+ const resp = await this.get<{ data: unknown }>("/memory/navigate");
79
+ return resp.data;
80
+ }
81
+
82
+ async deleteMemory(memoryId: string): Promise<void> {
83
+ await this.del(`/memory/${encodeURIComponent(memoryId)}`);
84
+ }
85
+
86
+ async stats(): Promise<{ vectorCount: number; tripleCount: number; storageEstimateBytes: number }> {
87
+ const resp = await this.get<{ data: { vectorCount: number; tripleCount: number; storageEstimateBytes: number } }>("/memory/stats");
88
+ return resp.data;
89
+ }
90
+
91
+ private async post<T>(path: string, body: unknown): Promise<T> {
92
+ const resp = await fetch(`${this.baseUrl}${path}`, {
93
+ method: "POST",
94
+ headers: {
95
+ "Content-Type": "application/json",
96
+ Authorization: `Bearer ${this.agentApiKey}`,
97
+ "x-agent-id": this.agentId,
98
+ },
99
+ body: JSON.stringify(body),
100
+ });
101
+ if (!resp.ok) {
102
+ throw new Error(`Memory API error: ${String(resp.status)} ${await resp.text()}`);
103
+ }
104
+ return resp.json() as Promise<T>;
105
+ }
106
+
107
+ private async get<T>(path: string): Promise<T> {
108
+ const resp = await fetch(`${this.baseUrl}${path}`, {
109
+ headers: {
110
+ Authorization: `Bearer ${this.agentApiKey}`,
111
+ "x-agent-id": this.agentId,
112
+ },
113
+ });
114
+ if (!resp.ok) {
115
+ throw new Error(`Memory API error: ${String(resp.status)} ${await resp.text()}`);
116
+ }
117
+ return resp.json() as Promise<T>;
118
+ }
119
+
120
+ private async del(path: string): Promise<void> {
121
+ const resp = await fetch(`${this.baseUrl}${path}`, {
122
+ method: "DELETE",
123
+ headers: {
124
+ Authorization: `Bearer ${this.agentApiKey}`,
125
+ "x-agent-id": this.agentId,
126
+ },
127
+ });
128
+ if (!resp.ok) {
129
+ throw new Error(`Memory API error: ${String(resp.status)} ${await resp.text()}`);
130
+ }
131
+ }
132
+ }
package/src/types.ts ADDED
@@ -0,0 +1,47 @@
1
+ export interface MemoryCloudConfig {
2
+ autoCapture: boolean;
3
+ autoRecall: boolean;
4
+ captureMaxChars: number;
5
+ idleFlushSeconds: number;
6
+ }
7
+
8
+ export interface MemoryMessage {
9
+ role: string;
10
+ content: string;
11
+ index: number;
12
+ timestamp?: string;
13
+ }
14
+
15
+ export interface SessionMetadata {
16
+ channelId?: string;
17
+ userId?: string;
18
+ userName?: string;
19
+ }
20
+
21
+ export interface MemorySearchResult {
22
+ facts: {
23
+ subject: string;
24
+ predicate: string;
25
+ object: string;
26
+ since: string;
27
+ confidence: number;
28
+ }[];
29
+ memories: {
30
+ id: string;
31
+ text: string;
32
+ topic: string;
33
+ subtopic: string;
34
+ tag: string;
35
+ importance: number;
36
+ timestamp: number;
37
+ score: number;
38
+ }[];
39
+ }
40
+
41
+ export interface MemoryContext {
42
+ tier: number;
43
+ facts: { subject: string; predicate: string; object: string; since: string }[];
44
+ memories: { text: string; topic: string; subtopic: string; score: number }[];
45
+ tokenEstimate: number;
46
+ formatted: string;
47
+ }
package/sst-env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /* This file is auto-generated by SST. Do not edit. */
2
+ /* tslint:disable */
3
+ /* eslint-disable */
4
+ /* deno-fmt-ignore-file */
5
+ /* biome-ignore-all lint: auto-generated */
6
+
7
+ /// <reference path="../../sst-env.d.ts" />
8
+
9
+ import "sst"
10
+ export {}
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }