feat(FN-2087): finalize canonical memory path migration

- Remove legacy .fusion/memory.md fallback references and normalize prompts/docs to .fusion/memory/MEMORY.md
- Stop legacy mirror writes and fallback reads in core memory backend and project memory flows
- Update engine worktree boundary checks and tests for canonical memory file handling
- Align dashboard memory/settings surfaces and route tests with canonical memory behavior
- Add model-favorites persistence test coverage for mission interview and new agent dialogs
This commit is contained in:
Fusion
2026-04-18 22:47:17 -07:00
committed by gsxdsm
parent 63ecb21c39
commit d642af311d
32 changed files with 503 additions and 307 deletions

View File

@@ -38,7 +38,7 @@ describe("test-project fixture", () => {
expect(existsSync(join(fixture.rootDir, ".fusion", "fusion.db"))).toBe(true);
expect(existsSync(join(fixture.rootDir, ".fusion", "config.json"))).toBe(true);
expect(existsSync(join(fixture.rootDir, ".fusion", "tasks"))).toBe(true);
expect(existsSync(join(fixture.rootDir, ".fusion", "memory.md"))).toBe(true);
expect(existsSync(join(fixture.rootDir, ".fusion", "memory", "MEMORY.md"))).toBe(true);
const configRaw = await readFile(join(fixture.rootDir, ".fusion", "config.json"), "utf-8");
const config = JSON.parse(configRaw);

View File

@@ -154,7 +154,7 @@ describe("resolveAgentPrompt", () => {
it("built-in executor prompt mentions memory exception", () => {
const result = resolveAgentPrompt("executor");
expect(result).toContain(".fusion/memory/");
expect(result).toContain(".fusion/memory/MEMORY.md");
});
it("built-in executor prompt mentions attachments exception", () => {
@@ -183,7 +183,7 @@ describe("resolveAgentPrompt", () => {
};
const result = resolveAgentPrompt("executor", config);
expect(result).toContain(".fusion/memory/");
expect(result).toContain(".fusion/memory/MEMORY.md");
});
it("senior-engineer prompt mentions attachments exception", () => {

View File

@@ -108,7 +108,7 @@ model, read-only access) to independently assess your work.
You are running in an **isolated git worktree**. This means:
- **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree — the worktree is your isolated execution environment.
- **Exception — Project memory:** You MAY read and write to files under .fusion/memory/ at the project root (MEMORY.md for durable learnings, YYYY-MM-DD.md for daily notes) to save durable project learnings.
- **Exception — Project memory:** You MAY read and write to .fusion/memory/MEMORY.md at the project root to save durable project learnings (architecture patterns, conventions, pitfalls).
- **Exception — Task attachments:** You MAY read files under .fusion/tasks/{taskId}/attachments/ at the project root for context screenshots and documents attached to this task.
- **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree.
@@ -519,7 +519,7 @@ model, read-only access) to independently assess your work.
You are running in an **isolated git worktree**. This means:
- **All code changes must be made inside the current worktree directory.** Do not modify files outside the worktree — the worktree is your isolated execution environment.
- **Exception — Project memory:** You MAY read and write to files under .fusion/memory/ at the project root (MEMORY.md for durable learnings, YYYY-MM-DD.md for daily notes) to save durable project learnings.
- **Exception — Project memory:** You MAY read and write to .fusion/memory/MEMORY.md at the project root to save durable project learnings (architecture patterns, conventions, pitfalls).
- **Exception — Task attachments:** You MAY read files under .fusion/tasks/{taskId}/attachments/ at the project root for context screenshots and documents attached to this task.
- **Shell commands** run inside the worktree by default. Avoid using cd to navigate outside the worktree.

View File

@@ -18,6 +18,7 @@ import {
memoryExists,
MEMORY_BACKEND_SETTINGS_KEYS,
DEFAULT_MEMORY_BACKEND,
LEGACY_MEMORY_FILE_PATH,
QMD_INSTALL_COMMAND,
buildQmdSearchArgs,
buildQmdCollectionAddArgs,
@@ -28,12 +29,18 @@ import {
qmdMemoryCollectionName,
QMD_REFRESH_INTERVAL_MS,
shouldSkipBackgroundQmdRefresh,
listProjectMemoryFiles,
readProjectMemoryFile,
writeProjectMemoryFile,
} from "./memory-backend.js";
import type { MemoryBackend } from "./memory-backend.js";
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");
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-memory-backend-test-"));
await mkdir(join(tempDir, ".fusion"), { recursive: true });
@@ -78,7 +85,7 @@ describe("memory-backend", () => {
it("should have human-readable name", () => {
const backend = new FileMemoryBackend();
expect(backend.name).toBe("File (.fusion/memory.md)");
expect(backend.name).toBe("File (.fusion/memory/MEMORY.md)");
});
});
@@ -112,7 +119,8 @@ describe("memory-backend", () => {
});
it("should return content when file exists", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "# Project Memory\n\nTest content", "utf-8");
const backend = new FileMemoryBackend();
@@ -123,6 +131,16 @@ describe("memory-backend", () => {
expect(result.backend).toBe("file");
});
it("ignores legacy memory.md when long-term memory is missing", async () => {
writeFileSync(legacyMemoryPath(tempDir), "legacy content", "utf-8");
const backend = new FileMemoryBackend();
const result = await backend.read(tempDir);
expect(result.content).toBe("");
expect(result.exists).toBe(false);
});
// Note: Testing read failure is complex in ESM because we can't easily mock
// the fs/promises module. The error handling is tested through integration tests
// and the MemoryBackendError class tests above.
@@ -137,9 +155,10 @@ describe("memory-backend", () => {
expect(result.success).toBe(true);
expect(result.backend).toBe("file");
const memoryPath = join(tempDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(tempDir);
expect(existsSync(memoryPath)).toBe(true);
expect(readFileSync(memoryPath, "utf-8")).toBe("# Project Memory\n\nNew content");
expect(existsSync(legacyMemoryPath(tempDir))).toBe(false);
});
it("should create .fusion directory if missing", async () => {
@@ -149,12 +168,13 @@ describe("memory-backend", () => {
const backend = new FileMemoryBackend();
await backend.write(newDir, "# Memory");
const memoryPath = join(newDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(newDir);
expect(existsSync(memoryPath)).toBe(true);
});
it("should overwrite existing content", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(tempDir);
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
writeFileSync(memoryPath, "Original content", "utf-8");
const backend = new FileMemoryBackend();
@@ -165,17 +185,17 @@ describe("memory-backend", () => {
it("should not leave temp files on error", async () => {
// This test verifies atomic write behavior
const memoryPath = join(tempDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(tempDir);
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
writeFileSync(memoryPath, "Original", "utf-8");
const backend = new FileMemoryBackend();
// Write should succeed, temp file should be cleaned up
await backend.write(tempDir, "Updated");
// No temp files should exist
const fusionDir = join(tempDir, ".fusion");
const files = require("node:fs").readdirSync(fusionDir);
const files = require("node:fs").readdirSync(join(tempDir, ".fusion", "memory"));
expect(files.filter((f: string) => f.endsWith(".tmp"))).toHaveLength(0);
});
});
@@ -188,13 +208,53 @@ describe("memory-backend", () => {
});
it("should return true when file exists", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "Content", "utf-8");
const backend = new FileMemoryBackend();
const result = await backend.exists(tempDir);
expect(result).toBe(true);
});
it("returns false when only legacy memory.md exists", async () => {
writeFileSync(legacyMemoryPath(tempDir), "legacy content", "utf-8");
const backend = new FileMemoryBackend();
await expect(backend.exists(tempDir)).resolves.toBe(false);
});
});
describe("project memory file APIs", () => {
it("writeProjectMemoryFile writes long-term memory without legacy mirror", async () => {
await writeProjectMemoryFile(tempDir, ".fusion/memory/MEMORY.md", "layered content");
expect(readFileSync(longTermMemoryPath(tempDir), "utf-8")).toBe("layered content");
expect(existsSync(legacyMemoryPath(tempDir))).toBe(false);
});
it("readProjectMemoryFile rejects legacy memory.md paths", async () => {
await expect(readProjectMemoryFile(tempDir, { path: LEGACY_MEMORY_FILE_PATH })).rejects.toThrow(MemoryBackendError);
});
it("listProjectMemoryFiles excludes legacy memory.md entries", async () => {
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
writeFileSync(longTermMemoryPath(tempDir), "# Memory\n\nLong-term", "utf-8");
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);
});
it("search ignores legacy memory.md content", async () => {
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
writeFileSync(longTermMemoryPath(tempDir), "# Memory\n\nDurable conventions", "utf-8");
writeFileSync(legacyMemoryPath(tempDir), "legacy-only-token", "utf-8");
const backend = new FileMemoryBackend();
const results = await backend.search(tempDir, { query: "legacy-only-token" });
expect(results).toHaveLength(0);
});
});
});
@@ -294,7 +354,8 @@ describe("memory-backend", () => {
describe("read", () => {
it("should read memory from filesystem and return qmd backend identifier", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "# Project Memory\n\nTest content", "utf-8");
const backend = new QmdMemoryBackend();
@@ -315,7 +376,8 @@ describe("memory-backend", () => {
});
it("should return empty content for empty file", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "", "utf-8");
const backend = new QmdMemoryBackend();
@@ -325,6 +387,16 @@ describe("memory-backend", () => {
expect(result.exists).toBe(true); // File exists, just empty
expect(result.backend).toBe("qmd");
});
it("ignores legacy memory.md when long-term memory is missing", async () => {
writeFileSync(legacyMemoryPath(tempDir), "legacy content", "utf-8");
const backend = new QmdMemoryBackend();
const result = await backend.read(tempDir);
expect(result.content).toBe("");
expect(result.exists).toBe(false);
});
});
describe("write", () => {
@@ -336,13 +408,14 @@ describe("memory-backend", () => {
expect(result.backend).toBe("qmd");
// Verify file was actually written
const memoryPath = join(tempDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(tempDir);
expect(existsSync(memoryPath)).toBe(true);
expect(readFileSync(memoryPath, "utf-8")).toBe("# Memory\n\nContent");
});
it("should overwrite existing content", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(tempDir);
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
writeFileSync(memoryPath, "Original content", "utf-8");
const backend = new QmdMemoryBackend();
@@ -358,7 +431,7 @@ describe("memory-backend", () => {
const backend = new QmdMemoryBackend();
await backend.write(newDir, "# Memory");
const memoryPath = join(newDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(newDir);
expect(existsSync(memoryPath)).toBe(true);
});
@@ -375,7 +448,8 @@ describe("memory-backend", () => {
describe("exists", () => {
it("should return true when memory file exists", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "Content", "utf-8");
const backend = new QmdMemoryBackend();
@@ -391,8 +465,16 @@ describe("memory-backend", () => {
expect(result).toBe(false);
});
it("returns false when only legacy memory.md exists", async () => {
writeFileSync(legacyMemoryPath(tempDir), "legacy content", "utf-8");
const backend = new QmdMemoryBackend();
await expect(backend.exists(tempDir)).resolves.toBe(false);
});
it("should return true for empty file", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "", "utf-8");
const backend = new QmdMemoryBackend();
@@ -705,7 +787,8 @@ describe("memory-backend", () => {
describe("readMemory", () => {
it("should read using qmd backend by default", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "Test memory content", "utf-8");
const result = await readMemory(tempDir);
@@ -735,7 +818,7 @@ describe("memory-backend", () => {
expect(result.success).toBe(true);
expect(result.backend).toBe("qmd");
const memoryPath = join(tempDir, ".fusion", "memory.md");
const memoryPath = longTermMemoryPath(tempDir);
expect(readFileSync(memoryPath, "utf-8")).toBe("# Memory\n\nContent");
});
@@ -765,7 +848,8 @@ describe("memory-backend", () => {
});
it("should return true when file exists", async () => {
const memoryPath = join(tempDir, ".fusion", "memory.md");
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
const memoryPath = longTermMemoryPath(tempDir);
writeFileSync(memoryPath, "Content", "utf-8");
const result = await memoryExists(tempDir);
@@ -786,7 +870,7 @@ describe("memory-backend", () => {
it("should handle backend switching via settings", async () => {
// First, write with file backend
await writeMemory(tempDir, "Initial content");
expect(existsSync(join(tempDir, ".fusion", "memory.md"))).toBe(true);
expect(existsSync(longTermMemoryPath(tempDir))).toBe(true);
// Read with readonly backend (should still find the file even though it's read-only)
// Note: readMemory doesn't check file existence for readonly - it just returns empty
@@ -800,7 +884,7 @@ describe("memory-backend", () => {
await writeMemory(tempDir, "Persistent content");
// File should exist
expect(existsSync(join(tempDir, ".fusion", "memory.md"))).toBe(true);
expect(existsSync(longTermMemoryPath(tempDir))).toBe(true);
// Read back with file backend
const fileSettings = { [MEMORY_BACKEND_SETTINGS_KEYS.MEMORY_BACKEND_TYPE]: "file" };
@@ -942,7 +1026,7 @@ describe("memory-backend", () => {
const result = await readMemory(nestedDir);
expect(result.content).toBe("Nested content");
expect(existsSync(join(nestedDir, ".fusion", "memory.md"))).toBe(true);
expect(existsSync(longTermMemoryPath(nestedDir))).toBe(true);
});
});
});

View File

@@ -114,7 +114,7 @@ export interface MemorySearchResult {
export interface MemoryFileInfo {
path: string;
label: string;
layer: "long-term" | "daily" | "dreams" | "legacy";
layer: "long-term" | "daily" | "dreams";
size: number;
updatedAt: string;
}
@@ -210,12 +210,12 @@ const backendRegistry = new Map<string, MemoryBackend>();
/**
* File-based memory backend.
*
* Stores project memory in `.fusion/memory.md` at the project root.
* Preserves the legacy `.fusion/memory.md` storage path when explicitly selected.
* 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.md)";
readonly name = "File (.fusion/memory/MEMORY.md)";
readonly capabilities: MemoryBackendCapabilities = {
readable: true,
writable: true,
@@ -224,25 +224,12 @@ export class FileMemoryBackend implements MemoryBackend {
persistent: true,
};
/**
* Get the absolute path to the memory file.
*/
private getFilePath(rootDir: string): string {
return join(rootDir, LEGACY_MEMORY_FILE_PATH);
}
private getLongTermPath(rootDir: string): string {
return join(rootDir, MEMORY_WORKSPACE_PATH, MEMORY_LONG_TERM_FILENAME);
}
async read(rootDir: string): Promise<MemoryReadResult> {
const longTermPath = this.getLongTermPath(rootDir);
const legacyPath = this.getFilePath(rootDir);
let filePath = existsSync(longTermPath) ? longTermPath : legacyPath;
if (existsSync(longTermPath) && existsSync(legacyPath)) {
const [longTermStat, legacyStat] = await Promise.all([stat(longTermPath), stat(legacyPath)]);
filePath = legacyStat.mtimeMs > longTermStat.mtimeMs ? legacyPath : longTermPath;
}
const filePath = this.getLongTermPath(rootDir);
try {
const content = await readFile(filePath, "utf-8");
return {
@@ -269,17 +256,12 @@ export class FileMemoryBackend implements MemoryBackend {
async write(rootDir: string, content: string): Promise<MemoryWriteResult> {
const filePath = this.getLongTermPath(rootDir);
const dir = join(rootDir, MEMORY_WORKSPACE_PATH);
const legacyPath = this.getFilePath(rootDir);
const legacyDir = join(rootDir, ".fusion");
try {
// Ensure directory exists
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
if (!existsSync(legacyDir)) {
await mkdir(legacyDir, { recursive: true });
}
// Write atomically using temp file
const tmpPath = filePath + ".tmp";
@@ -289,11 +271,6 @@ export class FileMemoryBackend implements MemoryBackend {
const { rename } = await import("node:fs/promises");
await rename(tmpPath, filePath);
// Temporary compatibility mirror while callers migrate to the layered path.
const legacyTmpPath = legacyPath + ".tmp";
await writeFile(legacyTmpPath, content, "utf-8");
await rename(legacyTmpPath, legacyPath);
return {
success: true,
backend: this.type,
@@ -308,11 +285,8 @@ export class FileMemoryBackend implements MemoryBackend {
}
async exists(rootDir: string): Promise<boolean> {
const filePath = existsSync(this.getLongTermPath(rootDir))
? this.getLongTermPath(rootDir)
: this.getFilePath(rootDir);
try {
await access(filePath, constants.R_OK);
await access(this.getLongTermPath(rootDir), constants.R_OK);
return true;
} catch {
return false;
@@ -377,17 +351,17 @@ export class ReadOnlyMemoryBackend implements MemoryBackend {
/**
* QMD (qmd index/query integration) memory backend.
*
* Stores project memory in `.fusion/memory.md` so it can be indexed and queried
* Stores project memory in `.fusion/memory/MEMORY.md` so it can be indexed and queried
* by the external `qmd` tool. Read/write operations use direct filesystem access
* for reliability. The `qmd` tool can be configured separately to watch and index
* the memory file for advanced querying capabilities.
* layered memory files for advanced querying capabilities.
*
* **Capabilities:**
* - readable: true
* - writable: true
* - supportsAtomicWrite: false (QMD indexing is async/external)
* - hasConflictResolution: false
* - persistent: true (memory file persists in `.fusion/memory.md`)
* - persistent: true (memory files persist in `.fusion/memory/`)
*
* @example
* ```typescript
@@ -587,7 +561,6 @@ export async function ensureOpenClawMemoryFiles(rootDir: string, date = new Date
function getMemoryFileLayer(displayPath: string): MemoryFileInfo["layer"] {
if (displayPath === `${MEMORY_WORKSPACE_PATH}/${MEMORY_LONG_TERM_FILENAME}`) return "long-term";
if (displayPath === `${MEMORY_WORKSPACE_PATH}/${MEMORY_DREAMS_FILENAME}`) return "dreams";
if (displayPath === LEGACY_MEMORY_FILE_PATH) return "legacy";
return "daily";
}
@@ -595,7 +568,6 @@ function getMemoryFileLabel(displayPath: string): string {
const layer = getMemoryFileLayer(displayPath);
if (layer === "long-term") return "Long-term memory";
if (layer === "dreams") return "Dreams";
if (layer === "legacy") return "Legacy memory";
return `Daily notes ${basename(displayPath, ".md")}`;
}
@@ -618,7 +590,6 @@ export async function listProjectMemoryFiles(rootDir: string, date = new Date())
"long-term": 0,
daily: 1,
dreams: 2,
legacy: 3,
};
return infos.sort((a, b) => order[a.layer] - order[b.layer] || b.path.localeCompare(a.path));
}
@@ -641,20 +612,13 @@ export async function readProjectMemoryFileContent(rootDir: string, path: string
}
export async function writeProjectMemoryFile(rootDir: string, path: string, content: string): Promise<MemoryWriteResult> {
const { absPath, displayPath } = resolveMemoryFilePath(rootDir, path);
const { absPath } = resolveMemoryFilePath(rootDir, path);
await mkdir(dirname(absPath), { recursive: true });
const tmpPath = `${absPath}.tmp`;
await writeFile(tmpPath, content, "utf-8");
const { rename } = await import("node:fs/promises");
await rename(tmpPath, absPath);
if (displayPath === `${MEMORY_WORKSPACE_PATH}/${MEMORY_LONG_TERM_FILENAME}`) {
const legacyPath = join(rootDir, LEGACY_MEMORY_FILE_PATH);
const legacyTmpPath = `${legacyPath}.tmp`;
await writeFile(legacyTmpPath, content, "utf-8");
await rename(legacyTmpPath, legacyPath);
}
return { success: true, backend: "file" };
}
@@ -679,9 +643,6 @@ function normalizeMemoryRequestPath(rawPath: string): string {
) {
return `${MEMORY_WORKSPACE_PATH}/${basename(normalized)}`;
}
if (normalized === LEGACY_MEMORY_FILE_PATH) {
return normalized;
}
if (DAILY_MEMORY_RE.test(basename(normalized)) && (normalized === basename(normalized) || normalized.startsWith("memory/"))) {
return `${MEMORY_WORKSPACE_PATH}/${basename(normalized)}`;
}
@@ -693,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, .fusion/memory.md`,
`Memory path '${rawPath}' is outside allowed files: MEMORY.md, DREAMS.md, memory/YYYY-MM-DD.md`,
"memory",
);
}
@@ -760,11 +721,6 @@ async function listMemoryFiles(rootDir: string): Promise<Array<{ absPath: string
}
}
const legacyPath = join(rootDir, LEGACY_MEMORY_FILE_PATH);
if (existsSync(legacyPath)) {
files.push({ absPath: legacyPath, displayPath: LEGACY_MEMORY_FILE_PATH });
}
return files;
}

View File

@@ -31,7 +31,7 @@ describe("memory-insights", () => {
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-memory-insights-test-"));
await mkdir(join(tempDir, ".fusion"), { recursive: true });
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
});
afterEach(async () => {
@@ -612,7 +612,7 @@ describe("memory-insights", () => {
describe("constants", () => {
it("should have correct file paths", () => {
expect(MEMORY_WORKING_PATH).toBe(".fusion/memory.md");
expect(MEMORY_WORKING_PATH).toBe(".fusion/memory/MEMORY.md");
expect(MEMORY_INSIGHTS_PATH).toBe(".fusion/memory-insights.md");
});
@@ -637,7 +637,7 @@ describe("memory-insights audit file operations", () => {
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-memory-audit-test-"));
await mkdir(join(tempDir, ".fusion"), { recursive: true });
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
});
afterEach(async () => {
@@ -697,7 +697,7 @@ describe("memory-insights run processing", () => {
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-memory-run-test-"));
await mkdir(join(tempDir, ".fusion"), { recursive: true });
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
});
afterEach(async () => {
@@ -1028,7 +1028,7 @@ describe("memory-insights audit generation", () => {
beforeEach(async () => {
tempDir = mkdtempSync(join(tmpdir(), "kb-memory-audit-gen-test-"));
await mkdir(join(tempDir, ".fusion"), { recursive: true });
await mkdir(join(tempDir, ".fusion", "memory"), { recursive: true });
});
afterEach(async () => {

View File

@@ -52,7 +52,7 @@
*
* ## Retention Policy
*
* - **Working memory** (`memory.md`): Manual/agent-maintained. No automatic
* - **Working memory** (`MEMORY.md`): Manual/agent-maintained. No automatic
* pruning — agents are expected to keep it relevant.
*
* - **Insights memory** (`memory-insights.md`): Only grows through
@@ -66,14 +66,14 @@
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { dirname, join } from "node:path";
import type { ProjectSettings } from "./types.js";
import type { ScheduledTaskCreateInput } from "./automation.js";
// ── Constants ────────────────────────────────────────────────────────
/** Path to working memory relative to project root. */
export const MEMORY_WORKING_PATH = ".fusion/memory.md";
export const MEMORY_WORKING_PATH = ".fusion/memory/MEMORY.md";
/** Path to insights memory relative to project root. */
export const MEMORY_INSIGHTS_PATH = ".fusion/memory-insights.md";
@@ -225,7 +225,7 @@ export interface ProcessRunInput {
// ── File I/O ─────────────────────────────────────────────────────────
/**
* Read the working memory file (`memory.md`).
* Read the working memory file (`MEMORY.md`).
*
* Returns an empty string if the file does not exist, enabling graceful
* handling when FN-810's memory system is not yet in place.
@@ -277,7 +277,7 @@ export async function writeInsightsMemory(rootDir: string, content: string): Pro
}
/**
* Write the working memory file (`memory.md`).
* Write the working memory file (`MEMORY.md`).
*
* Creates the `.fusion` directory if it does not exist.
*
@@ -286,7 +286,7 @@ export async function writeInsightsMemory(rootDir: string, content: string): Pro
*/
export async function writeWorkingMemory(rootDir: string, content: string): Promise<void> {
const filePath = join(rootDir, MEMORY_WORKING_PATH);
const dir = join(rootDir, ".fusion");
const dir = dirname(filePath);
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}

View File

@@ -17,6 +17,7 @@ import {
searchProjectMemory,
resolveMemoryInstructionContext,
} from "./project-memory.js";
import { LEGACY_MEMORY_FILE_PATH } from "./memory-backend.js";
describe("project-memory", () => {
let testDir: string;
@@ -24,7 +25,7 @@ describe("project-memory", () => {
beforeEach(async () => {
testDir = join(tmpdir(), `kb-memory-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
memoryPath = join(testDir, ".fusion", "memory.md");
memoryPath = join(testDir, ".fusion", "memory", "MEMORY.md");
// Create the test directory but not the .fusion subdirectory
// Individual tests can create .fusion as needed
await mkdir(testDir, { recursive: true });
@@ -39,13 +40,13 @@ describe("project-memory", () => {
describe("MEMORY_FILE_PATH", () => {
it("is a relative path under .fusion", () => {
expect(MEMORY_FILE_PATH).toBe(".fusion/memory.md");
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.md");
expect(memoryFilePath("/project")).toBe("/project/.fusion/memory/MEMORY.md");
});
});
@@ -115,7 +116,7 @@ describe("project-memory", () => {
// Manually edit the content
const { writeFile } = await import("node:fs/promises");
const customContent = "# Custom Memory\n\nMy custom content";
await writeFile(memoryFilePath(testDir), customContent, "utf-8");
await writeFile(memoryPath, customContent, "utf-8");
// Ensure again — should NOT overwrite
const created = await ensureMemoryFile(testDir);
@@ -154,6 +155,14 @@ describe("project-memory", () => {
const content = await readProjectMemory(testDir);
expect(content).toContain("# 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");
const content = await readProjectMemory(testDir);
expect(content).toBe("");
});
});
// ── buildTriageMemoryInstructions ─────────────────────────────────
@@ -166,7 +175,7 @@ describe("project-memory", () => {
it("does not inject a raw memory file path by default", () => {
const instructions = buildTriageMemoryInstructions(testDir);
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
});
it("instructs agent to search memory first", () => {
@@ -191,7 +200,7 @@ describe("project-memory", () => {
it("does not inject a raw memory file path by default", () => {
const instructions = buildExecutionMemoryInstructions(testDir);
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
});
it("instructs agent to search memory at start", () => {
@@ -228,7 +237,7 @@ describe("project-memory", () => {
it("keeps qmd default path-agnostic", () => {
const instructions = buildExecutionMemoryInstructions(testDir);
expect(instructions).not.toContain("`.fusion/memory.md`");
expect(instructions).not.toContain("`.fusion/memory/MEMORY.md`");
});
});
@@ -392,7 +401,7 @@ describe("project-memory", () => {
it("returns memory content when using QMD backend", async () => {
// Create the memory file directly (simulating prior creation)
await mkdir(join(testDir, ".fusion"), { recursive: true });
await mkdir(join(testDir, ".fusion", "memory"), { recursive: true });
await writeFile(memoryPath, "# QMD Memory\n\nSome content", "utf-8");
const settings = { memoryBackendType: "qmd" };
@@ -451,7 +460,7 @@ describe("project-memory", () => {
it("returns file backend context when explicitly set", () => {
const ctx = resolveMemoryInstructionContext({ memoryBackendType: "file" });
expect(ctx.backendType).toBe("file");
expect(ctx.instructionPathHint).toBe(".fusion/memory.md");
expect(ctx.instructionPathHint).toBe(".fusion/memory/MEMORY.md");
});
it("returns readonly backend context", () => {
@@ -482,10 +491,10 @@ describe("project-memory", () => {
// ── Backend-aware buildTriageMemoryInstructions ─────────────────────────────────
describe("buildTriageMemoryInstructions with backend settings", () => {
it("includes .fusion/memory.md for file backend", () => {
it("includes .fusion/memory/MEMORY.md for file backend", () => {
const settings = { memoryBackendType: "file" };
const instructions = buildTriageMemoryInstructions(testDir, settings);
expect(instructions).toContain(".fusion/memory.md");
expect(instructions).toContain(".fusion/memory/MEMORY.md");
expect(instructions).toContain("## Project Memory");
});
@@ -496,17 +505,17 @@ describe("project-memory", () => {
// Should NOT contain write/update directives
expect(instructions).not.toMatch(/write|update/i);
// Should NOT contain the specific file path
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
// Should instruct to consult memory
expect(instructions).toMatch(/consult.*memory|memory.*context/i);
});
it("does not include .fusion/memory.md for qmd backend", () => {
it("does not include .fusion/memory/MEMORY.md for qmd backend", () => {
const settings = { memoryBackendType: "qmd" };
const instructions = buildTriageMemoryInstructions(testDir, settings);
expect(instructions).toContain("## Project Memory");
// QMD should NOT unconditionally reference .fusion/memory.md
expect(instructions).not.toContain(".fusion/memory.md");
// QMD should NOT unconditionally reference .fusion/memory/MEMORY.md
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
expect(instructions).toContain("memory_search");
expect(instructions).toContain("memory_get");
});
@@ -515,32 +524,32 @@ describe("project-memory", () => {
const settings = { memoryBackendType: "qmd" };
const instructions = buildTriageMemoryInstructions(testDir, settings);
expect(instructions).toContain("## Project Memory");
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
expect(instructions).toContain("memory_search");
});
it("does not include .fusion/memory.md for non-file backends without instructionPathHint", () => {
it("does not include .fusion/memory/MEMORY.md for non-file backends without instructionPathHint", () => {
const settings = { memoryBackendType: "some-custom-backend" };
const instructions = buildTriageMemoryInstructions(testDir, settings);
expect(instructions).toContain("memory_search");
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
});
it("defaults to qmd guidance when settings are omitted", () => {
const instructions = buildTriageMemoryInstructions(testDir);
expect(instructions).toContain("memory_search");
expect(instructions).toContain("memory_get");
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
});
});
// ── Backend-aware buildExecutionMemoryInstructions ─────────────────────────────────
describe("buildExecutionMemoryInstructions with backend settings", () => {
it("includes .fusion/memory.md for file backend", () => {
it("includes .fusion/memory/MEMORY.md for file backend", () => {
const settings = { memoryBackendType: "file" };
const instructions = buildExecutionMemoryInstructions(testDir, settings);
expect(instructions).toContain(".fusion/memory.md");
expect(instructions).toContain(".fusion/memory/MEMORY.md");
expect(instructions).toContain("## Project Memory");
// Should have write instructions
expect(instructions).toMatch(/end of execution|before calling.*task_done/i);
@@ -553,17 +562,17 @@ describe("project-memory", () => {
// Should NOT contain write/update directives
expect(instructions).not.toMatch(/write.*memory|update.*memory/i);
// Should NOT contain the specific file path
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
// Should instruct to consult memory at start
expect(instructions).toMatch(/consult.*memory/i);
});
it("does not include .fusion/memory.md for qmd backend", () => {
it("does not include .fusion/memory/MEMORY.md for qmd backend", () => {
const settings = { memoryBackendType: "qmd" };
const instructions = buildExecutionMemoryInstructions(testDir, settings);
expect(instructions).toContain("## Project Memory");
// QMD should NOT unconditionally reference .fusion/memory.md
expect(instructions).not.toContain(".fusion/memory.md");
// QMD should NOT unconditionally reference .fusion/memory/MEMORY.md
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
expect(instructions).toContain("memory_search");
expect(instructions).toContain("memory_get");
});
@@ -572,7 +581,7 @@ describe("project-memory", () => {
const settings = { memoryBackendType: "qmd" };
const instructions = buildExecutionMemoryInstructions(testDir, settings);
expect(instructions).toContain("## Project Memory");
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
expect(instructions).toContain("memory_search");
// Contains "end of execution" write guidance
expect(instructions).toMatch(/end of execution/i);
@@ -586,7 +595,7 @@ describe("project-memory", () => {
const instructions = buildExecutionMemoryInstructions(testDir);
expect(instructions).toContain("memory_search");
expect(instructions).toContain("memory_get");
expect(instructions).not.toContain(".fusion/memory.md");
expect(instructions).not.toContain(".fusion/memory/MEMORY.md");
expect(instructions).toMatch(/end of execution|before calling.*task_done/i);
});

View File

@@ -1,7 +1,7 @@
/**
* Project Memory Bootstrap
*
* Provides the canonical path and default scaffold for `.fusion/memory.md`,
* Provides the canonical path and default scaffold for `.fusion/memory/MEMORY.md`,
* plus idempotent `ensure` functions that create memory only when missing.
*
* This module supports both file-based (direct filesystem) and backend-aware
@@ -19,7 +19,7 @@
* - The memory instruction templates used by triage and executor prompts
*/
import { readFile, writeFile, mkdir, stat } from "node:fs/promises";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import {
@@ -34,7 +34,7 @@ import {
// ── Constants ────────────────────────────────────────────────────────
/** Path to the project memory file relative to project root. */
export const MEMORY_FILE_PATH = ".fusion/memory.md";
export const MEMORY_FILE_PATH = ".fusion/memory/MEMORY.md";
/** Canonical absolute path helper. */
export function memoryFilePath(rootDir: string): string {
@@ -88,19 +88,15 @@ export function getDefaultMemoryScaffold(): string {
*/
export async function ensureMemoryFile(rootDir: string): Promise<boolean> {
const filePath = memoryFilePath(rootDir);
if (existsSync(filePath)) {
await ensureOpenClawMemoryFiles(rootDir);
return false;
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");
}
const dir = join(rootDir, ".fusion");
if (!existsSync(dir)) {
await mkdir(dir, { recursive: true });
}
await writeFile(filePath, getDefaultMemoryScaffold(), "utf-8");
await ensureOpenClawMemoryFiles(rootDir);
return true;
return longTermCreated;
}
/**
@@ -140,7 +136,7 @@ export interface MemoryInstructionContext {
capabilities: import("./memory-backend.js").MemoryBackendCapabilities;
/**
* Path hint for memory instructions.
* - For "file" backend: ".fusion/memory.md"
* - For "file" backend: ".fusion/memory/MEMORY.md"
* - For "readonly" backend: null (no write path)
* - For "qmd"/non-file backends: null (path is backend-specific)
*/
@@ -152,9 +148,9 @@ export interface MemoryInstructionContext {
*
* This function determines what memory instructions should be injected
* based on the configured backend type:
* - "file" backend: full read/write instructions with `.fusion/memory.md` path
* - "file" backend: full read/write instructions with `.fusion/memory/MEMORY.md` path
* - "readonly" backend: read-only instructions, no write/update directives
* - "qmd"/non-file backends: instructions without unconditional `.fusion/memory.md` path
* - "qmd"/non-file backends: instructions without unconditional `.fusion/memory/MEMORY.md` path
* (unless `instructionPathHint` is explicitly non-null)
*
* @param settings - Optional project settings containing memoryEnabled and memoryBackendType
@@ -213,7 +209,7 @@ export function resolveMemoryInstructionContext(
case "file":
return {
backendType: "file",
backendName: "File (.fusion/memory.md)",
backendName: "File (.fusion/memory/MEMORY.md)",
capabilities: {
readable: true,
writable: true,
@@ -221,7 +217,7 @@ export function resolveMemoryInstructionContext(
hasConflictResolution: false,
persistent: true,
},
instructionPathHint: ".fusion/memory.md",
instructionPathHint: ".fusion/memory/MEMORY.md",
};
default:
return {
@@ -396,9 +392,9 @@ export async function getProjectMemory(
* @param rootDir - Absolute path to the project root directory.
* @param settings - Optional project settings for backend-aware instruction generation.
* When provided, the function branches based on memoryBackendType:
* - "file": includes `.fusion/memory.md` read guidance
* - "file": includes `.fusion/memory/MEMORY.md` read guidance
* - "readonly": read-only instructions, no write directives
* - "qmd"/non-file: instructions without unconditional `.fusion/memory.md` path
* - "qmd"/non-file: instructions without unconditional `.fusion/memory/MEMORY.md` path
* @returns The memory instruction section string, or empty string if the
* memory file does not exist yet.
*/
@@ -436,14 +432,13 @@ This project has a memory system that stores durable project learnings.
This project has OpenClaw-style memory files:
- \`.fusion/memory/MEMORY.md\` — curated long-term memory for durable decisions, conventions, and pitfalls
- \`.fusion/memory/YYYY-MM-DD.md\` — append-only daily notes for running context
- Legacy fallback: \`.fusion/memory.md\`
**Before writing the specification:**
1. Use \`memory_search\` first for task-relevant context
2. Use \`memory_get\` only for specific memory files/line ranges returned by search
3. Incorporate relevant learnings into your specification — reference actual patterns, constraints, and conventions documented there
Do not read all memory or read \`.fusion/memory.md\` directly by default. If memory is irrelevant, skip it.
Do not read all memory directly by default. If memory is irrelevant, skip it.
`;
}
@@ -476,9 +471,9 @@ This project has a memory system that stores durable project learnings.
* @param rootDir - Absolute path to the project root directory.
* @param settings - Optional project settings for backend-aware instruction generation.
* When provided, the function branches based on memoryBackendType:
* - "file": includes `.fusion/memory.md` read/write guidance
* - "file": includes `.fusion/memory/MEMORY.md` read/write guidance
* - "readonly": read-only instructions, no write/update directives
* - "qmd"/non-file: instructions without unconditional `.fusion/memory.md` path
* - "qmd"/non-file: instructions without unconditional `.fusion/memory/MEMORY.md` path
* @returns The memory instruction section string.
*/
export function buildExecutionMemoryInstructions(
@@ -515,13 +510,12 @@ This project has a memory system that stores durable project learnings.
This project has OpenClaw-style memory files:
- \`.fusion/memory/MEMORY.md\` — curated long-term memory for durable decisions, conventions, and pitfalls
- \`.fusion/memory/YYYY-MM-DD.md\` — append-only daily notes for running observations and open loops
- Legacy fallback: \`.fusion/memory.md\`
**At the start of execution:**
1. Use \`memory_search\` first for task-relevant context
2. Use \`memory_get\` only for specific memory files/line ranges returned by search
3. Apply relevant learnings to your implementation — follow documented patterns and avoid known pitfalls
4. Do not load all memory or read \`.fusion/memory.md\` directly by default. Skip memory reads when memory is irrelevant or context is tight.
4. Do not load all memory directly by default. Skip memory reads when memory is irrelevant or context is tight.
**At the end of execution (before calling \`task_done()\`):**
1. Review what you learned during this task that would genuinely benefit future runs
@@ -607,16 +601,8 @@ This project has a memory system that stores durable project learnings.
*/
export async function readProjectMemory(rootDir: string): Promise<string> {
const longTermPath = memoryLongTermPath(rootDir);
const filePath = memoryFilePath(rootDir);
if (existsSync(longTermPath) && existsSync(filePath)) {
const [longTermStat, legacyStat] = await Promise.all([stat(longTermPath), stat(filePath)]);
return readFile(legacyStat.mtimeMs > longTermStat.mtimeMs ? filePath : longTermPath, "utf-8");
}
if (existsSync(longTermPath)) {
return readFile(longTermPath, "utf-8");
}
if (!existsSync(filePath)) {
if (!existsSync(longTermPath)) {
return "";
}
return readFile(filePath, "utf-8");
return readFile(longTermPath, "utf-8");
}

View File

@@ -8251,10 +8251,8 @@ Task with acceptance criteria
});
describe("project memory bootstrap", () => {
it("creates .fusion/memory.md on init when memoryEnabled is default (true)", async () => {
// The default store in beforeEach already called init() with default settings
// memoryEnabled defaults to true, so memory.md should exist
const memoryPath = join(rootDir, ".fusion", "memory.md");
it("creates .fusion/memory/MEMORY.md on init when memoryEnabled is default (true)", async () => {
const memoryPath = join(rootDir, ".fusion", "memory", "MEMORY.md");
expect(existsSync(memoryPath)).toBe(true);
const content = await readFile(memoryPath, "utf-8");
@@ -8263,71 +8261,60 @@ Task with acceptance criteria
expect(content).toContain("## Conventions");
});
it("does not create .fusion/memory.md when memoryEnabled is false", async () => {
it("does not create .fusion/memory/MEMORY.md when memoryEnabled is false after re-init", async () => {
const localRoot = makeTmpDir();
const localGlobal = makeTmpDir();
let localStore: TaskStore | undefined;
let secondStore: TaskStore | undefined;
try {
const localStore = new TaskStore(localRoot, localGlobal);
localStore = new TaskStore(localRoot, localGlobal);
await localStore.init();
// Explicitly disable memory
await localStore.updateSettings({ memoryEnabled: false } as any);
// Delete the file if it was created during init (default enabled)
const memoryPath = join(localRoot, ".fusion", "memory.md");
if (existsSync(memoryPath)) {
await unlink(memoryPath);
}
localStore.close();
// Re-init with memory disabled
const store2 = new TaskStore(localRoot, localGlobal);
// Manually set memoryEnabled to false before init
await store2.init();
await store2.updateSettings({ memoryEnabled: false } as any);
// After setting false, verify we can init without creating
store2.close();
// Create a third store with memory disabled in config
const store3 = new TaskStore(localRoot, localGlobal);
await store3.updateSettings({ memoryEnabled: false } as any);
await store3.init();
// Memory file should not exist if it was deleted
// But init creates it by default, then we disabled it
// The key behavior is that when memoryEnabled is explicitly false,
// init() should not create the file
store3.close();
} finally {
await rm(localRoot, { recursive: true, force: true });
await rm(localGlobal, { recursive: true, force: true });
}
});
it("creates .fusion/memory.md when memory is toggled on via updateSettings", async () => {
const localRoot = makeTmpDir();
const localGlobal = makeTmpDir();
try {
const localStore = new TaskStore(localRoot, localGlobal);
await localStore.init();
// First disable memory
await localStore.updateSettings({ memoryEnabled: false } as any);
const memoryPath = join(localRoot, ".fusion", "memory.md");
// Delete the file that was created during init
const memoryPath = join(localRoot, ".fusion", "memory", "MEMORY.md");
if (existsSync(memoryPath)) {
await unlink(memoryPath);
}
expect(existsSync(memoryPath)).toBe(false);
localStore.close();
localStore = undefined;
secondStore = new TaskStore(localRoot, localGlobal);
await secondStore.init();
expect(existsSync(memoryPath)).toBe(false);
} finally {
secondStore?.close();
localStore?.close();
await rm(localRoot, { recursive: true, force: true });
await rm(localGlobal, { recursive: true, force: true });
}
});
it("creates .fusion/memory/MEMORY.md when memory is toggled on via updateSettings", async () => {
const localRoot = makeTmpDir();
const localGlobal = makeTmpDir();
let localStore: TaskStore | undefined;
try {
localStore = new TaskStore(localRoot, localGlobal);
await localStore.init();
await localStore.updateSettings({ memoryEnabled: false } as any);
const memoryPath = join(localRoot, ".fusion", "memory", "MEMORY.md");
if (existsSync(memoryPath)) {
await unlink(memoryPath);
}
expect(existsSync(memoryPath)).toBe(false);
// Now toggle memory back on
await localStore.updateSettings({ memoryEnabled: true } as any);
expect(existsSync(memoryPath)).toBe(true);
const content = await readFile(memoryPath, "utf-8");
expect(content).toContain("# Project Memory");
localStore.close();
} finally {
localStore?.close();
await rm(localRoot, { recursive: true, force: true });
await rm(localGlobal, { recursive: true, force: true });
}
@@ -8336,25 +8323,22 @@ Task with acceptance criteria
it("does not overwrite existing memory content when toggled on", async () => {
const localRoot = makeTmpDir();
const localGlobal = makeTmpDir();
let localStore: TaskStore | undefined;
try {
const localStore = new TaskStore(localRoot, localGlobal);
localStore = new TaskStore(localRoot, localGlobal);
await localStore.init();
const memoryPath = join(localRoot, ".fusion", "memory.md");
const memoryPath = join(localRoot, ".fusion", "memory", "MEMORY.md");
// Write custom content
const customContent = "# My Custom Memory\n\nImportant stuff";
await writeFile(memoryPath, customContent, "utf-8");
// Disable then re-enable memory
await localStore.updateSettings({ memoryEnabled: false } as any);
await localStore.updateSettings({ memoryEnabled: true } as any);
// Custom content should be preserved
const content = await readFile(memoryPath, "utf-8");
expect(content).toBe(customContent);
localStore.close();
} finally {
localStore?.close();
await rm(localRoot, { recursive: true, force: true });
await rm(localGlobal, { recursive: true, force: true });
}

View File

@@ -1269,9 +1269,9 @@ export interface ProjectSettings {
* Extraction only runs if BOTH this time has elapsed AND memory has grown
* by more than MIN_INSIGHT_GROWTH_CHARS characters. Default: 86400000 (24h). */
insightExtractionMinIntervalMs?: number;
/** When enabled, agents will consult and update the .fusion/memory/ directory with durable
/** When enabled, agents will consult and update .fusion/memory/MEMORY.md with durable
* project learnings. When disabled, agents will not include memory instructions
* in their prompts and will not read or write to the .fusion/memory/ directory.
* in their prompts and will not read or write to .fusion/memory/MEMORY.md.
* Default: true (enabled for backward compatibility). */
memoryEnabled?: boolean;
/** Memory backend type for pluggable memory storage.