feat(FN-3199): align memory insight docs with canonical paths
Updates three documentation files to align memory insight and plugin path references with their canonical locations. Fusion-Task-Id: FN-3199
This commit is contained in:
5
.changeset/fn-3199-memory-insights-path.md
Normal file
5
.changeset/fn-3199-memory-insights-path.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Store generated memory insight artifacts under `.fusion/memory/` (`memory-insights.md`, `memory-audit.md`, and `memory-audit-state.json`) instead of top-level `.fusion/` files, with compatibility migration for existing legacy files.
|
||||||
@@ -182,8 +182,8 @@ Use project memory for reusable patterns, constraints, and pitfalls that should
|
|||||||
Fusion can automatically extract insights from memory and prune transient content. Enable via `insightExtractionEnabled` setting:
|
Fusion can automatically extract insights from memory and prune transient content. Enable via `insightExtractionEnabled` setting:
|
||||||
|
|
||||||
- `.fusion/memory/MEMORY.md` — Canonical long-term memory source (inside the layered `.fusion/memory/` workspace) compacted/pruned by extraction jobs
|
- `.fusion/memory/MEMORY.md` — Canonical long-term memory source (inside the layered `.fusion/memory/` workspace) compacted/pruned by extraction jobs
|
||||||
- `.fusion/memory-insights.md` — Distilled insights output
|
- `.fusion/memory/memory-insights.md` — Distilled insights output
|
||||||
- `.fusion/memory-audit.md` — Audit report after each extraction (includes pruning outcome)
|
- `.fusion/memory/memory-audit.md` — Audit report after each extraction (includes pruning outcome)
|
||||||
|
|
||||||
See [Settings Reference](./settings-reference.md#background-memory-summarization--audit) for configuration details.
|
See [Settings Reference](./settings-reference.md#background-memory-summarization--audit) for configuration details.
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ Fusion currently has two related but distinct memory systems:
|
|||||||
| `MEMORY_DREAMS_FILENAME` | `DREAMS.md` | `memory-backend.ts` |
|
| `MEMORY_DREAMS_FILENAME` | `DREAMS.md` | `memory-backend.ts` |
|
||||||
| `DEFAULT_MEMORY_BACKEND` | `qmd` | `memory-backend.ts` |
|
| `DEFAULT_MEMORY_BACKEND` | `qmd` | `memory-backend.ts` |
|
||||||
| `MEMORY_WORKING_PATH` | `.fusion/memory/MEMORY.md` | `memory-insights.ts` |
|
| `MEMORY_WORKING_PATH` | `.fusion/memory/MEMORY.md` | `memory-insights.ts` |
|
||||||
| `MEMORY_INSIGHTS_PATH` | `.fusion/memory-insights.md` | `memory-insights.ts` |
|
| `MEMORY_INSIGHTS_PATH` | `.fusion/memory/memory-insights.md` | `memory-insights.ts` |
|
||||||
| `MEMORY_AUDIT_PATH` | `.fusion/memory-audit.md` | `memory-insights.ts` |
|
| `MEMORY_AUDIT_PATH` | `.fusion/memory/memory-audit.md` | `memory-insights.ts` |
|
||||||
|
|
||||||
### 1.3 Exported Surface (Post-Migration)
|
### 1.3 Exported Surface (Post-Migration)
|
||||||
|
|
||||||
@@ -264,8 +264,8 @@ Dashboard memory routes must remain rooted in project-scoped memory APIs:
|
|||||||
Insight extraction is a separate subsystem that currently uses:
|
Insight extraction is a separate subsystem that currently uses:
|
||||||
|
|
||||||
- Working source: `.fusion/memory/MEMORY.md`
|
- Working source: `.fusion/memory/MEMORY.md`
|
||||||
- Insight output: `.fusion/memory-insights.md`
|
- Insight output: `.fusion/memory/memory-insights.md`
|
||||||
- Audit output: `.fusion/memory-audit.md`
|
- Audit output: `.fusion/memory/memory-audit.md`
|
||||||
|
|
||||||
It is related to, but not equivalent to, backend selection and prompt instruction logic.
|
It is related to, but not equivalent to, backend selection and prompt instruction logic.
|
||||||
|
|
||||||
|
|||||||
@@ -848,15 +848,15 @@ Fusion can automatically extract insights from project memory and prune transien
|
|||||||
|
|
||||||
1. **Scheduled Extraction**: When `insightExtractionEnabled` is `true`, a background automation runs on the configured `insightExtractionSchedule` (default: daily at 2 AM).
|
1. **Scheduled Extraction**: When `insightExtractionEnabled` is `true`, a background automation runs on the configured `insightExtractionSchedule` (default: daily at 2 AM).
|
||||||
|
|
||||||
2. **AI-Powered Analysis**: The automation uses an AI agent to read canonical long-term memory (`.fusion/memory/MEMORY.md`) from the layered `.fusion/memory/` workspace plus `.fusion/memory-insights.md`, extract new insights, and produce a pruned working memory candidate.
|
2. **AI-Powered Analysis**: The automation uses an AI agent to read canonical long-term memory (`.fusion/memory/MEMORY.md`) from the layered `.fusion/memory/` workspace plus `.fusion/memory/memory-insights.md`, extract new insights, and produce a pruned working memory candidate.
|
||||||
|
|
||||||
3. **Insight Merging**: New insights are automatically merged into `.fusion/memory-insights.md` under the appropriate category (Patterns, Principles, Conventions, Pitfalls, Context). Duplicates are skipped.
|
3. **Insight Merging**: New insights are automatically merged into `.fusion/memory/memory-insights.md` under the appropriate category (Patterns, Principles, Conventions, Pitfalls, Context). Duplicates are skipped.
|
||||||
|
|
||||||
4. **Memory Pruning**: The AI agent also produces a pruned version of working memory containing only durable items:
|
4. **Memory Pruning**: The AI agent also produces a pruned version of working memory containing only durable items:
|
||||||
- **Preserved**: Architecture, Conventions, Pitfalls, Context sections with durable content
|
- **Preserved**: Architecture, Conventions, Pitfalls, Context sections with durable content
|
||||||
- **Pruned**: Task-specific notes, one-time observations, outdated entries
|
- **Pruned**: Task-specific notes, one-time observations, outdated entries
|
||||||
|
|
||||||
5. **Audit Report**: After each extraction run, a `.fusion/memory-audit.md` file is generated with:
|
5. **Audit Report**: After each extraction run, a `.fusion/memory/memory-audit.md` file is generated with:
|
||||||
- Working memory status (presence, size, sections)
|
- Working memory status (presence, size, sections)
|
||||||
- Insights memory status (insight counts by category)
|
- Insights memory status (insight counts by category)
|
||||||
- Last extraction results (success/failure, insight count, duplicates skipped)
|
- Last extraction results (success/failure, insight count, duplicates skipped)
|
||||||
@@ -870,8 +870,8 @@ Fusion can automatically extract insights from project memory and prune transien
|
|||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `.fusion/memory/MEMORY.md` | Long-term memory (updated when pruning is applied and validated) |
|
| `.fusion/memory/MEMORY.md` | Long-term memory (updated when pruning is applied and validated) |
|
||||||
| Legacy top-level memory file | Deprecated migration fallback (compatibility only; not canonical storage) |
|
| Legacy top-level memory file | Deprecated migration fallback (compatibility only; not canonical storage) |
|
||||||
| `.fusion/memory-insights.md` | Long-term insights distilled from working memory |
|
| `.fusion/memory/memory-insights.md` | Long-term insights distilled from working memory |
|
||||||
| `.fusion/memory-audit.md` | Human-readable audit report after each extraction |
|
| `.fusion/memory/memory-audit.md` | Human-readable audit report after each extraction |
|
||||||
|
|
||||||
### Settings Interaction
|
### Settings Interaction
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,18 @@ describe("memory-insights", () => {
|
|||||||
const result = await readInsightsMemory(tempDir);
|
const result = await readInsightsMemory(tempDir);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should migrate legacy top-level insights file on read", async () => {
|
||||||
|
const legacyPath = join(tempDir, ".fusion", "memory-insights.md");
|
||||||
|
const content = "# Legacy Insights\n\n- Keep this";
|
||||||
|
writeFileSync(legacyPath, content);
|
||||||
|
|
||||||
|
const result = await readInsightsMemory(tempDir);
|
||||||
|
|
||||||
|
expect(result).toBe(content);
|
||||||
|
expect(existsSync(join(tempDir, MEMORY_INSIGHTS_PATH))).toBe(true);
|
||||||
|
expect(existsSync(legacyPath)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── writeInsightsMemory ──────────────────────────────────────────────
|
// ── writeInsightsMemory ──────────────────────────────────────────────
|
||||||
@@ -98,6 +110,16 @@ describe("memory-insights", () => {
|
|||||||
await writeInsightsMemory(newDir, "test content");
|
await writeInsightsMemory(newDir, "test content");
|
||||||
expect(existsSync(join(newDir, MEMORY_INSIGHTS_PATH))).toBe(true);
|
expect(existsSync(join(newDir, MEMORY_INSIGHTS_PATH))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should remove legacy top-level insights file after canonical write", async () => {
|
||||||
|
const legacyPath = join(tempDir, ".fusion", "memory-insights.md");
|
||||||
|
writeFileSync(legacyPath, "legacy");
|
||||||
|
|
||||||
|
await writeInsightsMemory(tempDir, "canonical");
|
||||||
|
|
||||||
|
expect(readFileSync(join(tempDir, MEMORY_INSIGHTS_PATH), "utf-8")).toBe("canonical");
|
||||||
|
expect(existsSync(legacyPath)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── writeWorkingMemory ──────────────────────────────────────────────
|
// ── writeWorkingMemory ──────────────────────────────────────────────
|
||||||
@@ -613,7 +635,7 @@ describe("memory-insights", () => {
|
|||||||
describe("constants", () => {
|
describe("constants", () => {
|
||||||
it("should have correct file paths", () => {
|
it("should have correct file paths", () => {
|
||||||
expect(MEMORY_WORKING_PATH).toBe(".fusion/memory/MEMORY.md");
|
expect(MEMORY_WORKING_PATH).toBe(".fusion/memory/MEMORY.md");
|
||||||
expect(MEMORY_INSIGHTS_PATH).toBe(".fusion/memory-insights.md");
|
expect(MEMORY_INSIGHTS_PATH).toBe(".fusion/memory/memory-insights.md");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have sensible defaults", () => {
|
it("should have sensible defaults", () => {
|
||||||
@@ -657,6 +679,18 @@ describe("memory-insights audit file operations", () => {
|
|||||||
const result = await readMemoryAudit(tempDir);
|
const result = await readMemoryAudit(tempDir);
|
||||||
expect(result).toBe(content);
|
expect(result).toBe(content);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should migrate legacy top-level audit file on read", async () => {
|
||||||
|
const legacyPath = join(tempDir, ".fusion", "memory-audit.md");
|
||||||
|
const content = "# Legacy Audit";
|
||||||
|
writeFileSync(legacyPath, content);
|
||||||
|
|
||||||
|
const result = await readMemoryAudit(tempDir);
|
||||||
|
|
||||||
|
expect(result).toBe(content);
|
||||||
|
expect(existsSync(join(tempDir, MEMORY_AUDIT_PATH))).toBe(true);
|
||||||
|
expect(existsSync(legacyPath)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("writeMemoryAudit", () => {
|
describe("writeMemoryAudit", () => {
|
||||||
@@ -682,6 +716,16 @@ describe("memory-insights audit file operations", () => {
|
|||||||
await writeMemoryAudit(newDir, "test content");
|
await writeMemoryAudit(newDir, "test content");
|
||||||
expect(existsSync(join(newDir, MEMORY_AUDIT_PATH))).toBe(true);
|
expect(existsSync(join(newDir, MEMORY_AUDIT_PATH))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should remove legacy top-level audit file after canonical write", async () => {
|
||||||
|
const legacyPath = join(tempDir, ".fusion", "memory-audit.md");
|
||||||
|
writeFileSync(legacyPath, "legacy");
|
||||||
|
|
||||||
|
await writeMemoryAudit(tempDir, "canonical");
|
||||||
|
|
||||||
|
expect(readFileSync(join(tempDir, MEMORY_AUDIT_PATH), "utf-8")).toBe("canonical");
|
||||||
|
expect(existsSync(legacyPath)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1123,6 +1167,33 @@ describe("memory-insights audit generation", () => {
|
|||||||
expect(report.extraction.success).toBe(true);
|
expect(report.extraction.success).toBe(true);
|
||||||
expect(report.extraction.summary).toBe("Persisted extraction summary");
|
expect(report.extraction.summary).toBe("Persisted extraction summary");
|
||||||
expect(report.checks.find((c) => c.id === "recent-extraction")?.passed).toBe(true);
|
expect(report.checks.find((c) => c.id === "recent-extraction")?.passed).toBe(true);
|
||||||
|
expect(existsSync(join(tempDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates legacy top-level audit state to canonical memory directory", async () => {
|
||||||
|
const runAt = new Date().toISOString();
|
||||||
|
const legacyStatePath = join(tempDir, ".fusion", "memory-audit-state.json");
|
||||||
|
writeFileSync(
|
||||||
|
legacyStatePath,
|
||||||
|
JSON.stringify({
|
||||||
|
extraction: {
|
||||||
|
runAt,
|
||||||
|
success: true,
|
||||||
|
insightCount: 2,
|
||||||
|
duplicateCount: 0,
|
||||||
|
skippedCount: 0,
|
||||||
|
summary: "Legacy summary",
|
||||||
|
},
|
||||||
|
updatedAt: runAt,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const report = await generateMemoryAudit(tempDir);
|
||||||
|
|
||||||
|
expect(report.extraction.runAt).toBe(runAt);
|
||||||
|
expect(report.extraction.summary).toBe("Legacy summary");
|
||||||
|
expect(existsSync(join(tempDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
|
||||||
|
expect(existsSync(legacyStatePath)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should include pruning outcome in report", async () => {
|
it("should include pruning outcome in report", async () => {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
* - **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.
|
* pruning — agents are expected to keep it relevant.
|
||||||
*
|
*
|
||||||
* - **Insights memory** (`memory-insights.md`): Only grows through
|
* - **Insights memory** (`.fusion/memory/memory-insights.md`): Only grows through
|
||||||
* extraction. New insights are merged with existing ones. Simple duplicate
|
* extraction. New insights are merged with existing ones. Simple duplicate
|
||||||
* detection prevents re-adding the same insight.
|
* detection prevents re-adding the same insight.
|
||||||
*
|
*
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
* near-exact content match), it is skipped.
|
* near-exact content match), it is skipped.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
import { readFile, writeFile, mkdir, unlink } from "node:fs/promises";
|
||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import type { ProjectSettings } from "./types.js";
|
import type { ProjectSettings } from "./types.js";
|
||||||
@@ -76,13 +76,17 @@ import type { ScheduledTaskCreateInput } from "./automation.js";
|
|||||||
export const MEMORY_WORKING_PATH = ".fusion/memory/MEMORY.md";
|
export const MEMORY_WORKING_PATH = ".fusion/memory/MEMORY.md";
|
||||||
|
|
||||||
/** Path to insights memory relative to project root. */
|
/** Path to insights memory relative to project root. */
|
||||||
export const MEMORY_INSIGHTS_PATH = ".fusion/memory-insights.md";
|
export const MEMORY_INSIGHTS_PATH = ".fusion/memory/memory-insights.md";
|
||||||
|
|
||||||
/** Path to memory audit report relative to project root. */
|
/** Path to memory audit report relative to project root. */
|
||||||
export const MEMORY_AUDIT_PATH = ".fusion/memory-audit.md";
|
export const MEMORY_AUDIT_PATH = ".fusion/memory/memory-audit.md";
|
||||||
|
|
||||||
/** Path to persisted memory audit state (latest extraction/pruning metadata). */
|
/** Path to persisted memory audit state (latest extraction/pruning metadata). */
|
||||||
export const MEMORY_AUDIT_STATE_PATH = ".fusion/memory-audit-state.json";
|
export const MEMORY_AUDIT_STATE_PATH = ".fusion/memory/memory-audit-state.json";
|
||||||
|
|
||||||
|
const LEGACY_MEMORY_INSIGHTS_PATH = ".fusion/memory-insights.md";
|
||||||
|
const LEGACY_MEMORY_AUDIT_PATH = ".fusion/memory-audit.md";
|
||||||
|
const LEGACY_MEMORY_AUDIT_STATE_PATH = ".fusion/memory-audit-state.json";
|
||||||
|
|
||||||
/** Default cron schedule for insight extraction: daily at 2 AM. */
|
/** Default cron schedule for insight extraction: daily at 2 AM. */
|
||||||
export const DEFAULT_INSIGHT_SCHEDULE = "0 2 * * *";
|
export const DEFAULT_INSIGHT_SCHEDULE = "0 2 * * *";
|
||||||
@@ -254,17 +258,60 @@ export async function readWorkingMemory(rootDir: string): Promise<string> {
|
|||||||
return readFile(filePath, "utf-8");
|
return readFile(filePath, "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateLegacyArtifactIfNeeded(
|
||||||
|
rootDir: string,
|
||||||
|
canonicalPath: string,
|
||||||
|
legacyPath: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const canonicalFilePath = join(rootDir, canonicalPath);
|
||||||
|
const legacyFilePath = join(rootDir, legacyPath);
|
||||||
|
if (existsSync(canonicalFilePath) || !existsSync(legacyFilePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await readFile(legacyFilePath, "utf-8");
|
||||||
|
const canonicalDir = dirname(canonicalFilePath);
|
||||||
|
if (!existsSync(canonicalDir)) {
|
||||||
|
await mkdir(canonicalDir, { recursive: true });
|
||||||
|
}
|
||||||
|
await writeFile(canonicalFilePath, content, "utf-8");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await unlink(legacyFilePath);
|
||||||
|
} catch {
|
||||||
|
// Best effort cleanup; canonical write already preserves data.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeLegacyArtifactIfPresent(rootDir: string, legacyPath: string): Promise<void> {
|
||||||
|
const legacyFilePath = join(rootDir, legacyPath);
|
||||||
|
if (!existsSync(legacyFilePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await unlink(legacyFilePath);
|
||||||
|
} catch {
|
||||||
|
// Best effort cleanup; inability to delete should not block writes.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the insights memory file (`memory-insights.md`).
|
* Read the insights memory file (`.fusion/memory/memory-insights.md`).
|
||||||
*
|
*
|
||||||
* Returns `null` if the file does not exist, indicating that no insights
|
* Returns `null` if the file does not exist, indicating that no insights
|
||||||
* have been extracted yet. The caller should treat this as "no prior
|
* have been extracted yet. The caller should treat this as "no prior
|
||||||
* extraction" and pass `null` to `buildInsightExtractionPrompt()`.
|
* extraction" and pass `null` to `buildInsightExtractionPrompt()`.
|
||||||
*
|
*
|
||||||
|
* Legacy compatibility: transparently migrates `.fusion/memory-insights.md`
|
||||||
|
* to the canonical memory workspace path on first read.
|
||||||
|
*
|
||||||
* @param rootDir - Absolute path to the project root directory.
|
* @param rootDir - Absolute path to the project root directory.
|
||||||
* @returns The insights memory content, or null if not found.
|
* @returns The insights memory content, or null if not found.
|
||||||
*/
|
*/
|
||||||
export async function readInsightsMemory(rootDir: string): Promise<string | null> {
|
export async function readInsightsMemory(rootDir: string): Promise<string | null> {
|
||||||
|
await migrateLegacyArtifactIfNeeded(rootDir, MEMORY_INSIGHTS_PATH, LEGACY_MEMORY_INSIGHTS_PATH);
|
||||||
|
|
||||||
const filePath = join(rootDir, MEMORY_INSIGHTS_PATH);
|
const filePath = join(rootDir, MEMORY_INSIGHTS_PATH);
|
||||||
if (!existsSync(filePath)) {
|
if (!existsSync(filePath)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -273,20 +320,21 @@ export async function readInsightsMemory(rootDir: string): Promise<string | null
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the insights memory file (`memory-insights.md`).
|
* Write the insights memory file (`.fusion/memory/memory-insights.md`).
|
||||||
*
|
*
|
||||||
* 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 rootDir - Absolute path to the project root directory.
|
||||||
* @param content - The markdown content to write.
|
* @param content - The markdown content to write.
|
||||||
*/
|
*/
|
||||||
export async function writeInsightsMemory(rootDir: string, content: string): Promise<void> {
|
export async function writeInsightsMemory(rootDir: string, content: string): Promise<void> {
|
||||||
const filePath = join(rootDir, MEMORY_INSIGHTS_PATH);
|
const filePath = join(rootDir, MEMORY_INSIGHTS_PATH);
|
||||||
const dir = join(rootDir, ".fusion");
|
const dir = dirname(filePath);
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
await writeFile(filePath, content, "utf-8");
|
await writeFile(filePath, content, "utf-8");
|
||||||
|
await removeLegacyArtifactIfPresent(rootDir, LEGACY_MEMORY_INSIGHTS_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,7 +355,7 @@ export async function writeWorkingMemory(rootDir: string, content: string): Prom
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read the memory audit file (`memory-audit.md`).
|
* Read the memory audit file (`.fusion/memory/memory-audit.md`).
|
||||||
*
|
*
|
||||||
* Returns `null` if the file does not exist.
|
* Returns `null` if the file does not exist.
|
||||||
*
|
*
|
||||||
@@ -315,6 +363,8 @@ export async function writeWorkingMemory(rootDir: string, content: string): Prom
|
|||||||
* @returns The audit file content, or null if not found.
|
* @returns The audit file content, or null if not found.
|
||||||
*/
|
*/
|
||||||
export async function readMemoryAudit(rootDir: string): Promise<string | null> {
|
export async function readMemoryAudit(rootDir: string): Promise<string | null> {
|
||||||
|
await migrateLegacyArtifactIfNeeded(rootDir, MEMORY_AUDIT_PATH, LEGACY_MEMORY_AUDIT_PATH);
|
||||||
|
|
||||||
const filePath = join(rootDir, MEMORY_AUDIT_PATH);
|
const filePath = join(rootDir, MEMORY_AUDIT_PATH);
|
||||||
if (!existsSync(filePath)) {
|
if (!existsSync(filePath)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -323,28 +373,31 @@ export async function readMemoryAudit(rootDir: string): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the memory audit file (`memory-audit.md`).
|
* Write the memory audit file (`.fusion/memory/memory-audit.md`).
|
||||||
*
|
*
|
||||||
* 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 rootDir - Absolute path to the project root directory.
|
||||||
* @param content - The markdown content to write.
|
* @param content - The markdown content to write.
|
||||||
*/
|
*/
|
||||||
export async function writeMemoryAudit(rootDir: string, content: string): Promise<void> {
|
export async function writeMemoryAudit(rootDir: string, content: string): Promise<void> {
|
||||||
const filePath = join(rootDir, MEMORY_AUDIT_PATH);
|
const filePath = join(rootDir, MEMORY_AUDIT_PATH);
|
||||||
const dir = join(rootDir, ".fusion");
|
const dir = dirname(filePath);
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
await writeFile(filePath, content, "utf-8");
|
await writeFile(filePath, content, "utf-8");
|
||||||
|
await removeLegacyArtifactIfPresent(rootDir, LEGACY_MEMORY_AUDIT_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read persisted memory audit state (`memory-audit-state.json`).
|
* Read persisted memory audit state (`.fusion/memory/memory-audit-state.json`).
|
||||||
*
|
*
|
||||||
* Returns `null` when no prior state exists.
|
* Returns `null` when no prior state exists.
|
||||||
*/
|
*/
|
||||||
async function readMemoryAuditState(rootDir: string): Promise<MemoryAuditState | null> {
|
async function readMemoryAuditState(rootDir: string): Promise<MemoryAuditState | null> {
|
||||||
|
await migrateLegacyArtifactIfNeeded(rootDir, MEMORY_AUDIT_STATE_PATH, LEGACY_MEMORY_AUDIT_STATE_PATH);
|
||||||
|
|
||||||
const filePath = join(rootDir, MEMORY_AUDIT_STATE_PATH);
|
const filePath = join(rootDir, MEMORY_AUDIT_STATE_PATH);
|
||||||
if (!existsSync(filePath)) {
|
if (!existsSync(filePath)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -370,16 +423,17 @@ async function readMemoryAuditState(rootDir: string): Promise<MemoryAuditState |
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist memory audit state (`memory-audit-state.json`).
|
* Persist memory audit state (`.fusion/memory/memory-audit-state.json`).
|
||||||
*/
|
*/
|
||||||
async function writeMemoryAuditState(rootDir: string, state: MemoryAuditState): Promise<void> {
|
async function writeMemoryAuditState(rootDir: string, state: MemoryAuditState): Promise<void> {
|
||||||
const filePath = join(rootDir, MEMORY_AUDIT_STATE_PATH);
|
const filePath = join(rootDir, MEMORY_AUDIT_STATE_PATH);
|
||||||
const dir = join(rootDir, ".fusion");
|
const dir = dirname(filePath);
|
||||||
if (!existsSync(dir)) {
|
if (!existsSync(dir)) {
|
||||||
await mkdir(dir, { recursive: true });
|
await mkdir(dir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeFile(filePath, JSON.stringify(state, null, 2), "utf-8");
|
await writeFile(filePath, JSON.stringify(state, null, 2), "utf-8");
|
||||||
|
await removeLegacyArtifactIfPresent(rootDir, LEGACY_MEMORY_AUDIT_STATE_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidExtractionMetadata(value: unknown): value is MemoryExtractionMetadata {
|
function isValidExtractionMetadata(value: unknown): value is MemoryExtractionMetadata {
|
||||||
@@ -885,7 +939,7 @@ export function createInsightExtractionAutomation(
|
|||||||
## Instructions
|
## Instructions
|
||||||
|
|
||||||
1. Read the working memory file at \`.fusion/memory/MEMORY.md\` using your file reading tools
|
1. Read the working memory file at \`.fusion/memory/MEMORY.md\` using your file reading tools
|
||||||
2. Read the existing insights file at \`.fusion/memory-insights.md\` (it may not exist yet)
|
2. Read the existing insights file at \`.fusion/memory/memory-insights.md\` (it may not exist yet)
|
||||||
3. Analyze the working memory content and identify:
|
3. Analyze the working memory content and identify:
|
||||||
a) **New insights** that should be preserved in long-term memory
|
a) **New insights** that should be preserved in long-term memory
|
||||||
b) **Durable content** that should remain in working memory
|
b) **Durable content** that should remain in working memory
|
||||||
@@ -1330,7 +1384,7 @@ export async function generateMemoryAudit(
|
|||||||
id: "insights-memory-exists",
|
id: "insights-memory-exists",
|
||||||
name: "Insights memory file exists",
|
name: "Insights memory file exists",
|
||||||
passed: false,
|
passed: false,
|
||||||
details: "File .fusion/memory-insights.md does not exist yet",
|
details: "File .fusion/memory/memory-insights.md does not exist yet",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1957,7 +1957,7 @@ export interface ProjectSettings {
|
|||||||
/** When true, enables periodic AI-powered extraction of insights from working memory
|
/** When true, enables periodic AI-powered extraction of insights from working memory
|
||||||
* into a distilled long-term memory file. Creates an automation schedule that reads
|
* into a distilled long-term memory file. Creates an automation schedule that reads
|
||||||
* `.fusion/memory/MEMORY.md`, identifies patterns/principles/pitfalls, and writes to
|
* `.fusion/memory/MEMORY.md`, identifies patterns/principles/pitfalls, and writes to
|
||||||
* `.fusion/memory-insights.md`. Default: false. */
|
* `.fusion/memory/memory-insights.md`. Default: false. */
|
||||||
insightExtractionEnabled?: boolean;
|
insightExtractionEnabled?: boolean;
|
||||||
/** Cron expression for insight extraction schedule. Only used when
|
/** Cron expression for insight extraction schedule. Only used when
|
||||||
* insightExtractionEnabled is true. Default: "0 2 * * *" (daily at 2 AM). */
|
* insightExtractionEnabled is true. Default: "0 2 * * *" (daily at 2 AM). */
|
||||||
|
|||||||
@@ -2230,7 +2230,7 @@ describe("GET /api/memory/insights", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
rootDir = mkdtempSync(join(tmpdir(), "fusion-memory-insights-"));
|
rootDir = mkdtempSync(join(tmpdir(), "fusion-memory-insights-"));
|
||||||
mkdirSync(join(rootDir, ".fusion"), { recursive: true });
|
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
|
||||||
store = createMockStore({
|
store = createMockStore({
|
||||||
getRootDir: vi.fn().mockReturnValue(rootDir),
|
getRootDir: vi.fn().mockReturnValue(rootDir),
|
||||||
});
|
});
|
||||||
@@ -2248,8 +2248,8 @@ describe("GET /api/memory/insights", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it("returns 200 with content and exists:true when insights file exists", async () => {
|
it("returns 200 with content and exists:true when insights file exists", async () => {
|
||||||
// Insights file is at .fusion/memory-insights.md
|
// Insights file is at .fusion/memory/memory-insights.md
|
||||||
writeFileSync(join(rootDir, ".fusion", "memory-insights.md"), "## Patterns\n- Pattern 1\n- Pattern 2");
|
writeFileSync(join(rootDir, ".fusion", "memory", "memory-insights.md"), "## Patterns\n- Pattern 1\n- Pattern 2");
|
||||||
|
|
||||||
const res = await GET(buildApp(), "/api/memory/insights");
|
const res = await GET(buildApp(), "/api/memory/insights");
|
||||||
|
|
||||||
@@ -2303,8 +2303,8 @@ describe("PUT /api/memory/insights", () => {
|
|||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
expect(res.body).toHaveProperty("success", true);
|
expect(res.body).toHaveProperty("success", true);
|
||||||
|
|
||||||
// Verify file was written (insights file is .fusion/memory-insights.md)
|
// Verify file was written (insights file is .fusion/memory/memory-insights.md)
|
||||||
const insightsPath = join(rootDir, ".fusion", "memory-insights.md");
|
const insightsPath = join(rootDir, ".fusion", "memory", "memory-insights.md");
|
||||||
expect(existsSync(insightsPath)).toBe(true);
|
expect(existsSync(insightsPath)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2376,6 +2376,24 @@ describe("POST /api/memory/extract", () => {
|
|||||||
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
|
mkdirSync(join(rootDir, ".fusion", "memory"), { recursive: true });
|
||||||
writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Working memory content for extraction that is long enough.");
|
writeFileSync(join(rootDir, ".fusion", "memory", "MEMORY.md"), "Working memory content for extraction that is long enough.");
|
||||||
|
|
||||||
|
const session = {
|
||||||
|
state: {
|
||||||
|
messages: [] as Array<{ role: string; content: string }>,
|
||||||
|
},
|
||||||
|
prompt: vi.fn(async function (this: { state: { messages: Array<{ role: string; content: string }> } }) {
|
||||||
|
const response = JSON.stringify({
|
||||||
|
summary: "Extracted insights",
|
||||||
|
insights: [{ category: "pattern", content: "Persist reusable conventions" }],
|
||||||
|
prunedMemory: "## Architecture\n\nDurable architecture notes.",
|
||||||
|
});
|
||||||
|
this.state.messages.push({ role: "assistant", content: response });
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
vi.mocked(createFnAgent).mockResolvedValue({ session } as never);
|
||||||
|
|
||||||
const res = await REQUEST(
|
const res = await REQUEST(
|
||||||
buildApp(),
|
buildApp(),
|
||||||
"POST",
|
"POST",
|
||||||
@@ -2389,6 +2407,9 @@ describe("POST /api/memory/extract", () => {
|
|||||||
expect(res.body).toHaveProperty("summary");
|
expect(res.body).toHaveProperty("summary");
|
||||||
expect(res.body).toHaveProperty("insightCount");
|
expect(res.body).toHaveProperty("insightCount");
|
||||||
expect(res.body).toHaveProperty("pruned");
|
expect(res.body).toHaveProperty("pruned");
|
||||||
|
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-insights.md"))).toBe(true);
|
||||||
|
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit.md"))).toBe(true);
|
||||||
|
expect(existsSync(join(rootDir, ".fusion", "memory", "memory-audit-state.json"))).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user