memorylake-openclaw 1.0.1 → 1.0.2-beta.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.
Files changed (2) hide show
  1. package/index.ts +199 -114
  2. package/package.json +1 -1
package/index.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Long-term memory via MemoryLake platform.
5
5
  *
6
6
  * Features:
7
- * - 8 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search, advanced_web_search, open_data_search
7
+ * - 9 tools: memory_search, memory_list, memory_store, memory_get, memory_forget, document_search, document_download, advanced_web_search, open_data_search
8
8
  * - Auto-recall: injects relevant memories and document excerpts before each agent turn
9
9
  * - Auto-capture: stores key facts scoped to the current session after each agent turn
10
10
  * - CLI: openclaw memorylake search, openclaw memorylake stats
@@ -293,6 +293,7 @@ interface MemoryLakeProvider {
293
293
  getAll(options: ListOptions): Promise<MemoryItem[]>;
294
294
  delete(memoryId: string): Promise<void>;
295
295
  searchDocuments(query: string, topN: number): Promise<DocumentSearchResponse>;
296
+ getDocumentDownloadUrl(documentId: string): Promise<string>;
296
297
  searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse>;
297
298
  searchOpenData(query: string, options: OpenDataSearchOptions): Promise<OpenDataSearchResponse>;
298
299
  getProject(): Promise<ProjectInfo>;
@@ -418,6 +419,24 @@ class PlatformProvider implements MemoryLakeProvider {
418
419
  };
419
420
  }
420
421
 
422
+ async getDocumentDownloadUrl(documentId: string): Promise<string> {
423
+ const downloadPath = `openapi/memorylake/api/v1/projects/${this.projectId}/documents/${documentId}/download`;
424
+ const resp = await this.http.get(downloadPath, {
425
+ followRedirect: false,
426
+ responseType: "text" as any,
427
+ throwHttpErrors: false,
428
+ });
429
+ if (resp.statusCode === 303 || resp.statusCode === 302) {
430
+ const location = resp.headers.location;
431
+ if (!location) throw new Error("Download redirect missing Location header");
432
+ return location;
433
+ }
434
+ if (resp.statusCode === 404) {
435
+ throw new Error(`Document not found: ${documentId}`);
436
+ }
437
+ throw new Error(`Unexpected download response status: ${resp.statusCode}`);
438
+ }
439
+
421
440
  async searchWeb(query: string, options: WebSearchOptions): Promise<WebSearchResponse> {
422
441
  const domain = options.domain != null ? normalizeWebSearchDomain(options.domain) : "web";
423
442
  const body: Record<string, unknown> = {
@@ -821,6 +840,62 @@ const memoryPlugin = {
821
840
  // Cache project industries per session — fetched once, reused on subsequent prompts
822
841
  const sessionIndustriesCache = new Map<string, OpenDataIndustry[]>();
823
842
 
843
+ // ========================================================================
844
+ // System Prompt: Memory Section (injected into every system prompt)
845
+ // ========================================================================
846
+ api.registerMemoryPromptSection((params) => {
847
+ const lines: string[] = [
848
+ "## Memory (MANDATORY — MemoryLake)",
849
+ "",
850
+ "You have access to MemoryLake, a long-term memory system that stores the user's preferences, history, personal context, and uploaded documents across sessions.",
851
+ "",
852
+ ];
853
+
854
+ if (params.availableTools.has("memory_search")) {
855
+ lines.push(
856
+ "### memory_search — MUST be called BEFORE every response",
857
+ "",
858
+ "**RULE: Your FIRST action for EVERY user message MUST be calling `memory_search`.** No exceptions.",
859
+ "",
860
+ "This tool searches BOTH memories AND documents in one call. It returns personal context (preferences, history, decisions) and relevant document excerpts (uploaded files, tables, figures).",
861
+ "",
862
+ "This applies to ALL types of questions, not just questions about memory or recall:",
863
+ "- Greetings -> search for who the user is",
864
+ "- Recommendations (books, activities, food) -> search for preferences and interests",
865
+ "- Advice (what to wear, what to eat) -> search for habits, location, preferences",
866
+ "- Tasks (write an email, self-introduction) -> search for user name, role, background",
867
+ "- Document questions -> search for relevant uploaded document content",
868
+ "- General chat -> search for recent context and ongoing topics",
869
+ "",
870
+ "Derive a broad query from the user's message. For example:",
871
+ "- User asks for a book recommendation -> search: reading preferences favorite books",
872
+ "- User asks about weekend plans -> search: hobbies interests weekend activities",
873
+ "- User says hello -> search: user name background recent context",
874
+ "",
875
+ "**If you respond without calling memory_search first, your response is WRONG.**",
876
+ "",
877
+ );
878
+ }
879
+
880
+ if (params.availableTools.has("memory_list")) {
881
+ lines.push(
882
+ "### memory_list",
883
+ "- When the user asks what you remember or wants to see all stored memories, call `memory_list`.",
884
+ "",
885
+ );
886
+ }
887
+
888
+ if (params.availableTools.has("memory_forget")) {
889
+ lines.push(
890
+ "### memory_forget",
891
+ "- When the user explicitly asks to delete or forget a specific memory, call `memory_forget` with the memory ID.",
892
+ "",
893
+ );
894
+ }
895
+
896
+ return lines;
897
+ });
898
+
824
899
  api.logger.info(
825
900
  `memorylake-openclaw: registered (user: ${cfg.userId}, autoRecall: ${cfg.autoRecall}, autoCapture: ${cfg.autoCapture}, autoUpload: ${cfg.autoUpload})`,
826
901
  );
@@ -859,7 +934,7 @@ const memoryPlugin = {
859
934
  name: "memory_search",
860
935
  label: "Memory Search",
861
936
  description:
862
- "Search through long-term memories stored in MemoryLake. Use when you need context about user preferences, past decisions, or previously discussed topics.",
937
+ "MANDATORY: Search through long-term memories AND uploaded documents stored in MemoryLake. You MUST call this tool at the start of every conversation to recall the user's context, preferences, past decisions, previously discussed topics, and relevant document content. Always search before answering.",
863
938
  parameters: Type.Object({
864
939
  query: Type.String({ description: "Search query" }),
865
940
  limit: Type.Optional(
@@ -894,54 +969,65 @@ const memoryPlugin = {
894
969
  scope?: "session" | "long-term" | "all";
895
970
  };
896
971
 
897
- try {
898
- const results = await effectiveProvider.search(
972
+ const [memoryResult, docResult] = await Promise.allSettled([
973
+ effectiveProvider.search(
899
974
  query,
900
975
  buildSearchOptions(effectiveCfg, userId, limit),
901
- );
976
+ ),
977
+ effectiveProvider.searchDocuments(query, effectiveCfg.topK),
978
+ ]);
902
979
 
903
- if (!results || results.length === 0) {
904
- return {
905
- content: [
906
- { type: "text", text: "No relevant memories found." },
907
- ],
908
- details: { count: 0 },
909
- };
910
- }
980
+ const sections: string[] = [];
981
+ let memoryCount = 0;
982
+ let docCount = 0;
983
+ let sanitizedMemories: { id: string; content: string; created_at: string }[] = [];
911
984
 
985
+ if (memoryResult.status === "fulfilled" && memoryResult.value.length > 0) {
986
+ const results = memoryResult.value;
987
+ memoryCount = results.length;
912
988
  const text = results
913
- .map(
914
- (r, i) =>
915
- `${i + 1}. ${r.content} (id: ${r.id})`,
916
- )
989
+ .map((r, i) => `${i + 1}. ${r.content} (id: ${r.id})`)
917
990
  .join("\n");
918
-
919
- const sanitized = results.map((r) => ({
991
+ sections.push(`## Memories\nFound ${results.length} memories:\n\n${text}`);
992
+ sanitizedMemories = results.map((r) => ({
920
993
  id: r.id,
921
994
  content: r.content,
922
995
  created_at: r.created_at,
923
996
  }));
997
+ } else if (memoryResult.status === "rejected") {
998
+ sections.push(`## Memories\nMemory search failed: ${String(memoryResult.reason)}`);
999
+ }
924
1000
 
1001
+ if (docResult.status === "fulfilled" && docResult.value.results.length > 0) {
1002
+ docCount = docResult.value.results.length;
1003
+ const context = buildDocumentContext(docResult.value.results);
1004
+ sections.push(`## Documents\nFound ${docCount} document results:\n\n${context}`);
1005
+ } else if (docResult.status === "rejected") {
1006
+ sections.push(`## Documents\nDocument search failed: ${String(docResult.reason)}`);
1007
+ }
1008
+
1009
+ if (memoryCount === 0 && docCount === 0) {
925
1010
  return {
926
1011
  content: [
927
- {
928
- type: "text",
929
- text: `Found ${results.length} memories:\n\n${text}`,
930
- },
931
- ],
932
- details: { count: results.length, memories: sanitized },
933
- };
934
- } catch (err) {
935
- return {
936
- content: [
937
- {
938
- type: "text",
939
- text: `Memory search failed: ${String(err)}`,
940
- },
1012
+ { type: "text", text: "No relevant memories or documents found." },
941
1013
  ],
942
- details: { error: String(err) },
1014
+ details: { count: 0 },
943
1015
  };
944
1016
  }
1017
+
1018
+ return {
1019
+ content: [
1020
+ {
1021
+ type: "text",
1022
+ text: sections.join("\n\n"),
1023
+ },
1024
+ ],
1025
+ details: {
1026
+ memoryCount,
1027
+ documentCount: docCount,
1028
+ memories: sanitizedMemories,
1029
+ },
1030
+ };
945
1031
  },
946
1032
  }),
947
1033
  { name: "memory_search" },
@@ -1234,6 +1320,55 @@ const memoryPlugin = {
1234
1320
  { name: "document_search" },
1235
1321
  );
1236
1322
 
1323
+ api.registerTool(
1324
+ (ctx) => ({
1325
+ name: "document_download",
1326
+ label: "Document Download",
1327
+ description:
1328
+ "Download a document (image, PDF, etc.) from MemoryLake and send it to the user. Returns a temporary download URL. Use document_search first to find the document_id.",
1329
+ parameters: Type.Object({
1330
+ documentId: Type.String({
1331
+ description:
1332
+ "The document ID to download (from document_search results or document listing)",
1333
+ }),
1334
+ }),
1335
+ async execute(_toolCallId, params) {
1336
+ const effectiveCfg = resolveConfig(ctx);
1337
+ const effectiveProvider = getProvider(effectiveCfg);
1338
+ const { documentId } = params as { documentId: string };
1339
+
1340
+ try {
1341
+ const downloadUrl =
1342
+ await effectiveProvider.getDocumentDownloadUrl(documentId);
1343
+
1344
+ return {
1345
+ content: [
1346
+ {
1347
+ type: "text",
1348
+ text: `Document ${documentId} is ready. Tell the user you are sending the file now.\nMEDIA: ${downloadUrl}`,
1349
+ },
1350
+ ],
1351
+ details: {
1352
+ media: { mediaUrl: downloadUrl },
1353
+ documentId,
1354
+ },
1355
+ };
1356
+ } catch (err) {
1357
+ return {
1358
+ content: [
1359
+ {
1360
+ type: "text",
1361
+ text: `Document download failed: ${String(err)}`,
1362
+ },
1363
+ ],
1364
+ details: { error: String(err) },
1365
+ };
1366
+ }
1367
+ },
1368
+ }),
1369
+ { name: "document_download" },
1370
+ );
1371
+
1237
1372
  api.registerTool(
1238
1373
  (ctx) => ({
1239
1374
  name: "advanced_web_search",
@@ -1811,7 +1946,9 @@ const memoryPlugin = {
1811
1946
  }
1812
1947
 
1813
1948
  // ------------------------------------------------------------------
1814
- // Auto-recall: inject relevant memories and documents before prompt build
1949
+ // Auto-recall: inject system-level memory instructions and open data
1950
+ // categories before prompt build. Memory content is NOT pre-fetched;
1951
+ // the model is instructed to call memory_search itself.
1815
1952
  // ------------------------------------------------------------------
1816
1953
  if (cfg.autoRecall) {
1817
1954
  api.on("before_prompt_build", async (event, ctx) => {
@@ -1826,18 +1963,6 @@ const memoryPlugin = {
1826
1963
 
1827
1964
  const sessionId = (ctx as any)?.sessionId ?? undefined;
1828
1965
 
1829
- // LLM-rewrite FIRST — short prompts like "它呢?" can become meaningful
1830
- // search queries when the LLM has conversation history context.
1831
- const searchQuery = await rewriteQueryForSearch(event.prompt, event.messages, ctx);
1832
-
1833
- // Only skip if the rewritten result is still too short
1834
- if (searchQuery.length < 5) {
1835
- api.logger.info(
1836
- `memorylake-openclaw: skipping auto-recall, rewritten query too short (${searchQuery.length} chars)`,
1837
- );
1838
- return;
1839
- }
1840
-
1841
1966
  // Fetch industries once per session, then cache
1842
1967
  let industries: OpenDataIndustry[] | undefined;
1843
1968
  if (sessionId && sessionIndustriesCache.has(sessionId)) {
@@ -1854,82 +1979,42 @@ const memoryPlugin = {
1854
1979
  }
1855
1980
  }
1856
1981
 
1857
- const [memoryResult, docResult] = await Promise.allSettled([
1858
- effectiveProvider.search(searchQuery, buildSearchOptions(effectiveCfg)),
1859
- effectiveProvider.searchDocuments(searchQuery, effectiveCfg.topK),
1860
- ]);
1861
-
1862
- const contextParts: string[] = [];
1863
-
1864
- if (memoryResult.status === "fulfilled" && memoryResult.value.length > 0) {
1865
- const memories = memoryResult.value;
1866
- const memoryContext = memories
1867
- .map((r) => `- ${r.content}`)
1868
- .join("\n");
1869
- contextParts.push(
1870
- `<relevant-memories>\nThe following memories may be relevant to this conversation:\n${memoryContext}\n</relevant-memories>`,
1871
- );
1872
- api.logger.info(
1873
- `memorylake-openclaw: injecting ${memories.length} memories into context`,
1874
- );
1875
-
1876
- // Fetch conflict details for memories flagged with unresolved conflicts
1877
- const conflictedIds = memories
1878
- .filter((m) => m.has_unresolved_conflict)
1879
- .map((m) => m.id);
1880
- if (conflictedIds.length > 0) {
1881
- try {
1882
- const conflicts = await effectiveProvider.listConflicts(conflictedIds, effectiveCfg.userId);
1883
- if (conflicts.length > 0) {
1884
- const conflictContext = buildConflictContext(conflicts);
1885
- contextParts.push(
1886
- `<memory-conflicts>\nThe following conflicts exist among the recalled memories. ` +
1887
- `Consider these contradictions when using the above memories.\n` +
1888
- `If you have not already informed the user about these conflicts in this conversation, briefly mention that some recalled memories contain contradictions and note which points are uncertain. Do not repeat this notice if you have already done so.\n` +
1889
- `${conflictContext}\n</memory-conflicts>`,
1890
- );
1891
- api.logger.info(
1892
- `memorylake-openclaw: injecting ${conflicts.length} memory conflicts into context`,
1893
- );
1894
- }
1895
- } catch (err) {
1896
- api.logger.warn(`memorylake-openclaw: conflict fetch failed: ${String(err)}`);
1897
- }
1898
- }
1899
- } else if (memoryResult.status === "rejected") {
1900
- api.logger.warn(`memorylake-openclaw: memory recall failed: ${String(memoryResult.reason)}`);
1901
- }
1902
-
1903
- if (docResult.status === "fulfilled" && docResult.value.results.length > 0) {
1904
- const docContext = buildDocumentContext(docResult.value.results);
1905
- contextParts.push(
1906
- `<relevant-documents>\nThe following document excerpts may be relevant to this conversation:\n${docContext}\n</relevant-documents>`,
1907
- );
1908
- api.logger.info(
1909
- `memorylake-openclaw: injecting ${docResult.value.results.length} document results into context`,
1910
- );
1911
- } else if (docResult.status === "rejected") {
1912
- api.logger.warn(`memorylake-openclaw: document search failed: ${String(docResult.reason)}`);
1913
- }
1914
-
1915
- const result: { prependContext?: string; appendSystemContext?: string } = {};
1916
-
1917
- if (contextParts.length > 0) {
1918
- result.prependContext = contextParts.join("\n\n");
1919
- }
1920
-
1982
+ const result: { prependSystemContext?: string; appendSystemContext?: string } = {};
1983
+
1984
+ const memoryDirectiveParts: string[] = [
1985
+ `<memory-instructions>`,
1986
+ `You have access to the user's long-term memory AND documents via MemoryLake (memory_search tool).`,
1987
+ `The memory_search tool searches both memories and uploaded documents in a single call.`,
1988
+ ``,
1989
+ `MANDATORY PROCEDURE for EVERY user message:`,
1990
+ `1. Read the user's message.`,
1991
+ `2. Call memory_search with a query derived from the message — this step is NOT optional.`,
1992
+ `3. Use the search results (memories + documents) to personalize your response.`,
1993
+ `4. Respond to the user.`,
1994
+ ``,
1995
+ `This applies to ALL messages greetings, recommendations, advice, casual chat, tasks, questions.`,
1996
+ `The user's preferences, history, name, hobbies, context, and documents are stored in MemoryLake.`,
1997
+ `A response without memory_search is INCORRECT because it ignores personalized context.`,
1998
+ `</memory-instructions>`,
1999
+ ];
2000
+ result.prependSystemContext = memoryDirectiveParts.join("\n");
2001
+
2002
+ const appendParts: string[] = [];
1921
2003
  if (industries && industries.length > 0) {
1922
2004
  const categoryList = industries
1923
2005
  .map((ind) => `- ${ind.id}: ${ind.name}${ind.description ? ` — ${ind.description}` : ""}`)
1924
2006
  .join("\n");
1925
- result.appendSystemContext =
1926
- `<open-data-categories>\nThis project has access to the following open data categories via the open_data_search tool:\n${categoryList}\nWhen the user's question relates to any of these categories, use the open_data_search tool to retrieve relevant data.\n</open-data-categories>`;
2007
+ appendParts.push(
2008
+ `<open-data-categories>\nThis project has access to the following open data categories via the open_data_search tool:\n${categoryList}\nWhen the user's question relates to any of these categories, use the open_data_search tool to retrieve relevant data.\n</open-data-categories>`,
2009
+ );
1927
2010
  api.logger.info(
1928
2011
  `memorylake-openclaw: injecting ${industries.length} open data categories into system context`,
1929
2012
  );
1930
2013
  }
1931
2014
 
1932
- if (!result.prependContext && !result.appendSystemContext) return;
2015
+ if (appendParts.length > 0) {
2016
+ result.appendSystemContext = appendParts.join("\n\n");
2017
+ }
1933
2018
 
1934
2019
  return result;
1935
2020
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorylake-openclaw",
3
- "version": "1.0.1",
3
+ "version": "1.0.2-beta.1",
4
4
  "type": "module",
5
5
  "description": "MemoryLake memory backend for OpenClaw",
6
6
  "license": "MIT",