diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 93c52013dcc..8008bcf8c7c 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -296,6 +296,7 @@ Current bundled provider examples: | `plugin-sdk/memory-host-events` | Memory host event journal alias | Vendor-neutral alias for memory host event journal helpers | | `plugin-sdk/memory-host-files` | Memory host file/runtime alias | Vendor-neutral alias for memory host file/runtime helpers | | `plugin-sdk/memory-host-markdown` | Managed markdown helpers | Shared managed-markdown helpers for memory-adjacent plugins | + | `plugin-sdk/memory-host-search` | Active memory search facade | Lazy active-memory search-manager runtime facade | | `plugin-sdk/memory-host-status` | Memory host status alias | Vendor-neutral alias for memory host status helpers | | `plugin-sdk/memory-lancedb` | Bundled memory-lancedb helpers | Memory-lancedb helper surface | | `plugin-sdk/testing` | Test utilities | Test helpers and mocks | diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 7dbd4f00f04..bf97f3d43b3 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -262,6 +262,7 @@ explicitly promotes one as public. | `plugin-sdk/memory-host-events` | Vendor-neutral alias for memory host event journal helpers | | `plugin-sdk/memory-host-files` | Vendor-neutral alias for memory host file/runtime helpers | | `plugin-sdk/memory-host-markdown` | Shared managed-markdown helpers for memory-adjacent plugins | + | `plugin-sdk/memory-host-search` | Active memory runtime facade for search-manager access | | `plugin-sdk/memory-host-status` | Vendor-neutral alias for memory host status helpers | | `plugin-sdk/memory-lancedb` | Bundled memory-lancedb helper surface | diff --git a/extensions/memory-wiki/src/cli.ts b/extensions/memory-wiki/src/cli.ts index 84bd4a4c424..4fb54b04850 100644 --- a/extensions/memory-wiki/src/cli.ts +++ b/extensions/memory-wiki/src/cli.ts @@ -249,17 +249,18 @@ export async function runWikiSearch(params: { await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig }); const results = await searchMemoryWiki({ config: params.config, + appConfig: params.appConfig, query: params.query, maxResults: params.maxResults, }); const summary = params.json ? JSON.stringify(results, null, 2) : results.length === 0 - ? "No wiki results." + ? "No wiki or memory results." : results .map( (result, index) => - `${index + 1}. ${result.title} (${result.kind})\nPath: ${result.path}\nSnippet: ${result.snippet}`, + `${index + 1}. ${result.title} (${result.corpus}/${result.kind})\nPath: ${result.path}${typeof result.startLine === "number" && typeof result.endLine === "number" ? `\nLines: ${result.startLine}-${result.endLine}` : ""}\nSnippet: ${result.snippet}`, ) .join("\n\n"); writeOutput(summary, params.stdout); @@ -278,6 +279,7 @@ export async function runWikiGet(params: { await syncMemoryWikiImportedSources({ config: params.config, appConfig: params.appConfig }); const result = await getMemoryWikiPage({ config: params.config, + appConfig: params.appConfig, lookup: params.lookup, fromLine: params.fromLine, lineCount: params.lineCount, @@ -540,7 +542,7 @@ export function registerWikiCli( wiki .command("search") - .description("Search wiki pages") + .description("Search wiki pages and, when configured, the active memory corpus") .argument("", "Search query") .option("--max-results ", "Maximum results", (value: string) => Number(value)) .option("--json", "Print JSON") @@ -556,7 +558,7 @@ export function registerWikiCli( wiki .command("get") - .description("Read a wiki page by id or relative path") + .description("Read a wiki page by id or relative path, with optional active-memory fallback") .argument("", "Relative path or page id") .option("--from ", "Start line", (value: string) => Number(value)) .option("--lines ", "Number of lines", (value: string) => Number(value)) diff --git a/extensions/memory-wiki/src/gateway.ts b/extensions/memory-wiki/src/gateway.ts index 724ed4dd1f3..4fd3ef58e97 100644 --- a/extensions/memory-wiki/src/gateway.ts +++ b/extensions/memory-wiki/src/gateway.ts @@ -207,6 +207,7 @@ export function registerMemoryWikiGatewayMethods(params: { true, await searchMemoryWiki({ config, + appConfig, query, maxResults, }), @@ -249,6 +250,7 @@ export function registerMemoryWikiGatewayMethods(params: { true, await getMemoryWikiPage({ config, + appConfig, lookup, fromLine, lineCount, diff --git a/extensions/memory-wiki/src/query.test.ts b/extensions/memory-wiki/src/query.test.ts index 7b212fee1ac..2afc4b39777 100644 --- a/extensions/memory-wiki/src/query.test.ts +++ b/extensions/memory-wiki/src/query.test.ts @@ -1,20 +1,69 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../api.js"; import { resolveMemoryWikiConfig } from "./config.js"; import { renderWikiMarkdown } from "./markdown.js"; import { getMemoryWikiPage, searchMemoryWiki } from "./query.js"; import { initializeMemoryWikiVault } from "./vault.js"; +const { getActiveMemorySearchManagerMock } = vi.hoisted(() => ({ + getActiveMemorySearchManagerMock: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/memory-host-search", () => ({ + getActiveMemorySearchManager: getActiveMemorySearchManagerMock, +})); + const tempDirs: string[] = []; afterEach(async () => { await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); }); +beforeEach(() => { + getActiveMemorySearchManagerMock.mockReset(); + getActiveMemorySearchManagerMock.mockResolvedValue({ manager: null, error: "unavailable" }); +}); + +function createAppConfig(): OpenClawConfig { + return { + agents: { + list: [{ id: "main", default: true }], + }, + } as OpenClawConfig; +} + +function createMemoryManager(overrides?: { + searchResults?: Array<{ + path: string; + startLine: number; + endLine: number; + score: number; + snippet: string; + source: "memory" | "sessions"; + citation?: string; + }>; + readResult?: { text: string; path: string }; +}) { + return { + search: vi.fn().mockResolvedValue(overrides?.searchResults ?? []), + readFile: vi.fn().mockImplementation(async () => { + if (!overrides?.readResult) { + throw new Error("missing"); + } + return overrides.readResult; + }), + status: vi.fn().mockReturnValue({ backend: "builtin", provider: "builtin" }), + probeEmbeddingAvailability: vi.fn().mockResolvedValue({ ok: true }), + probeVectorAvailability: vi.fn().mockResolvedValue(false), + close: vi.fn().mockResolvedValue(undefined), + }; +} + describe("searchMemoryWiki", () => { - it("finds pages by title and body", async () => { + it("finds wiki pages by title and body", async () => { const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-")); tempDirs.push(rootDir); const config = resolveMemoryWikiConfig( @@ -34,12 +83,105 @@ describe("searchMemoryWiki", () => { const results = await searchMemoryWiki({ config, query: "alpha" }); expect(results).toHaveLength(1); + expect(results[0]?.corpus).toBe("wiki"); expect(results[0]?.path).toBe("sources/alpha.md"); + expect(getActiveMemorySearchManagerMock).not.toHaveBeenCalled(); + }); + + it("includes active memory results when shared search and all corpora are enabled", async () => { + const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-")); + tempDirs.push(rootDir); + const config = resolveMemoryWikiConfig( + { + vault: { path: rootDir }, + search: { backend: "shared", corpus: "all" }, + }, + { homedir: "/Users/tester" }, + ); + await initializeMemoryWikiVault(config); + await fs.writeFile( + path.join(rootDir, "sources", "alpha.md"), + renderWikiMarkdown({ + frontmatter: { pageType: "source", id: "source.alpha", title: "Alpha Source" }, + body: "# Alpha Source\n\nalpha body text\n", + }), + "utf8", + ); + const manager = createMemoryManager({ + searchResults: [ + { + path: "MEMORY.md", + startLine: 4, + endLine: 8, + score: 42, + snippet: "alpha durable memory", + source: "memory", + citation: "MEMORY.md#L4-L8", + }, + ], + }); + getActiveMemorySearchManagerMock.mockResolvedValue({ manager }); + + const results = await searchMemoryWiki({ + config, + appConfig: createAppConfig(), + query: "alpha", + maxResults: 5, + }); + + expect(results).toHaveLength(2); + expect(results.some((result) => result.corpus === "wiki")).toBe(true); + expect(results.some((result) => result.corpus === "memory")).toBe(true); + expect(manager.search).toHaveBeenCalledWith("alpha", { maxResults: 5 }); + }); + + it("keeps memory search disabled when the backend is local", async () => { + const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-")); + tempDirs.push(rootDir); + const config = resolveMemoryWikiConfig( + { + vault: { path: rootDir }, + search: { backend: "local", corpus: "all" }, + }, + { homedir: "/Users/tester" }, + ); + await initializeMemoryWikiVault(config); + await fs.writeFile( + path.join(rootDir, "sources", "alpha.md"), + renderWikiMarkdown({ + frontmatter: { pageType: "source", id: "source.alpha", title: "Alpha Source" }, + body: "# Alpha Source\n\nalpha only wiki\n", + }), + "utf8", + ); + const manager = createMemoryManager({ + searchResults: [ + { + path: "MEMORY.md", + startLine: 1, + endLine: 2, + score: 50, + snippet: "alpha memory", + source: "memory", + }, + ], + }); + getActiveMemorySearchManagerMock.mockResolvedValue({ manager }); + + const results = await searchMemoryWiki({ + config, + appConfig: createAppConfig(), + query: "alpha", + }); + + expect(results).toHaveLength(1); + expect(results[0]?.corpus).toBe("wiki"); + expect(manager.search).not.toHaveBeenCalled(); }); }); describe("getMemoryWikiPage", () => { - it("reads pages by relative path and slices line ranges", async () => { + it("reads wiki pages by relative path and slices line ranges", async () => { const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-")); tempDirs.push(rootDir); const config = resolveMemoryWikiConfig( @@ -63,9 +205,53 @@ describe("getMemoryWikiPage", () => { lineCount: 2, }); + expect(result?.corpus).toBe("wiki"); expect(result?.path).toBe("sources/alpha.md"); expect(result?.content).toContain("line one"); expect(result?.content).toContain("line two"); expect(result?.content).not.toContain("line three"); }); + + it("falls back to active memory reads when memory corpus is selected", async () => { + const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-query-")); + tempDirs.push(rootDir); + const config = resolveMemoryWikiConfig( + { + vault: { path: rootDir }, + search: { backend: "shared", corpus: "memory" }, + }, + { homedir: "/Users/tester" }, + ); + await initializeMemoryWikiVault(config); + const manager = createMemoryManager({ + readResult: { + path: "MEMORY.md", + text: "durable alpha memory\nline two", + }, + }); + getActiveMemorySearchManagerMock.mockResolvedValue({ manager }); + + const result = await getMemoryWikiPage({ + config, + appConfig: createAppConfig(), + lookup: "MEMORY.md", + fromLine: 2, + lineCount: 2, + }); + + expect(result).toEqual({ + corpus: "memory", + path: "MEMORY.md", + title: "MEMORY", + kind: "memory", + content: "durable alpha memory\nline two", + fromLine: 2, + lineCount: 2, + }); + expect(manager.readFile).toHaveBeenCalledWith({ + relPath: "MEMORY.md", + from: 2, + lines: 2, + }); + }); }); diff --git a/extensions/memory-wiki/src/query.ts b/extensions/memory-wiki/src/query.ts index 33727c37140..5e2aac4bb04 100644 --- a/extensions/memory-wiki/src/query.ts +++ b/extensions/memory-wiki/src/query.ts @@ -1,5 +1,9 @@ import fs from "node:fs/promises"; import path from "node:path"; +import { resolveDefaultAgentId } from "openclaw/plugin-sdk/config-runtime"; +import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-host-files"; +import { getActiveMemorySearchManager } from "openclaw/plugin-sdk/memory-host-search"; +import type { OpenClawConfig } from "../api.js"; import type { ResolvedMemoryWikiConfig } from "./config.js"; import { parseWikiMarkdown, toWikiPageSummary, type WikiPageSummary } from "./markdown.js"; import { initializeMemoryWikiVault } from "./vault.js"; @@ -7,18 +11,24 @@ import { initializeMemoryWikiVault } from "./vault.js"; const QUERY_DIRS = ["entities", "concepts", "sources", "syntheses", "reports"] as const; export type WikiSearchResult = { + corpus: "wiki" | "memory"; path: string; title: string; - kind: WikiPageSummary["kind"]; + kind: WikiPageSummary["kind"] | "memory"; score: number; snippet: string; id?: string; + startLine?: number; + endLine?: number; + citation?: string; + memorySource?: MemorySearchResult["source"]; }; export type WikiGetResult = { + corpus: "wiki" | "memory"; path: string; title: string; - kind: WikiPageSummary["kind"]; + kind: WikiPageSummary["kind"] | "memory"; content: string; fromLine: number; lineCount: number; @@ -113,6 +123,74 @@ function normalizeLookupKey(value: string): string { return normalized.endsWith(".md") ? normalized : normalized.replace(/\/+$/, ""); } +function buildLookupCandidates(lookup: string): string[] { + const normalized = normalizeLookupKey(lookup); + const withExtension = normalized.endsWith(".md") ? normalized : `${normalized}.md`; + return [...new Set([normalized, withExtension])]; +} + +function shouldSearchWiki(config: ResolvedMemoryWikiConfig): boolean { + return config.search.corpus === "wiki" || config.search.corpus === "all"; +} + +function shouldSearchSharedMemory( + config: ResolvedMemoryWikiConfig, + appConfig?: OpenClawConfig, +): boolean { + return ( + config.search.backend === "shared" && + appConfig !== undefined && + (config.search.corpus === "memory" || config.search.corpus === "all") + ); +} + +async function resolveActiveMemoryManager(appConfig?: OpenClawConfig) { + if (!appConfig) { + return null; + } + try { + const { manager } = await getActiveMemorySearchManager({ + cfg: appConfig, + agentId: resolveDefaultAgentId(appConfig), + }); + return manager; + } catch { + return null; + } +} + +function buildMemorySearchTitle(resultPath: string): string { + const basename = path.basename(resultPath, path.extname(resultPath)); + return basename.length > 0 ? basename : resultPath; +} + +function toWikiSearchResult(page: QueryableWikiPage, query: string): WikiSearchResult { + return { + corpus: "wiki", + path: page.relativePath, + title: page.title, + kind: page.kind, + score: scorePage(page, query), + snippet: buildSnippet(page.raw, query), + ...(page.id ? { id: page.id } : {}), + }; +} + +function toMemoryWikiSearchResult(result: MemorySearchResult): WikiSearchResult { + return { + corpus: "memory", + path: result.path, + title: buildMemorySearchTitle(result.path), + kind: "memory", + score: result.score, + snippet: result.snippet, + startLine: result.startLine, + endLine: result.endLine, + memorySource: result.source, + ...(result.citation ? { citation: result.citation } : {}), + }; +} + export function resolveQueryableWikiPageByLookup( pages: QueryableWikiPage[], lookup: string, @@ -131,22 +209,29 @@ export function resolveQueryableWikiPageByLookup( export async function searchMemoryWiki(params: { config: ResolvedMemoryWikiConfig; + appConfig?: OpenClawConfig; query: string; maxResults?: number; }): Promise { await initializeMemoryWikiVault(params.config); - const pages = await readQueryableWikiPages(params.config.vault.path); const maxResults = Math.max(1, params.maxResults ?? 10); - return pages - .map((page) => ({ - path: page.relativePath, - title: page.title, - kind: page.kind, - score: scorePage(page, params.query), - snippet: buildSnippet(page.raw, params.query), - ...(page.id ? { id: page.id } : {}), - })) - .filter((page) => page.score > 0) + + const wikiResults = shouldSearchWiki(params.config) + ? (await readQueryableWikiPages(params.config.vault.path)) + .map((page) => toWikiSearchResult(page, params.query)) + .filter((page) => page.score > 0) + : []; + + const sharedMemoryManager = shouldSearchSharedMemory(params.config, params.appConfig) + ? await resolveActiveMemoryManager(params.appConfig) + : null; + const memoryResults = sharedMemoryManager + ? (await sharedMemoryManager.search(params.query, { maxResults })).map((result) => + toMemoryWikiSearchResult(result), + ) + : []; + + return [...wikiResults, ...memoryResults] .toSorted((left, right) => { if (left.score !== right.score) { return right.score - left.score; @@ -158,30 +243,65 @@ export async function searchMemoryWiki(params: { export async function getMemoryWikiPage(params: { config: ResolvedMemoryWikiConfig; + appConfig?: OpenClawConfig; lookup: string; fromLine?: number; lineCount?: number; }): Promise { await initializeMemoryWikiVault(params.config); - const pages = await readQueryableWikiPages(params.config.vault.path); - const page = resolveQueryableWikiPageByLookup(pages, params.lookup); - if (!page) { + const fromLine = Math.max(1, params.fromLine ?? 1); + const lineCount = Math.max(1, params.lineCount ?? 200); + + if (shouldSearchWiki(params.config)) { + const pages = await readQueryableWikiPages(params.config.vault.path); + const page = resolveQueryableWikiPageByLookup(pages, params.lookup); + if (page) { + const parsed = parseWikiMarkdown(page.raw); + const lines = parsed.body.split(/\r?\n/); + const slice = lines.slice(fromLine - 1, fromLine - 1 + lineCount).join("\n"); + + return { + corpus: "wiki", + path: page.relativePath, + title: page.title, + kind: page.kind, + content: slice, + fromLine, + lineCount, + ...(page.id ? { id: page.id } : {}), + }; + } + } + + if (!shouldSearchSharedMemory(params.config, params.appConfig)) { return null; } - const parsed = parseWikiMarkdown(page.raw); - const lines = parsed.body.split(/\r?\n/); - const fromLine = Math.max(1, params.fromLine ?? 1); - const lineCount = Math.max(1, params.lineCount ?? 200); - const slice = lines.slice(fromLine - 1, fromLine - 1 + lineCount).join("\n"); + const manager = await resolveActiveMemoryManager(params.appConfig); + if (!manager) { + return null; + } - return { - path: page.relativePath, - title: page.title, - kind: page.kind, - content: slice, - fromLine, - lineCount, - ...(page.id ? { id: page.id } : {}), - }; + for (const relPath of buildLookupCandidates(params.lookup)) { + try { + const result = await manager.readFile({ + relPath, + from: fromLine, + lines: lineCount, + }); + return { + corpus: "memory", + path: result.path, + title: buildMemorySearchTitle(result.path), + kind: "memory", + content: result.text, + fromLine, + lineCount, + }; + } catch { + continue; + } + } + + return null; } diff --git a/extensions/memory-wiki/src/tool.ts b/extensions/memory-wiki/src/tool.ts index 3afcf812d52..8d3628eb860 100644 --- a/extensions/memory-wiki/src/tool.ts +++ b/extensions/memory-wiki/src/tool.ts @@ -74,23 +74,25 @@ export function createWikiSearchTool( return { name: "wiki_search", label: "Wiki Search", - description: "Search wiki pages by title, path, id, or body text.", + description: + "Search wiki pages and, when shared search is enabled, the active memory corpus by title, path, id, or body text.", parameters: WikiSearchSchema, execute: async (_toolCallId, rawParams) => { const params = rawParams as { query: string; maxResults?: number }; await syncImportedSourcesIfNeeded(config, appConfig); const results = await searchMemoryWiki({ config, + appConfig, query: params.query, maxResults: params.maxResults, }); const text = results.length === 0 - ? "No wiki results." + ? "No wiki or memory results." : results .map( (result, index) => - `${index + 1}. ${result.title} (${result.kind})\nPath: ${result.path}\nSnippet: ${result.snippet}`, + `${index + 1}. ${result.title} (${result.corpus}/${result.kind})\nPath: ${result.path}${typeof result.startLine === "number" && typeof result.endLine === "number" ? `\nLines: ${result.startLine}-${result.endLine}` : ""}\nSnippet: ${result.snippet}`, ) .join("\n\n"); return { @@ -176,13 +178,15 @@ export function createWikiGetTool( return { name: "wiki_get", label: "Wiki Get", - description: "Read a wiki page by id or relative path.", + description: + "Read a wiki page by id or relative path, or fall back to the active memory corpus when shared search is enabled.", parameters: WikiGetSchema, execute: async (_toolCallId, rawParams) => { const params = rawParams as { lookup: string; fromLine?: number; lineCount?: number }; await syncImportedSourcesIfNeeded(config, appConfig); const result = await getMemoryWikiPage({ config, + appConfig, lookup: params.lookup, fromLine: params.fromLine, lineCount: params.lineCount, diff --git a/package.json b/package.json index f7858117711..2f56e089488 100644 --- a/package.json +++ b/package.json @@ -715,6 +715,10 @@ "types": "./dist/plugin-sdk/memory-host-markdown.d.ts", "default": "./dist/plugin-sdk/memory-host-markdown.js" }, + "./plugin-sdk/memory-host-search": { + "types": "./dist/plugin-sdk/memory-host-search.d.ts", + "default": "./dist/plugin-sdk/memory-host-search.js" + }, "./plugin-sdk/memory-host-status": { "types": "./dist/plugin-sdk/memory-host-status.d.ts", "default": "./dist/plugin-sdk/memory-host-status.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index f10414347a8..1dc215fbb29 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -168,6 +168,7 @@ "memory-host-events", "memory-host-files", "memory-host-markdown", + "memory-host-search", "memory-host-status", "memory-lancedb", "msteams", diff --git a/src/memory-host-sdk/runtime-files.ts b/src/memory-host-sdk/runtime-files.ts index dd50c31eb46..02de9c2f4bf 100644 --- a/src/memory-host-sdk/runtime-files.ts +++ b/src/memory-host-sdk/runtime-files.ts @@ -3,4 +3,4 @@ export { listMemoryFiles, normalizeExtraMemoryPaths } from "./host/internal.js"; export { readAgentMemoryFile } from "./host/read-file.js"; export { resolveMemoryBackendConfig } from "./host/backend-config.js"; -export type { MemorySearchResult } from "./host/types.js"; +export type { MemorySearchManager, MemorySearchResult } from "./host/types.js"; diff --git a/src/plugin-sdk/memory-host-search.runtime.ts b/src/plugin-sdk/memory-host-search.runtime.ts new file mode 100644 index 00000000000..5fc023afce2 --- /dev/null +++ b/src/plugin-sdk/memory-host-search.runtime.ts @@ -0,0 +1,5 @@ +export { + closeActiveMemorySearchManagers, + getActiveMemorySearchManager, + resolveActiveMemoryBackendConfig, +} from "../plugins/memory-runtime.js"; diff --git a/src/plugin-sdk/memory-host-search.test.ts b/src/plugin-sdk/memory-host-search.test.ts new file mode 100644 index 00000000000..7a08c97fc7c --- /dev/null +++ b/src/plugin-sdk/memory-host-search.test.ts @@ -0,0 +1,42 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { + closeActiveMemorySearchManagers, + getActiveMemorySearchManager, +} from "./memory-host-search.js"; + +const { closeActiveMemorySearchManagersMock, getActiveMemorySearchManagerMock } = vi.hoisted( + () => ({ + closeActiveMemorySearchManagersMock: vi.fn(), + getActiveMemorySearchManagerMock: vi.fn(), + }), +); + +vi.mock("./memory-host-search.runtime.js", () => ({ + closeActiveMemorySearchManagers: closeActiveMemorySearchManagersMock, + getActiveMemorySearchManager: getActiveMemorySearchManagerMock, +})); + +describe("memory-host-search facade", () => { + beforeEach(() => { + closeActiveMemorySearchManagersMock.mockReset(); + getActiveMemorySearchManagerMock.mockReset(); + }); + + it("delegates active manager lookup to the lazy runtime module", async () => { + const cfg = { agents: { list: [{ id: "main", default: true }] } } as OpenClawConfig; + const expected = { manager: null, error: "unavailable" }; + getActiveMemorySearchManagerMock.mockResolvedValue(expected); + + await expect(getActiveMemorySearchManager({ cfg, agentId: "main" })).resolves.toEqual(expected); + expect(getActiveMemorySearchManagerMock).toHaveBeenCalledWith({ cfg, agentId: "main" }); + }); + + it("delegates runtime cleanup to the lazy runtime module", async () => { + const cfg = { agents: { list: [{ id: "main", default: true }] } } as OpenClawConfig; + + await closeActiveMemorySearchManagers(cfg); + + expect(closeActiveMemorySearchManagersMock).toHaveBeenCalledWith(cfg); + }); +}); diff --git a/src/plugin-sdk/memory-host-search.ts b/src/plugin-sdk/memory-host-search.ts new file mode 100644 index 00000000000..47549c56f4b --- /dev/null +++ b/src/plugin-sdk/memory-host-search.ts @@ -0,0 +1,29 @@ +import type { OpenClawConfig } from "../config/config.js"; +import type { RegisteredMemorySearchManager } from "../plugins/memory-state.js"; + +type ActiveMemorySearchPurpose = "default" | "status"; + +export type ActiveMemorySearchManagerResult = { + manager: RegisteredMemorySearchManager | null; + error?: string; +}; + +type MemoryHostSearchRuntimeModule = typeof import("./memory-host-search.runtime.js"); + +async function loadMemoryHostSearchRuntime(): Promise { + return await import("./memory-host-search.runtime.js"); +} + +export async function getActiveMemorySearchManager(params: { + cfg: OpenClawConfig; + agentId: string; + purpose?: ActiveMemorySearchPurpose; +}): Promise { + const runtime = await loadMemoryHostSearchRuntime(); + return await runtime.getActiveMemorySearchManager(params); +} + +export async function closeActiveMemorySearchManagers(cfg?: OpenClawConfig): Promise { + const runtime = await loadMemoryHostSearchRuntime(); + await runtime.closeActiveMemorySearchManagers(cfg); +} diff --git a/src/plugins/memory-state.ts b/src/plugins/memory-state.ts index efc51f85227..ff30f11120a 100644 --- a/src/plugins/memory-state.ts +++ b/src/plugins/memory-state.ts @@ -1,10 +1,6 @@ import type { OpenClawConfig } from "../config/config.js"; import type { MemoryCitationsMode } from "../config/types.memory.js"; -import type { - MemoryEmbeddingProbeResult, - MemoryProviderStatus, - MemorySyncProgressUpdate, -} from "../memory-host-sdk/engine-storage.js"; +import type { MemorySearchManager } from "../memory-host-sdk/runtime-files.js"; export type MemoryPromptSectionBuilder = (params: { availableTools: Set; @@ -25,18 +21,7 @@ export type MemoryFlushPlanResolver = (params: { nowMs?: number; }) => MemoryFlushPlan | null; -export type RegisteredMemorySearchManager = { - status(): MemoryProviderStatus; - probeEmbeddingAvailability(): Promise; - probeVectorAvailability(): Promise; - sync?(params?: { - reason?: string; - force?: boolean; - sessionFiles?: string[]; - progress?: (update: MemorySyncProgressUpdate) => void; - }): Promise; - close?(): Promise; -}; +export type RegisteredMemorySearchManager = MemorySearchManager; export type MemoryRuntimeQmdConfig = { command?: string;