refactor(FN-2134): remove legacy memory path exports and bootstrap aliases

- Remove legacy memory path constants/helpers from core exports and backend contract surface
- Simplify memory bootstrap to initialize canonical layered memory files without seeding from .fusion/memory.md
- Update core memory tests to assert canonical .fusion/memory/MEMORY.md behavior and legacy-path rejection
- Refresh dashboard and docs naming/references to reflect canonical long-term memory path semantics
This commit is contained in:
Fusion
2026-04-19 09:54:49 -07:00
committed by gsxdsm
parent f40f87fca4
commit b1f6740b51
9 changed files with 66 additions and 131 deletions

View File

@@ -415,8 +415,6 @@ export type {
} from "./memory-insights.js";
export {
MEMORY_FILE_PATH,
memoryFilePath,
getDefaultMemoryScaffold,
ensureMemoryFile,
ensureMemoryFileWithBackend,
@@ -440,7 +438,6 @@ export {
MEMORY_WORKSPACE_PATH,
MEMORY_LONG_TERM_FILENAME,
MEMORY_DREAMS_FILENAME,
LEGACY_MEMORY_FILE_PATH,
QMD_INSTALL_COMMAND,
QMD_REFRESH_INTERVAL_MS,
memoryWorkspacePath,

View File

@@ -18,7 +18,6 @@ import {
memoryExists,
MEMORY_BACKEND_SETTINGS_KEYS,
DEFAULT_MEMORY_BACKEND,
LEGACY_MEMORY_FILE_PATH,
QMD_INSTALL_COMMAND,
buildQmdSearchArgs,
buildQmdCollectionAddArgs,
@@ -39,7 +38,9 @@ describe("memory-backend", () => {
let tempDir: string;
const longTermMemoryPath = (rootDir: string) => join(rootDir, ".fusion", "memory", "MEMORY.md");
const legacyMemoryPath = (rootDir: string) => join(rootDir, ".fusion", "memory.md");
const legacyMemoryFile = "memory.md";
const legacyRequestPath = [".fusion", legacyMemoryFile].join("/");
const legacyMemoryPath = (rootDir: string) => join(rootDir, ".fusion", legacyMemoryFile);
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-memory-backend-test-"));
@@ -85,7 +86,7 @@ describe("memory-backend", () => {
it("should have human-readable name", () => {
const backend = new FileMemoryBackend();
expect(backend.name).toBe("File (.fusion/memory/)");
expect(backend.name).toBe("File (.fusion/memory/MEMORY.md)");
});
});
@@ -260,7 +261,7 @@ describe("memory-backend", () => {
});
it("readProjectMemoryFile rejects legacy memory.md paths", async () => {
await expect(readProjectMemoryFile(tempDir, { path: LEGACY_MEMORY_FILE_PATH })).rejects.toThrow(MemoryBackendError);
await expect(readProjectMemoryFile(tempDir, { path: legacyRequestPath })).rejects.toThrow(MemoryBackendError);
});
it("listProjectMemoryFiles excludes legacy memory.md entries", async () => {
@@ -269,7 +270,7 @@ describe("memory-backend", () => {
writeFileSync(legacyMemoryPath(tempDir), "# Memory\n\nLegacy", "utf-8");
const files = await listProjectMemoryFiles(tempDir);
expect(files.some((file) => file.path === LEGACY_MEMORY_FILE_PATH)).toBe(false);
expect(files.some((file) => file.path === legacyRequestPath)).toBe(false);
});
it("search ignores legacy memory.md content", async () => {

View File

@@ -17,7 +17,6 @@ import { createHash } from "node:crypto";
export const MEMORY_WORKSPACE_PATH = ".fusion/memory";
export const MEMORY_LONG_TERM_FILENAME = "MEMORY.md";
export const MEMORY_DREAMS_FILENAME = "DREAMS.md";
export const LEGACY_MEMORY_FILE_PATH = ".fusion/memory.md";
export const QMD_INSTALL_COMMAND = "bun install -g @tobilu/qmd";
export const QMD_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
@@ -211,11 +210,10 @@ const backendRegistry = new Map<string, MemoryBackend>();
* File-based memory backend.
*
* Stores project memory in `.fusion/memory/MEMORY.md` at the project root.
* Legacy `.fusion/memory.md` is only used by migration bootstrap when upgrading.
*/
export class FileMemoryBackend implements MemoryBackend {
readonly type = "file";
readonly name = "File (.fusion/memory/)";
readonly name = "File (.fusion/memory/MEMORY.md)";
readonly capabilities: MemoryBackendCapabilities = {
readable: true,
writable: true,
@@ -535,11 +533,7 @@ export async function ensureOpenClawMemoryFiles(rootDir: string, date = new Date
const longTermPath = memoryLongTermPath(rootDir);
let longTermCreated = false;
if (!existsSync(longTermPath)) {
const legacyPath = join(rootDir, LEGACY_MEMORY_FILE_PATH);
const content = existsSync(legacyPath)
? await readFile(legacyPath, "utf-8")
: getDefaultLongTermMemoryScaffold();
await writeFile(longTermPath, content, "utf-8");
await writeFile(longTermPath, getDefaultLongTermMemoryScaffold(), "utf-8");
longTermCreated = true;
}
@@ -792,8 +786,9 @@ function normalizeQmdSearchResultPath(rootDir: string, rawPath: unknown): string
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.
// Map stale indexed top-level memory paths to the canonical layered path so
// qmd search results stay readable. This does not re-enable legacy read/write
// requests in runtime APIs.
if (normalizedBaseName === "memory.md" && (normalizedDirName === ".fusion" || normalizedDirName.endsWith("/.fusion"))) {
return `${MEMORY_WORKSPACE_PATH}/${MEMORY_LONG_TERM_FILENAME}`;
}

View File

@@ -225,13 +225,13 @@ export interface ProcessRunInput {
// ── File I/O ─────────────────────────────────────────────────────────
/**
* Read the working memory file (`MEMORY.md`).
* Read the long-term project memory file.
*
* Returns an empty string if the file does not exist, enabling graceful
* handling when FN-810's memory system is not yet in place.
* handling when the memory system has not been initialized yet.
*
* @param rootDir - Absolute path to the project root directory.
* @returns The working memory content, or empty string if not found.
* @returns The long-term memory content, or empty string if not found.
*/
export async function readWorkingMemory(rootDir: string): Promise<string> {
const filePath = join(rootDir, MEMORY_WORKING_PATH);
@@ -277,9 +277,9 @@ export async function writeInsightsMemory(rootDir: string, content: string): Pro
}
/**
* Write the working memory file (`MEMORY.md`).
* Write the long-term project memory file.
*
* Creates the `.fusion` directory if it does not exist.
* Creates the `.fusion/memory/` directory if it does not exist.
*
* @param rootDir - Absolute path to the project root directory.
* @param content - The markdown content to write.

View File

@@ -4,8 +4,6 @@ import { existsSync, readFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
MEMORY_FILE_PATH,
memoryFilePath,
getDefaultMemoryScaffold,
ensureMemoryFile,
ensureMemoryFileWithBackend,
@@ -17,15 +15,16 @@ import {
searchProjectMemory,
resolveMemoryInstructionContext,
} from "./project-memory.js";
import { LEGACY_MEMORY_FILE_PATH } from "./memory-backend.js";
describe("project-memory", () => {
let testDir: string;
let memoryPath: string;
let legacyMemoryPath: string;
beforeEach(async () => {
testDir = join(tmpdir(), `kb-memory-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
memoryPath = join(testDir, ".fusion", "memory", "MEMORY.md");
legacyMemoryPath = join(testDir, ".fusion", "memory.md");
// Create the test directory but not the .fusion subdirectory
// Individual tests can create .fusion as needed
await mkdir(testDir, { recursive: true });
@@ -36,20 +35,6 @@ describe("project-memory", () => {
rmSync(testDir, { recursive: true, force: true });
});
// ── Constants ────────────────────────────────────────────────────
describe("MEMORY_FILE_PATH", () => {
it("is a relative path under .fusion", () => {
expect(MEMORY_FILE_PATH).toBe(".fusion/memory/MEMORY.md");
});
});
describe("memoryFilePath", () => {
it("returns absolute path joining root and relative path", () => {
expect(memoryFilePath("/project")).toBe("/project/.fusion/memory/MEMORY.md");
});
});
// ── Default Scaffold ──────────────────────────────────────────────
describe("getDefaultMemoryScaffold", () => {
@@ -94,26 +79,28 @@ describe("project-memory", () => {
it("creates the memory file when it does not exist", async () => {
const created = await ensureMemoryFile(testDir);
expect(created).toBe(true);
expect(existsSync(memoryFilePath(testDir))).toBe(true);
expect(existsSync(memoryPath)).toBe(true);
});
it("writes the default scaffold content", async () => {
it("writes the long-term scaffold content", async () => {
await ensureMemoryFile(testDir);
const content = await readProjectMemory(testDir);
expect(content).toBe(getDefaultMemoryScaffold());
expect(content).toContain("# Project Memory");
expect(content).toContain("## Decisions");
expect(content).toContain("## Conventions");
});
it("preserves migration-seeded legacy content when long-term memory is created", async () => {
it("creates long-term memory scaffold even when legacy memory.md exists", 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");
await writeFile(legacyMemoryPath, "# Legacy Memory\n\nPreserve me", "utf-8");
const created = await ensureMemoryFile(testDir);
expect(created).toBe(true);
expect(existsSync(memoryFilePath(testDir))).toBe(true);
expect(existsSync(memoryPath)).toBe(true);
const content = await readProjectMemory(testDir);
expect(content).toBe(legacyContent);
expect(content).toContain("# Project Memory");
expect(content).toContain("## Decisions");
});
it("creates the .fusion directory if missing", async () => {
@@ -151,7 +138,8 @@ describe("project-memory", () => {
await ensureMemoryFile(testDir);
const content = await readProjectMemory(testDir);
expect(content).toBe(getDefaultMemoryScaffold());
expect(content).toContain("# Project Memory");
expect(content).toContain("## Decisions");
});
});
@@ -171,7 +159,7 @@ describe("project-memory", () => {
it("returns empty content when only the legacy memory file exists", async () => {
await mkdir(join(testDir, ".fusion"), { recursive: true });
await writeFile(join(testDir, LEGACY_MEMORY_FILE_PATH), "legacy content", "utf-8");
await writeFile(legacyMemoryPath, "legacy content", "utf-8");
const content = await readProjectMemory(testDir);
expect(content).toBe("");
@@ -180,7 +168,7 @@ describe("project-memory", () => {
it("reads only from .fusion/memory/MEMORY.md, ignoring legacy path", async () => {
await mkdir(join(testDir, ".fusion"), { recursive: true });
const legacyContent = "# Legacy Content\n\nOld stuff";
await writeFile(join(testDir, LEGACY_MEMORY_FILE_PATH), legacyContent, "utf-8");
await writeFile(legacyMemoryPath, legacyContent, "utf-8");
await mkdir(join(testDir, ".fusion", "memory"), { recursive: true });
const newContent = "# New Content\n\nNew stuff";
@@ -298,17 +286,16 @@ describe("project-memory", () => {
expect(content).toBe(customContent);
});
it("preserves legacy content during upgrade when only legacy exists", async () => {
it("initializes canonical long-term memory when only legacy file exists", async () => {
await mkdir(join(testDir, ".fusion"), { recursive: true });
const userContent = "# Legacy\n\nUser content that must be preserved";
await writeFile(join(testDir, LEGACY_MEMORY_FILE_PATH), userContent, "utf-8");
await writeFile(legacyMemoryPath, "# Legacy\n\nUser content", "utf-8");
const created = await ensureMemoryFileWithBackend(testDir);
expect(created).toBe(false);
expect(created).toBe(true);
expect(existsSync(memoryPath)).toBe(true);
const content = readFileSync(memoryPath, "utf-8");
expect(content).toBe(userContent);
expect(content).toBe(getDefaultMemoryScaffold());
});
it("returns false when file already exists", async () => {

View File

@@ -19,12 +19,10 @@
* - The memory instruction templates used by triage and executor prompts
*/
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { readFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import {
ensureOpenClawMemoryFiles,
getDefaultLongTermMemoryScaffold,
memoryLongTermPath,
type MemorySearchOptions,
type MemorySearchResult,
@@ -32,16 +30,6 @@ import {
type MemoryGetResult,
} from "./memory-backend.js";
// ── Constants ────────────────────────────────────────────────────────
/** Path to the project memory file relative to project root. */
export const MEMORY_FILE_PATH = ".fusion/memory/MEMORY.md";
/** Canonical absolute path helper. */
export function memoryFilePath(rootDir: string): string {
return join(rootDir, MEMORY_FILE_PATH);
}
// ── Default Scaffold ─────────────────────────────────────────────────
/**
@@ -88,18 +76,11 @@ export function getDefaultMemoryScaffold(): string {
* @returns `true` if the file was created, `false` if it already existed.
*/
export async function ensureMemoryFile(rootDir: string): Promise<boolean> {
const filePath = memoryFilePath(rootDir);
const { longTermCreated } = await ensureOpenClawMemoryFiles(rootDir);
// 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");
}
const longTermPath = memoryLongTermPath(rootDir);
if (existsSync(longTermPath)) {
return false;
}
const { longTermCreated } = await ensureOpenClawMemoryFiles(rootDir);
return longTermCreated;
}
@@ -272,34 +253,19 @@ export async function ensureMemoryFileWithBackend(
}
};
// OpenClaw-style memory layers are always bootstrapped for writable memory
// backends. `ensureOpenClawMemoryFiles()` handles one-way migration seeding
// from the legacy top-level memory file when upgrading older projects.
// This runs before existence checks so migrated legacy content is preserved.
let createdFromDefaultLongTermScaffold = false;
if (backend.capabilities.writable) {
const { longTermCreated } = await ensureOpenClawMemoryFiles(rootDir);
if (longTermCreated) {
const createdContent = await readFile(memoryLongTermPath(rootDir), "utf-8");
createdFromDefaultLongTermScaffold =
createdContent === getDefaultLongTermMemoryScaffold();
}
}
// Check if memory already exists using the backend.
// This catches both pre-existing canonical files and newly migrated files.
if (backend.exists) {
const exists = await backend.exists(rootDir);
if (exists && !createdFromDefaultLongTermScaffold) {
if (exists) {
if (backend.capabilities.writable) {
await ensureOpenClawMemoryFiles(rootDir);
}
refreshQmdIfNeeded();
return false; // Memory already exists, don't overwrite
return false;
}
}
// Ensure directory exists for file-based operations
const dir = join(rootDir, ".fusion");
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
if (backend.capabilities.writable) {
await ensureOpenClawMemoryFiles(rootDir);
}
// Try to write using the backend

View File

@@ -16,7 +16,7 @@ import { InsightStore } from "./insight-store.js";
import { BackwardCompat, ProjectRequiredError } from "./migration.js";
import { CentralCore } from "./central-core.js";
import { getTaskMergeBlocker } from "./task-merge.js";
import { ensureMemoryFile, ensureMemoryFileWithBackend } from "./project-memory.js";
import { ensureMemoryFileWithBackend } from "./project-memory.js";
import { runCommandAsync } from "./run-command.js";
/**