refactor(FN-2126): align memory handling with canonical .fusion/memory paths
- Update memory docs and contracts to reference canonical .fusion/memory files while treating the legacy top-level memory file as compatibility-only - Tighten memory backend path normalization messaging and map stale qmd legacy top-level memory results back to .fusion/memory/MEMORY.md - Remove legacy memory read/write fallback branches from backend initialization paths and rely on ensureOpenClawMemoryFiles() migration behavior - Preserve migration-seeded legacy content during ensureMemoryFile() bootstrap and add regression coverage for seeded long-term memory creation
This commit is contained in:
@@ -654,7 +654,7 @@ function normalizeMemoryRequestPath(rawPath: string): string {
|
||||
}
|
||||
throw new MemoryBackendError(
|
||||
"UNSUPPORTED",
|
||||
`Memory path '${rawPath}' is outside allowed files: MEMORY.md, DREAMS.md, memory/YYYY-MM-DD.md`,
|
||||
`Memory path '${rawPath}' is outside allowed files: .fusion/memory/MEMORY.md, .fusion/memory/DREAMS.md, .fusion/memory/YYYY-MM-DD.md`,
|
||||
"memory",
|
||||
);
|
||||
}
|
||||
@@ -789,12 +789,14 @@ function normalizeQmdSearchResultPath(rootDir: string, rawPath: unknown): string
|
||||
}
|
||||
|
||||
const lowerCandidate = candidate.toLowerCase();
|
||||
const legacyLower = LEGACY_MEMORY_FILE_PATH.toLowerCase();
|
||||
if (lowerCandidate === legacyLower || lowerCandidate.endsWith(`/${legacyLower}`)) {
|
||||
return LEGACY_MEMORY_FILE_PATH;
|
||||
}
|
||||
|
||||
const normalizedBaseName = basename(candidate).toLowerCase();
|
||||
const normalizedDirName = dirname(lowerCandidate).replace(/\\/g, "/");
|
||||
|
||||
// Map legacy top-level memory paths from stale qmd indexes to the canonical
|
||||
// layered long-term path without exposing legacy paths to callers.
|
||||
if (normalizedBaseName === "memory.md" && (normalizedDirName === ".fusion" || normalizedDirName.endsWith("/.fusion"))) {
|
||||
return `${MEMORY_WORKSPACE_PATH}/${MEMORY_LONG_TERM_FILENAME}`;
|
||||
}
|
||||
if (normalizedBaseName === MEMORY_LONG_TERM_FILENAME.toLowerCase()) {
|
||||
return `${MEMORY_WORKSPACE_PATH}/${MEMORY_LONG_TERM_FILENAME}`;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,19 @@ describe("project-memory", () => {
|
||||
expect(content).toBe(getDefaultMemoryScaffold());
|
||||
});
|
||||
|
||||
it("preserves migration-seeded legacy content when long-term memory is created", async () => {
|
||||
await mkdir(join(testDir, ".fusion"), { recursive: true });
|
||||
const legacyContent = "# Legacy Memory\n\nPreserve me";
|
||||
await writeFile(join(testDir, LEGACY_MEMORY_FILE_PATH), legacyContent, "utf-8");
|
||||
|
||||
const created = await ensureMemoryFile(testDir);
|
||||
|
||||
expect(created).toBe(true);
|
||||
expect(existsSync(memoryFilePath(testDir))).toBe(true);
|
||||
const content = await readProjectMemory(testDir);
|
||||
expect(content).toBe(legacyContent);
|
||||
});
|
||||
|
||||
it("creates the .fusion directory if missing", async () => {
|
||||
expect(existsSync(join(testDir, ".fusion"))).toBe(false);
|
||||
await ensureMemoryFile(testDir);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
ensureOpenClawMemoryFiles,
|
||||
getDefaultLongTermMemoryScaffold,
|
||||
memoryLongTermPath,
|
||||
type MemorySearchOptions,
|
||||
type MemorySearchResult,
|
||||
@@ -88,12 +89,15 @@ export function getDefaultMemoryScaffold(): string {
|
||||
*/
|
||||
export async function ensureMemoryFile(rootDir: string): Promise<boolean> {
|
||||
const filePath = memoryFilePath(rootDir);
|
||||
const legacyPath = join(rootDir, ".fusion", "memory.md");
|
||||
const hasLegacySeed = existsSync(legacyPath);
|
||||
|
||||
const { longTermCreated } = await ensureOpenClawMemoryFiles(rootDir);
|
||||
if (longTermCreated && !hasLegacySeed) {
|
||||
await writeFile(filePath, getDefaultMemoryScaffold(), "utf-8");
|
||||
|
||||
// Ensure direct bootstrap uses the historical scaffold expected by this module.
|
||||
// If migration seeded from an older legacy file, preserve that seeded content.
|
||||
if (longTermCreated) {
|
||||
const createdContent = await readFile(filePath, "utf-8");
|
||||
if (createdContent === getDefaultLongTermMemoryScaffold()) {
|
||||
await writeFile(filePath, getDefaultMemoryScaffold(), "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
return longTermCreated;
|
||||
@@ -272,15 +276,6 @@ export async function ensureMemoryFileWithBackend(
|
||||
if (backend.exists) {
|
||||
const exists = await backend.exists(rootDir);
|
||||
if (exists) {
|
||||
if (backend.capabilities.writable) {
|
||||
await ensureOpenClawMemoryFiles(rootDir);
|
||||
if (!existsSync(memoryFilePath(rootDir))) {
|
||||
const existingContent = await readProjectMemory(rootDir);
|
||||
if (existingContent) {
|
||||
await backend.write(rootDir, existingContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshQmdIfNeeded();
|
||||
return false; // Memory already exists, don't overwrite
|
||||
}
|
||||
@@ -288,15 +283,6 @@ export async function ensureMemoryFileWithBackend(
|
||||
// Fall back to direct file check
|
||||
const filePath = memoryFilePath(rootDir);
|
||||
if (existsSync(filePath)) {
|
||||
if (backend.capabilities.writable) {
|
||||
await ensureOpenClawMemoryFiles(rootDir);
|
||||
if (!existsSync(memoryFilePath(rootDir))) {
|
||||
const existingContent = await readProjectMemory(rootDir);
|
||||
if (existingContent) {
|
||||
await backend.write(rootDir, existingContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshQmdIfNeeded();
|
||||
return false; // Memory already exists, don't overwrite
|
||||
}
|
||||
@@ -309,8 +295,8 @@ export async function ensureMemoryFileWithBackend(
|
||||
}
|
||||
|
||||
// OpenClaw-style memory layers are always bootstrapped for writable memory
|
||||
// backends. The legacy `.fusion/memory.md` file remains as a compatibility
|
||||
// source, but new writes go to `.fusion/memory/MEMORY.md`.
|
||||
// backends. `ensureOpenClawMemoryFiles()` handles one-way migration seeding
|
||||
// from the legacy top-level memory file when upgrading older projects.
|
||||
if (backend.capabilities.writable) {
|
||||
await ensureOpenClawMemoryFiles(rootDir);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user