feat(FN-925): add memoryEnabled setting with UI toggle and engine integration
- Add memoryEnabled boolean setting to core types and default settings - Add Memory section toggle to SettingsModal dashboard UI - Integrate memoryEnabled check in executor and triage engine prompts - Add tests for settings UI, executor prompt behavior, and triage prompt behavior - Update SettingsModal section count test for the new Memory section
This commit is contained in:
@@ -872,6 +872,11 @@ 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 .fusion/memory.md with durable
|
||||
* project learnings. When disabled, agents will not include memory instructions
|
||||
* in their prompts and will not read or write to .fusion/memory.md.
|
||||
* Default: true (enabled for backward compatibility). */
|
||||
memoryEnabled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -958,6 +963,7 @@ export const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
|
||||
insightExtractionEnabled: false,
|
||||
insightExtractionSchedule: "0 2 * * *",
|
||||
insightExtractionMinIntervalMs: 86_400_000,
|
||||
memoryEnabled: true,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1034,6 +1040,7 @@ export const PROJECT_SETTINGS_KEYS: ReadonlyArray<keyof ProjectSettings> = [
|
||||
"insightExtractionEnabled",
|
||||
"insightExtractionSchedule",
|
||||
"insightExtractionMinIntervalMs",
|
||||
"memoryEnabled",
|
||||
] as const;
|
||||
|
||||
export interface BoardConfig {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { SettingsModal } from "./SettingsModal";
|
||||
import type { SettingsExportData } from "../api";
|
||||
|
||||
@@ -198,4 +199,74 @@ describe("SettingsModal", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory section", () => {
|
||||
it("renders the Memory section in the sidebar", async () => {
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Memory")).toBeDefined();
|
||||
});
|
||||
|
||||
it("shows the memory toggle with default enabled", async () => {
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Click the Memory section in the sidebar
|
||||
await userEvent.click(screen.getByText("Memory"));
|
||||
|
||||
const checkbox = screen.getByRole("checkbox", { name: /enable project memory/i });
|
||||
expect(checkbox).toBeDefined();
|
||||
// Default is enabled, so checkbox should be checked
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
|
||||
it("shows memory toggle unchecked when memoryEnabled is false", async () => {
|
||||
mockFetchSettings.mockResolvedValue({
|
||||
...defaultSettings,
|
||||
memoryEnabled: false,
|
||||
});
|
||||
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Click the Memory section in the sidebar
|
||||
await userEvent.click(screen.getByText("Memory"));
|
||||
|
||||
const checkbox = screen.getByRole("checkbox", { name: /enable project memory/i });
|
||||
expect(checkbox).toBeDefined();
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("toggles the memory setting when checkbox is clicked", async () => {
|
||||
renderModal();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// Click the Memory section in the sidebar
|
||||
await userEvent.click(screen.getByText("Memory"));
|
||||
|
||||
const checkbox = screen.getByRole("checkbox", { name: /enable project memory/i });
|
||||
expect(checkbox).toBeChecked();
|
||||
|
||||
// Uncheck it
|
||||
await userEvent.click(checkbox);
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
// Check it again
|
||||
await userEvent.click(checkbox);
|
||||
expect(checkbox).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,6 +41,7 @@ const SETTINGS_SECTIONS = [
|
||||
{ id: "worktrees", label: "Worktrees", scope: "project" as const },
|
||||
{ id: "commands", label: "Commands", scope: "project" as const },
|
||||
{ id: "merge", label: "Merge", scope: "project" as const },
|
||||
{ id: "memory", label: "Memory", scope: "project" as const },
|
||||
{ id: "backups", label: "Backups", scope: "project" as const },
|
||||
{ id: "notifications", label: "Notifications", scope: "global" as const },
|
||||
{ id: "authentication", label: "Authentication", scope: undefined },
|
||||
@@ -1397,6 +1398,27 @@ export function SettingsModal({
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "memory":
|
||||
return (
|
||||
<>
|
||||
{renderScopeBanner()}
|
||||
<h4 className="settings-section-heading">Memory</h4>
|
||||
<div className="form-group">
|
||||
<label htmlFor="memoryEnabled" className="checkbox-label">
|
||||
<input
|
||||
id="memoryEnabled"
|
||||
type="checkbox"
|
||||
checked={form.memoryEnabled !== false}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, memoryEnabled: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
Enable project memory
|
||||
</label>
|
||||
<small>When enabled, agents will consult and update .fusion/memory.md with durable project learnings</small>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
case "backups":
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1262,14 +1262,14 @@ describe("SettingsModal", () => {
|
||||
expect(layout!.querySelector(".settings-content")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("has .settings-sidebar with 10 .settings-nav-item buttons for all sections", async () => {
|
||||
it("has .settings-sidebar with 11 .settings-nav-item buttons for all sections", async () => {
|
||||
const { container } = render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
|
||||
const sidebar = container.querySelector(".settings-sidebar");
|
||||
expect(sidebar).toBeTruthy();
|
||||
const navItems = sidebar!.querySelectorAll(".settings-nav-item");
|
||||
expect(navItems.length).toBe(10);
|
||||
expect(navItems.length).toBe(11);
|
||||
|
||||
// Labels include scope emoji indicators (🌐 for global, 📁 for project)
|
||||
const labels = Array.from(navItems).map((el) => el.textContent);
|
||||
@@ -1281,6 +1281,7 @@ describe("SettingsModal", () => {
|
||||
"📁Worktrees",
|
||||
"📁Commands",
|
||||
"📁Merge",
|
||||
"📁Memory",
|
||||
"📁Backups",
|
||||
"🌐Notifications",
|
||||
"Authentication",
|
||||
|
||||
@@ -2125,6 +2125,31 @@ describe("buildExecutionPrompt", () => {
|
||||
expect(agentPrompt).toContain("- **Test:** `npm test`");
|
||||
expect(agentPrompt).toContain("- **Build:** `npm run build`");
|
||||
});
|
||||
|
||||
describe("memoryEnabled setting", () => {
|
||||
it("accepts memoryEnabled: true without error", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task, "/project", {
|
||||
memoryEnabled: true,
|
||||
} as any);
|
||||
// Memory instructions are a placeholder until FN-810; just verify no crash
|
||||
expect(result).toContain("Execute this task.");
|
||||
});
|
||||
|
||||
it("accepts memoryEnabled: false without error", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task, "/project", {
|
||||
memoryEnabled: false,
|
||||
} as any);
|
||||
expect(result).toContain("Execute this task.");
|
||||
});
|
||||
|
||||
it("accepts undefined memoryEnabled (default enabled) without error", () => {
|
||||
const task = createMockTaskDetail();
|
||||
const result = buildExecutionPrompt(task, "/project", {} as any);
|
||||
expect(result).toContain("Execute this task.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Import the summarizeToolArgs helper directly (not affected by mocks above)
|
||||
|
||||
@@ -2224,6 +2224,17 @@ git log --oneline
|
||||
commandsSection = "\n" + lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
// Build project memory section from settings
|
||||
// When enabled, agents consult and update .fusion/memory.md for durable project learnings.
|
||||
// Actual memory instructions will be injected by FN-810; this placeholder establishes
|
||||
// the conditional integration point.
|
||||
const memoryEnabled = settings?.memoryEnabled !== false;
|
||||
let memorySection = "";
|
||||
if (memoryEnabled && rootDir) {
|
||||
// TODO(FN-810): Call buildMemoryInstructions(rootDir) to populate memory context
|
||||
memorySection = "";
|
||||
}
|
||||
|
||||
// Build steering comments section (last 10 comments only to avoid context bloat)
|
||||
let steeringSection = "";
|
||||
if (task.steeringComments && task.steeringComments.length > 0) {
|
||||
@@ -2253,7 +2264,7 @@ ${task.dependencies.length > 0 ? `Dependencies: ${task.dependencies.join(", ")}`
|
||||
## PROMPT.md
|
||||
|
||||
${task.prompt}
|
||||
${attachmentsSection}${commandsSection}${progressSection}${steeringSection}
|
||||
${attachmentsSection}${commandsSection}${memorySection}${progressSection}${steeringSection}
|
||||
## Review level: ${reviewLevel}
|
||||
|
||||
${reviewLevel === 0 ? "No reviews required. Implement directly." : ""}
|
||||
|
||||
@@ -214,6 +214,52 @@ describe("buildSpecificationPrompt", () => {
|
||||
expect(prompt).toContain("If splitting: use the \\\`task_create\\\` tool");
|
||||
expect(prompt).not.toContain("## Subtask Consideration");
|
||||
});
|
||||
|
||||
describe("memoryEnabled setting", () => {
|
||||
it("accepts memoryEnabled: true without error", () => {
|
||||
const settings: Settings = {
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
memoryEnabled: true,
|
||||
};
|
||||
const prompt = buildSpecificationPrompt(
|
||||
baseTask,
|
||||
".fusion/tasks/KB-001/PROMPT.md",
|
||||
settings,
|
||||
);
|
||||
// Memory instructions are a placeholder until FN-810; just verify no crash
|
||||
expect(prompt).toContain("Specify this task");
|
||||
});
|
||||
|
||||
it("accepts memoryEnabled: false without error", () => {
|
||||
const settings: Settings = {
|
||||
maxConcurrent: 2,
|
||||
maxWorktrees: 4,
|
||||
pollIntervalMs: 10000,
|
||||
groupOverlappingFiles: false,
|
||||
autoMerge: true,
|
||||
memoryEnabled: false,
|
||||
};
|
||||
const prompt = buildSpecificationPrompt(
|
||||
baseTask,
|
||||
".fusion/tasks/KB-001/PROMPT.md",
|
||||
settings,
|
||||
);
|
||||
expect(prompt).toContain("Specify this task");
|
||||
});
|
||||
|
||||
it("accepts undefined memoryEnabled (default enabled) without error", () => {
|
||||
const prompt = buildSpecificationPrompt(
|
||||
baseTask,
|
||||
".fusion/tasks/KB-001/PROMPT.md",
|
||||
undefined,
|
||||
);
|
||||
expect(prompt).toContain("Specify this task");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("TRIAGE_SYSTEM_PROMPT", () => {
|
||||
|
||||
@@ -1193,6 +1193,17 @@ export function buildSpecificationPrompt(
|
||||
commandsSection = "\n\n" + lines.join("\n");
|
||||
}
|
||||
|
||||
// Build project memory section from settings.
|
||||
// When enabled, agents consult .fusion/memory.md for durable project learnings.
|
||||
// Actual memory instructions will be injected by FN-810; this placeholder
|
||||
// establishes the conditional integration point.
|
||||
const memoryEnabled = settings?.memoryEnabled !== false;
|
||||
let memorySection = "";
|
||||
if (memoryEnabled) {
|
||||
// TODO(FN-810): Call buildMemoryInstructions(rootDir) to populate memory context
|
||||
memorySection = "";
|
||||
}
|
||||
|
||||
let attachmentsSection = "";
|
||||
if (attachmentContents && attachmentContents.length > 0) {
|
||||
const parts = ["## Attachments", ""];
|
||||
@@ -1276,5 +1287,5 @@ ${task.dependencies.length > 0 ? `- **Dependencies:** ${task.dependencies.join("
|
||||
## Instructions
|
||||
${isRevision ? "1. Review the existing specification and user feedback carefully\n2. Revise the PROMPT.md to address the feedback while maintaining the structure\n3. Ensure the specification is detailed enough for an AI agent to execute" : "1. Read the project structure to understand context (package.json, source files, etc.)\n2. Write a complete PROMPT.md specification to the given path following the format in your system prompt\n3. The specification must be detailed enough for an autonomous AI agent to implement without asking questions\n4. Name actual files, functions, and patterns from the codebase — be specific"}
|
||||
|
||||
Use the write tool to write the specification file.${commandsSection}${attachmentsSection}`;
|
||||
Use the write tool to write the specification file.${commandsSection}${memorySection}${attachmentsSection}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user