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:
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user