@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.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * HTTP client for services/memory.
3
+ * All memory operations go through the Lambda service — no direct Turbopuffer/DynamoDB access.
4
+ */
5
+ export class MemoryClient {
6
+ baseUrl;
7
+ agentApiKey;
8
+ agentId;
9
+ constructor(baseUrl, agentApiKey, agentId) {
10
+ this.baseUrl = baseUrl;
11
+ this.agentApiKey = agentApiKey;
12
+ this.agentId = agentId;
13
+ if (!baseUrl)
14
+ throw new Error("MemoryClient: baseUrl is required");
15
+ }
16
+ async search(query, opts) {
17
+ const resp = await this.post("/memory/search", {
18
+ query,
19
+ limit: opts?.limit ?? 10,
20
+ topic: opts?.topic,
21
+ subtopic: opts?.subtopic,
22
+ tag: opts?.tag,
23
+ includeKnowledge: opts?.includeKnowledge ?? true,
24
+ });
25
+ return resp.data;
26
+ }
27
+ async store(text, opts) {
28
+ const resp = await this.post("/memory/store", {
29
+ text,
30
+ topic: opts?.topic ?? "general",
31
+ subtopic: opts?.subtopic ?? "general",
32
+ tag: opts?.tag ?? "fact",
33
+ importance: opts?.importance ?? 0.7,
34
+ });
35
+ return resp.data;
36
+ }
37
+ async ingest(sessionKey, messages, metadata) {
38
+ await this.post("/memory/ingest", {
39
+ agentId: this.agentId,
40
+ sessionKey,
41
+ lastProcessedIndex: messages.length > 0 ? messages[messages.length - 1].index : -1,
42
+ messages,
43
+ metadata,
44
+ });
45
+ }
46
+ async loadContext(tier, topicHint) {
47
+ const params = new URLSearchParams({ tier: String(tier) });
48
+ if (topicHint)
49
+ params.set("topicHint", topicHint);
50
+ const resp = await this.get(`/memory/context?${params.toString()}`);
51
+ return resp.data;
52
+ }
53
+ async lookupEntity(subject) {
54
+ const resp = await this.get(`/memory/knowledge/entities?subject=${encodeURIComponent(subject)}`);
55
+ return resp.data;
56
+ }
57
+ async navigate() {
58
+ const resp = await this.get("/memory/navigate");
59
+ return resp.data;
60
+ }
61
+ async deleteMemory(memoryId) {
62
+ await this.del(`/memory/${encodeURIComponent(memoryId)}`);
63
+ }
64
+ async stats() {
65
+ const resp = await this.get("/memory/stats");
66
+ return resp.data;
67
+ }
68
+ async post(path, body) {
69
+ const resp = await fetch(`${this.baseUrl}${path}`, {
70
+ method: "POST",
71
+ headers: {
72
+ "Content-Type": "application/json",
73
+ Authorization: `Bearer ${this.agentApiKey}`,
74
+ "x-agent-id": this.agentId,
75
+ },
76
+ body: JSON.stringify(body),
77
+ });
78
+ if (!resp.ok) {
79
+ throw new Error(`Memory API error: ${String(resp.status)} ${await resp.text()}`);
80
+ }
81
+ return resp.json();
82
+ }
83
+ async get(path) {
84
+ const resp = await fetch(`${this.baseUrl}${path}`, {
85
+ headers: {
86
+ Authorization: `Bearer ${this.agentApiKey}`,
87
+ "x-agent-id": this.agentId,
88
+ },
89
+ });
90
+ if (!resp.ok) {
91
+ throw new Error(`Memory API error: ${String(resp.status)} ${await resp.text()}`);
92
+ }
93
+ return resp.json();
94
+ }
95
+ async del(path) {
96
+ const resp = await fetch(`${this.baseUrl}${path}`, {
97
+ method: "DELETE",
98
+ headers: {
99
+ Authorization: `Bearer ${this.agentApiKey}`,
100
+ "x-agent-id": this.agentId,
101
+ },
102
+ });
103
+ if (!resp.ok) {
104
+ throw new Error(`Memory API error: ${String(resp.status)} ${await resp.text()}`);
105
+ }
106
+ }
107
+ }
108
+ //# sourceMappingURL=memory-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-client.js","sourceRoot":"","sources":["../src/memory-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ export interface MemoryCloudConfig {
2
+ autoCapture: boolean;
3
+ autoRecall: boolean;
4
+ captureMaxChars: number;
5
+ idleFlushSeconds: number;
6
+ }
7
+ export interface MemoryMessage {
8
+ role: string;
9
+ content: string;
10
+ index: number;
11
+ timestamp?: string;
12
+ }
13
+ export interface SessionMetadata {
14
+ channelId?: string;
15
+ userId?: string;
16
+ userName?: string;
17
+ }
18
+ export interface MemorySearchResult {
19
+ facts: {
20
+ subject: string;
21
+ predicate: string;
22
+ object: string;
23
+ since: string;
24
+ confidence: number;
25
+ }[];
26
+ memories: {
27
+ id: string;
28
+ text: string;
29
+ topic: string;
30
+ subtopic: string;
31
+ tag: string;
32
+ importance: number;
33
+ timestamp: number;
34
+ score: number;
35
+ }[];
36
+ }
37
+ export interface MemoryContext {
38
+ tier: number;
39
+ facts: {
40
+ subject: string;
41
+ predicate: string;
42
+ object: string;
43
+ since: string;
44
+ }[];
45
+ memories: {
46
+ text: string;
47
+ topic: string;
48
+ subtopic: string;
49
+ score: number;
50
+ }[];
51
+ tokenEstimate: number;
52
+ formatted: string;
53
+ }
54
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE;QACL,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;KACpB,EAAE,CAAC;IACJ,QAAQ,EAAE;QACR,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,EAAE,MAAM,CAAC;QACjB,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC;KACf,EAAE,CAAC;CACL;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC/E,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC7E,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ {
2
+ "id": "memory-cloud",
3
+ "kind": "memory",
4
+ "displayName": "Cloud Memory",
5
+ "description": "Persistent agent memory backed by Turbopuffer vectors and DynamoDB knowledge graph. Replaces the builtin LanceDB memory extension.",
6
+ "version": "0.1.0",
7
+ "configSchema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "autoCapture": {
11
+ "type": "boolean",
12
+ "default": true,
13
+ "description": "Automatically capture memories from conversations"
14
+ },
15
+ "autoRecall": {
16
+ "type": "boolean",
17
+ "default": true,
18
+ "description": "Automatically inject relevant memories into context at session start"
19
+ },
20
+ "captureMaxChars": {
21
+ "type": "number",
22
+ "default": 500,
23
+ "minimum": 100,
24
+ "maximum": 10000,
25
+ "description": "Maximum message length for auto-capture"
26
+ },
27
+ "idleFlushSeconds": {
28
+ "type": "number",
29
+ "default": 60,
30
+ "minimum": 10,
31
+ "maximum": 600,
32
+ "description": "Flush memories after this many seconds of no messages"
33
+ }
34
+ }
35
+ },
36
+ "uiHints": {
37
+ "autoCapture": {
38
+ "label": "Auto-capture",
39
+ "description": "Automatically store memories from conversations"
40
+ },
41
+ "autoRecall": {
42
+ "label": "Auto-recall",
43
+ "description": "Inject relevant memories into context at session start"
44
+ },
45
+ "idleFlushSeconds": {
46
+ "label": "Idle flush interval",
47
+ "description": "Seconds of inactivity before flushing pending memories"
48
+ }
49
+ }
50
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@alfe.ai/openclaw-memory-cloud",
3
+ "version": "0.0.1",
4
+ "description": "Cloud memory extension for OpenClaw — Turbopuffer vectors + DynamoDB knowledge graph",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "license": "UNLICENSED",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "typecheck": "tsc --noEmit",
12
+ "lint": "eslint ."
13
+ }
14
+ }
@@ -0,0 +1,96 @@
1
+ import type { MemoryClient } from "./memory-client.js";
2
+ import type { MemoryCloudConfig, MemoryMessage, SessionMetadata } from "./types.js";
3
+
4
+ /**
5
+ * Auto-capture manages the idle debounce timer and flushes
6
+ * accumulated messages to the memory service.
7
+ *
8
+ * Flow:
9
+ * 1. on_message_received: track message, reset timer
10
+ * 2. Timer fires (60s idle): flush batch to /memory/ingest
11
+ * 3. agent_end: final flush of remaining messages
12
+ */
13
+ export class AutoCapture {
14
+ private static readonly MAX_QUEUE_SIZE = 500;
15
+ private pendingMessages: MemoryMessage[] = [];
16
+ private messageIndex = 0;
17
+ private idleTimer: ReturnType<typeof setTimeout> | null = null;
18
+ private sessionKey: string | null = null;
19
+ private metadata: SessionMetadata = {};
20
+
21
+ constructor(
22
+ private readonly client: MemoryClient,
23
+ private readonly config: MemoryCloudConfig,
24
+ private readonly logger: { debug: (msg: string, ctx?: Record<string, unknown>) => void; warn: (msg: string, ctx?: Record<string, unknown>) => void },
25
+ ) {}
26
+
27
+ setSession(sessionKey: string, metadata?: SessionMetadata): void {
28
+ this.sessionKey = sessionKey;
29
+ if (metadata) this.metadata = metadata;
30
+ }
31
+
32
+ trackMessage(role: string, content: string): void {
33
+ if (!this.config.autoCapture) return;
34
+ if (content.length > this.config.captureMaxChars) {
35
+ content = content.slice(0, this.config.captureMaxChars);
36
+ }
37
+
38
+ // Enforce max queue size — drop oldest if exceeded
39
+ if (this.pendingMessages.length >= AutoCapture.MAX_QUEUE_SIZE) {
40
+ this.logger.warn("Memory queue full — dropping oldest message", { queueSize: this.pendingMessages.length });
41
+ this.pendingMessages.shift();
42
+ }
43
+
44
+ this.pendingMessages.push({
45
+ role,
46
+ content,
47
+ index: this.messageIndex++,
48
+ timestamp: new Date().toISOString(),
49
+ });
50
+
51
+ this.resetIdleTimer();
52
+ }
53
+
54
+ async flush(): Promise<void> {
55
+ if (this.pendingMessages.length === 0 || !this.sessionKey) return;
56
+
57
+ const batch = [...this.pendingMessages];
58
+ this.pendingMessages = [];
59
+
60
+ this.logger.debug("Flushing memory batch", {
61
+ sessionKey: this.sessionKey,
62
+ messageCount: batch.length,
63
+ });
64
+
65
+ try {
66
+ await this.client.ingest(this.sessionKey, batch, this.metadata);
67
+ } catch (err) {
68
+ this.logger.warn("Failed to flush memories", { error: String(err) });
69
+ // Re-add failed messages for next flush attempt
70
+ this.pendingMessages.unshift(...batch);
71
+ }
72
+ }
73
+
74
+ async onAgentEnd(): Promise<void> {
75
+ this.clearIdleTimer();
76
+ await this.flush();
77
+ }
78
+
79
+ destroy(): void {
80
+ this.clearIdleTimer();
81
+ }
82
+
83
+ private resetIdleTimer(): void {
84
+ this.clearIdleTimer();
85
+ this.idleTimer = setTimeout(() => {
86
+ void this.flush();
87
+ }, this.config.idleFlushSeconds * 1000);
88
+ }
89
+
90
+ private clearIdleTimer(): void {
91
+ if (this.idleTimer) {
92
+ clearTimeout(this.idleTimer);
93
+ this.idleTimer = null;
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,67 @@
1
+ import type { MemoryClient } from "./memory-client.js";
2
+ import type { MemoryCloudConfig } from "./types.js";
3
+
4
+ /**
5
+ * Auto-recall loads tiered memory context at session start
6
+ * and formats it for injection into the agent's prompt.
7
+ *
8
+ * Called from the before_agent_start hook.
9
+ */
10
+ export class AutoRecall {
11
+ constructor(
12
+ private readonly client: MemoryClient,
13
+ private readonly config: MemoryCloudConfig,
14
+ private readonly logger: { debug: (msg: string, ctx?: Record<string, unknown>) => void; warn: (msg: string, ctx?: Record<string, unknown>) => void },
15
+ ) {}
16
+
17
+ /**
18
+ * Load memory context and return formatted XML for prompt injection.
19
+ * Returns prependContext string for the before_agent_start hook result.
20
+ */
21
+ async loadForPrompt(userMessage: string): Promise<string | undefined> {
22
+ if (!this.config.autoRecall) return undefined;
23
+
24
+ try {
25
+ // Detect topic from user message (first significant word)
26
+ const topicHint = extractTopicHint(userMessage);
27
+
28
+ // Load L1 (facts) + L2 (topic context) in one call
29
+ const tier = topicHint ? 2 : 1;
30
+ const context = await this.client.loadContext(tier, topicHint);
31
+
32
+ if (!context.formatted || context.formatted.length === 0) {
33
+ this.logger.debug("No relevant memories found for prompt");
34
+ return undefined;
35
+ }
36
+
37
+ this.logger.debug("Loaded memory context for prompt", {
38
+ tier,
39
+ topicHint,
40
+ factCount: context.facts.length,
41
+ memoryCount: context.memories.length,
42
+ tokenEstimate: context.tokenEstimate,
43
+ });
44
+
45
+ return context.formatted;
46
+ } catch (err) {
47
+ this.logger.warn("Failed to load memory context", { error: String(err) });
48
+ return undefined;
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Simple topic hint extraction from user message.
55
+ * Returns the first capitalized word or proper noun as a potential topic.
56
+ */
57
+ function extractTopicHint(message: string): string | undefined {
58
+ // Look for proper nouns (capitalized words not at sentence start)
59
+ const words = message.split(/\s+/);
60
+ for (let i = 1; i < words.length; i++) {
61
+ const word = words[i];
62
+ if (word && word.length > 2 && /^[A-Z]/.test(word) && !/^(The|And|But|For|With|This|That|What|How|Why|When|Where)$/.test(word)) {
63
+ return word.toLowerCase();
64
+ }
65
+ }
66
+ return undefined;
67
+ }
@@ -0,0 +1,30 @@
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
+ }