feat(FN-3207): remove runtime memory-backend side-load

Merged FN-3207 and FN-3242: streamlined the memory backend by removing the runtime side-load pattern in `project-memory.ts` (simplified from 25+ lines), added regression tests across core, engine, and CLI bundle to catch the import issue at build time, and updated ChatView with a CSS fix for file me

Fusion-Task-Id: FN-3207
This commit is contained in:
Fusion
2026-05-03 07:18:52 -07:00
committed by gsxdsm
parent fa16b953c0
commit a82c3dcac7
5 changed files with 86 additions and 21 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Fix project memory tools failing in fresh worktrees and bundled runtime contexts when an internal memory backend artifact is missing. `fn_memory_search` and `fn_memory_get` now resolve the backend through bundled runtime code instead of a fragile side-load import path.

View File

@@ -42,6 +42,12 @@ describe("CLI bundle output", () => {
expect(content).not.toContain('"@fusion/engine"'); expect(content).not.toContain('"@fusion/engine"');
}); });
it("does not contain runtime memory-backend side-load imports", () => {
const content = readFileSync(bundlePath, "utf-8");
expect(content).not.toMatch(/await\s+import\(\s*["']\.\/memory-backend\.js["']\s*\)/);
expect(content).not.toMatch(/await\s+import\(\s*["']\.\.\/memory-backend\.js["']\s*\)/);
});
it("contains inlined workspace code", () => { it("contains inlined workspace code", () => {
const content = readFileSync(bundlePath, "utf-8"); const content = readFileSync(bundlePath, "utf-8");
// TaskStore from @fusion/core // TaskStore from @fusion/core

View File

@@ -13,6 +13,7 @@ import {
readProjectMemory, readProjectMemory,
readProjectMemoryWithBackend, readProjectMemoryWithBackend,
searchProjectMemory, searchProjectMemory,
getProjectMemory,
resolveMemoryInstructionContext, resolveMemoryInstructionContext,
} from "../project-memory.js"; } from "../project-memory.js";
@@ -649,4 +650,45 @@ describe("project-memory", () => {
expect(results.some((result) => result.path === ".fusion/memory/DREAMS.md")).toBe(true); expect(results.some((result) => result.path === ".fusion/memory/DREAMS.md")).toBe(true);
}); });
}); });
describe("getProjectMemory", () => {
it("reads bounded memory window via file backend", async () => {
const memoryDir = join(testDir, ".fusion", "memory");
await mkdir(memoryDir, { recursive: true });
await writeFile(
join(memoryDir, "MEMORY.md"),
"# Memory\nline-a\nline-b\nline-c\nline-d\n",
"utf-8",
);
const result = await getProjectMemory(
testDir,
{ path: ".fusion/memory/MEMORY.md", startLine: 2, lineCount: 2 },
{ memoryBackendType: "file" },
);
expect(result.path).toBe(".fusion/memory/MEMORY.md");
expect(result.content).toBe("line-a\nline-b");
expect(result.startLine).toBe(2);
expect(result.endLine).toBe(3);
expect(result.totalLines).toBeGreaterThanOrEqual(5);
expect(result.backend).toBe("file");
});
it("returns qmd backend marker for memory_get contract", async () => {
const memoryDir = join(testDir, ".fusion", "memory");
await mkdir(memoryDir, { recursive: true });
await writeFile(join(memoryDir, "MEMORY.md"), "# Memory\nqmd-line\n", "utf-8");
const result = await getProjectMemory(
testDir,
{ path: ".fusion/memory/MEMORY.md", startLine: 1, lineCount: 5 },
{ memoryBackendType: "qmd" },
);
expect(result.path).toBe(".fusion/memory/MEMORY.md");
expect(result.content).toContain("qmd-line");
expect(result.backend).toBe("qmd");
});
});
}); });

View File

@@ -24,6 +24,10 @@ import { existsSync } from "node:fs";
import { import {
ensureOpenClawMemoryFiles, ensureOpenClawMemoryFiles,
memoryLongTermPath, memoryLongTermPath,
resolveMemoryBackend,
MEMORY_BACKEND_SETTINGS_KEYS,
DEFAULT_MEMORY_BACKEND,
scheduleQmdInstallAndRefresh,
type MemorySearchOptions, type MemorySearchOptions,
type MemorySearchResult, type MemorySearchResult,
type MemoryGetOptions, type MemoryGetOptions,
@@ -93,17 +97,6 @@ type MemorySettings = {
[key: string]: unknown; [key: string]: unknown;
}; };
// Import memory backend utilities lazily to avoid circular dependencies
async function getMemoryBackendUtils() {
const module = await import("./memory-backend.js");
return {
resolveMemoryBackend: module.resolveMemoryBackend,
getMemoryBackendCapabilities: module.getMemoryBackendCapabilities,
MEMORY_BACKEND_SETTINGS_KEYS: module.MEMORY_BACKEND_SETTINGS_KEYS,
DEFAULT_MEMORY_BACKEND: module.DEFAULT_MEMORY_BACKEND,
scheduleQmdInstallAndRefresh: module.scheduleQmdInstallAndRefresh,
};
}
// ── Memory Instruction Context ───────────────────────────────────────── // ── Memory Instruction Context ─────────────────────────────────────────
@@ -236,13 +229,6 @@ export async function ensureMemoryFileWithBackend(
rootDir: string, rootDir: string,
settings?: MemorySettings, settings?: MemorySettings,
): Promise<boolean> { ): Promise<boolean> {
const {
resolveMemoryBackend,
MEMORY_BACKEND_SETTINGS_KEYS,
DEFAULT_MEMORY_BACKEND,
scheduleQmdInstallAndRefresh,
} = await getMemoryBackendUtils();
const backendType = const backendType =
(settings?.[MEMORY_BACKEND_SETTINGS_KEYS.MEMORY_BACKEND_TYPE] as string) || (settings?.[MEMORY_BACKEND_SETTINGS_KEYS.MEMORY_BACKEND_TYPE] as string) ||
DEFAULT_MEMORY_BACKEND; DEFAULT_MEMORY_BACKEND;
@@ -296,7 +282,6 @@ export async function readProjectMemoryWithBackend(
rootDir: string, rootDir: string,
settings?: MemorySettings, settings?: MemorySettings,
): Promise<string> { ): Promise<string> {
const { resolveMemoryBackend } = await getMemoryBackendUtils();
const backend = resolveMemoryBackend(settings); const backend = resolveMemoryBackend(settings);
try { try {
@@ -313,7 +298,6 @@ export async function searchProjectMemory(
options: MemorySearchOptions, options: MemorySearchOptions,
settings?: MemorySettings, settings?: MemorySettings,
): Promise<MemorySearchResult[]> { ): Promise<MemorySearchResult[]> {
const { resolveMemoryBackend } = await getMemoryBackendUtils();
const backend = resolveMemoryBackend(settings); const backend = resolveMemoryBackend(settings);
if (!backend.search) { if (!backend.search) {
return []; return [];
@@ -326,7 +310,6 @@ export async function getProjectMemory(
options: MemoryGetOptions, options: MemoryGetOptions,
settings?: MemorySettings, settings?: MemorySettings,
): Promise<MemoryGetResult> { ): Promise<MemoryGetResult> {
const { resolveMemoryBackend } = await getMemoryBackendUtils();
const backend = resolveMemoryBackend(settings); const backend = resolveMemoryBackend(settings);
if (!backend.get) { if (!backend.get) {
throw new Error(`Memory backend '${backend.type}' does not support memory_get`); throw new Error(`Memory backend '${backend.type}' does not support memory_get`);

View File

@@ -271,6 +271,35 @@ describe("createMemoryTools", () => {
expect(getResult.content[0]!.text).toContain("roadmap sequencing"); expect(getResult.content[0]!.text).toContain("roadmap sequencing");
}); });
it("uses project memory backend for fn_memory_search/fn_memory_get when agent memory is absent", async () => {
await core.ensureOpenClawMemoryFiles(tempDir);
await appendFile(
join(tempDir, ".fusion", "memory", "MEMORY.md"),
"\n- Runtime import regressions should be caught in bundle tests.\n",
"utf-8",
);
const [searchTool, getTool] = createMemoryTools(tempDir, { memoryBackendType: "file" });
const searchResult = await (searchTool as any).execute("call-project-search", {
query: "runtime import regressions",
limit: 5,
}, undefined, undefined, undefined);
expect(searchResult.details.results.length).toBeGreaterThan(0);
expect(searchResult.details.results.some((hit: any) => hit.path === ".fusion/memory/MEMORY.md")).toBe(true);
const getResult = await (getTool as any).execute("call-project-get", {
path: ".fusion/memory/MEMORY.md",
startLine: 1,
lineCount: 30,
}, undefined, undefined, undefined);
expect(getResult.content[0]!.text).toContain(".fusion/memory/MEMORY.md");
expect(getResult.content[0]!.text).toContain("Runtime import regressions");
expect(getResult.details.backend).toBe("file");
});
it("creates daily and dreams files for per-agent memory lookup", async () => { it("creates daily and dreams files for per-agent memory lookup", async () => {
const [searchTool] = createMemoryTools(tempDir, { memoryBackendType: "file" }, { const [searchTool] = createMemoryTools(tempDir, { memoryBackendType: "file" }, {
agentMemory: { agentMemory: {