feat(FN-2118): merge fusion/fn-2118

This commit is contained in:
Fusion
2026-04-19 03:06:37 -07:00
committed by gsxdsm
parent 11c5791622
commit a901975ed8
3 changed files with 234 additions and 553 deletions

View File

@@ -2,7 +2,7 @@
[← Docs index](./README.md)
This document defines the pluggable memory backend contract for Fusion, translating memory concepts into concrete interfaces that enable alternative storage backends while preserving all existing behavior.
This document is the source-of-truth contract for Fusion memory backend behavior after the FN-2087 migration series.
---
@@ -18,18 +18,23 @@ This document defines the pluggable memory backend contract for Fusion, translat
## 1. Current Fusion Memory Baseline
### 1.1 Core Memory Modules
### 1.1 Two Memory Systems (Distinct Responsibilities)
| Module | File | Responsibility |
|--------|------|----------------|
| `memory-backend.ts` | `packages/core/src/memory-backend.ts` | Canonical backend contract, built-in backends (`file`, `readonly`, `qmd`), backend registry/resolution, and file/get/search helpers |
| `project-memory.ts` | `packages/core/src/project-memory.ts` | Bootstrap, prompt instruction builders, backend-aware read/search/get wrappers, `resolveMemoryInstructionContext()` |
| `memory-insights.ts` | `packages/core/src/memory-insights.ts` | Scheduled insight extraction, merging, pruning validation/application, memory audit reporting |
Fusion currently has two related but distinct memory systems:
#### Exported constants (current code)
1. **Layered memory backend system** (`memory-backend.ts` + `project-memory.ts`)
- Handles agent-facing read/write/get/search for project memory
- Canonical layered workspace under `.fusion/memory/`
- Uses pluggable backends (`file`, `qmd`, `readonly`, custom)
2. **Insight extraction system** (`memory-insights.ts`)
- Runs scheduled extraction and pruning workflows
- Reads working memory and writes extracted insights/audit artifacts
- Independent automation lifecycle and settings
| Constant | Value | Source |
|----------|-------|--------|
### 1.2 Canonical Constants and Paths
| Constant | Value | Source Module |
|---|---|---|
| `MEMORY_WORKSPACE_PATH` | `.fusion/memory` | `memory-backend.ts` |
| `MEMORY_LONG_TERM_FILENAME` | `MEMORY.md` | `memory-backend.ts` |
| `MEMORY_DREAMS_FILENAME` | `DREAMS.md` | `memory-backend.ts` |
@@ -40,187 +45,91 @@ This document defines the pluggable memory backend contract for Fusion, translat
| `MEMORY_INSIGHTS_PATH` | `.fusion/memory-insights.md` | `memory-insights.ts` |
| `MEMORY_AUDIT_PATH` | `.fusion/memory-audit.md` | `memory-insights.ts` |
#### Key runtime interfaces/signatures (current code)
`MEMORY_FILE_PATH` and `MEMORY_WORKING_PATH` currently resolve to the same canonical long-term file (`.fusion/memory/MEMORY.md`) but are owned by different modules for different concerns (backend/prompt plumbing vs insight extraction workflow).
```typescript
// memory-backend.ts
export interface MemoryBackend {
readonly type: string;
readonly name: string;
readonly capabilities: MemoryBackendCapabilities;
read(rootDir: string): Promise<MemoryReadResult>;
write(rootDir: string, content: string): Promise<MemoryWriteResult>;
get?(rootDir: string, options: MemoryGetOptions): Promise<MemoryGetResult>;
search?(rootDir: string, options: MemorySearchOptions): Promise<MemorySearchResult[]>;
exists?(rootDir: string): Promise<boolean>;
}
### 1.3 Exported Surface (Post-Migration)
export function resolveMemoryBackend(settings?: { memoryBackendType?: string }): MemoryBackend;
#### `project-memory.ts` exports
// project-memory.ts
export function resolveMemoryInstructionContext(settings?: { memoryEnabled?: boolean; memoryBackendType?: string }): MemoryInstructionContext;
export function buildTriageMemoryInstructions(rootDir: string, settings?: { memoryEnabled?: boolean; memoryBackendType?: string }): string;
export function buildExecutionMemoryInstructions(rootDir: string, settings?: { memoryEnabled?: boolean; memoryBackendType?: string }): string;
export function buildReviewerMemoryInstructions(rootDir: string, settings?: { memoryEnabled?: boolean; memoryBackendType?: string }): string;
```
| Export | Purpose |
|---|---|
| `MEMORY_FILE_PATH`, `memoryFilePath()` | Canonical long-term path helpers |
| `getDefaultMemoryScaffold()` | Default long-term scaffold content |
| `ensureMemoryFile()` | Filesystem bootstrap for canonical memory |
| `ensureMemoryFileWithBackend()` | Backend-aware bootstrap |
| `readProjectMemoryWithBackend()` | Backend-aware read helper |
| `searchProjectMemory()`, `getProjectMemory()` | Backend-aware search/get wrappers |
| `resolveMemoryInstructionContext()` | Backend-aware instruction context |
| `buildTriageMemoryInstructions()` | Triage prompt memory instructions |
| `buildExecutionMemoryInstructions()` | Executor prompt memory instructions |
| `buildReviewerMemoryInstructions()` | Reviewer prompt memory instructions |
| `readProjectMemory()` | Direct canonical long-term file read |
### 1.2 Settings
#### `memory-backend.ts` key exports
**Files:**
- `packages/core/src/types.ts` (`ProjectSettings` fields)
- `packages/core/src/settings-schema.ts` (defaults)
| Export | Purpose |
|---|---|
| `MemoryBackend`, `MemoryBackendCapabilities` | Runtime backend contract |
| `MemoryBackendError`, `MemoryBackendErrorCode` | Typed backend error model |
| `FileMemoryBackend`, `QmdMemoryBackend`, `ReadOnlyMemoryBackend` | Built-in backend implementations |
| `registerMemoryBackend()`, `getMemoryBackend()`, `listMemoryBackendTypes()` | Function-based registry API |
| `resolveMemoryBackend()` | Backend resolution from settings |
| `memoryWorkspacePath()`, `memoryLongTermPath()`, `dailyMemoryPath()`, `memoryDreamsPath()` | Canonical layered path helpers |
| `ensureOpenClawMemoryFiles()` | Layered file bootstrap + legacy seeding |
| `listProjectMemoryFiles()`, `readProjectMemoryFile()`, `writeProjectMemoryFile()` | Validated layered file operations |
```typescript
interface ProjectSettings {
memoryEnabled?: boolean; // default true
memoryBackendType?: string; // default "qmd"
insightExtractionEnabled?: boolean; // default false
insightExtractionSchedule?: string; // default "0 2 * * *"
insightExtractionMinIntervalMs?: number; // default 86400000
}
```
### 1.4 Settings Baseline
**Current defaults:**
- `memoryEnabled`: `true`
- `memoryBackendType`: `"qmd"`
- `insightExtractionEnabled`: `false`
- `insightExtractionSchedule`: `"0 2 * * *"`
- `insightExtractionMinIntervalMs`: `86400000`
`ProjectSettings` fields that govern memory behavior:
### 1.3 Store Bootstrap + Toggle Behavior
- `memoryEnabled?: boolean` (default `true`)
- `memoryBackendType?: string` (default `"qmd"`)
- `insightExtractionEnabled?: boolean` (default `false`)
- `insightExtractionSchedule?: string` (default `"0 2 * * *"`)
- `insightExtractionMinIntervalMs?: number` (default `86400000`)
**File:** `packages/core/src/store.ts`
Runtime backend resolution uses an internal `MemorySettings` shape with `memoryBackendType` and optional additional keys.
- On store init, when `memoryEnabled !== false`, the store calls backend-aware memory bootstrap (`ensureMemoryFileWithBackend(this.rootDir, mergedSettings)`).
- On settings updates, when memory toggles from disabled → enabled, bootstrap is triggered again (idempotent).
- Bootstrap failures are non-fatal (wrapped in try/catch and logged); startup/settings flows continue.
### 1.5 Key Invariants
### 1.4 Engine Prompt Wiring
- Triage (`packages/engine/src/triage.ts`) appends `buildTriageMemoryInstructions(...)` when `memoryEnabled !== false`.
- Executor (`packages/engine/src/executor.ts`) appends `buildExecutionMemoryInstructions(...)` when `memoryEnabled !== false`.
- Reviewer prompt builders use `buildReviewerMemoryInstructions(...)` with the same backend-aware context branching.
Prompt wording is selected by `resolveMemoryInstructionContext(settings?)`:
- `file` backend → explicit path hint (`.fusion/memory/MEMORY.md`)
- `qmd` backend (default) → no file-path hardcoding in instructions
- `readonly` backend → read-only instruction set
### 1.5 Dashboard Routes
**File:** `packages/dashboard/src/routes.ts`
| Route | Method | Description |
|-------|--------|-------------|
| `/api/memory` | GET | Returns `{ content: string }` from project memory (empty string if absent) |
| `/api/memory` | PUT | Body `{ content: string }`; writes memory content |
Route handlers are still file-service based, but operate on OpenClaw-style canonical long-term memory path (`.fusion/memory/MEMORY.md`) with migration compatibility for legacy `.fusion/memory.md` seeding/alias handling.
### 1.6 Dashboard Settings UI
**File:** `packages/dashboard/app/components/SettingsModal.tsx`
- Exposes `memoryEnabled` toggle
- Exposes `memoryBackendType` selection for available backends
- Loads/saves memory content via `/api/memory`
- Reflects memory-disabled state in editor controls
### 1.7 Existing Tests
| Test File | Coverage |
|-----------|----------|
| `packages/core/src/memory-backend.test.ts` | Backend interface behavior, built-in backends, registry helpers, layered path helpers/bootstrap (`ensureOpenClawMemoryFiles`), `DEFAULT_MEMORY_BACKEND = "qmd"`, backend resolution/fallback |
| `packages/core/src/project-memory.test.ts` | `MEMORY_FILE_PATH`, bootstrap behavior, backend-aware instruction context and prompt content |
| `packages/core/src/store.test.ts` | Init/toggle bootstrap behavior for enabled/disabled memory states |
| `packages/engine/src/triage.test.ts` | `memoryEnabled` and `memoryBackendType` influence on triage prompt injection |
| `packages/engine/src/executor.test.ts` | `memoryEnabled` and `memoryBackendType` influence on executor prompt injection |
| `packages/dashboard/app/components/SettingsModal.test.tsx` | Settings UI toggles for memory controls |
### 1.8 Summary of Non-Negotiable Behaviors
1. **Canonical layout**: OpenClaw-style memory workspace is canonical (`.fusion/memory/` with `MEMORY.md`, daily files, and `DREAMS.md`).
2. **Legacy compatibility**: Legacy `.fusion/memory.md` is a **deprecated legacy fallback** (migration seed / accepted alias), not canonical storage.
3. **Backend default**: Memory backend default is `qmd`.
4. **Backend selector key**: `memoryBackendType` controls backend selection.
5. **Toggle gate**: `memoryEnabled: false` disables memory prompt injection regardless of backend type.
6. **Bootstrap guarantees**: Memory bootstrap is idempotent and non-fatal; existing memory is not overwritten.
7. **Prompt context is backend-aware**: Instruction path hints and write directives vary by backend capabilities/type.
1. Canonical layered long-term memory file is **`.fusion/memory/MEMORY.md`**.
2. Legacy **`.fusion/memory.md`** is compatibility-only (migration seed/legacy alias), not canonical storage.
3. Default backend is **`qmd`** (`DEFAULT_MEMORY_BACKEND`).
4. Backend selection key is **`memoryBackendType`**.
5. Prompt instruction context is backend-aware (`file` path hint vs `qmd`/`readonly` behavior).
6. Dashboard `/api/memory` routes are backend-aware; layered file routes validate requests against allowed memory workspace files.
---
## 2. OpenClaw Research Findings
> **Provenance:** OpenClaw / pi-style memory patterns were used as design input. This section documents what Fusion adopted versus what remained conceptual.
>
> **Update (2026-04-19):** Fusion now uses a layered OpenClaw-style workspace as canonical storage:
> - `.fusion/memory/MEMORY.md` — curated long-term memory
> - `.fusion/memory/YYYY-MM-DD.md` — append-only daily notes
> - `.fusion/memory/DREAMS.md` — synthesized patterns
> - `.fusion/memory.md` — deprecated legacy fallback (migration compatibility only)
Fusion adopted OpenClaw-style layered memory while keeping a concrete TypeScript runtime contract.
### 2.1 OpenClaw Memory Concepts
### 2.1 What Was Adopted
#### 2.1.1 Layered memory workspace
- Layered workspace (`MEMORY.md`, daily files, `DREAMS.md`)
- Backend abstraction with explicit capability declarations
- Search over layered files with bounded snippets
- Migration safety via compatibility seeding from legacy file
OpenClaw-style memory emphasizes a layered workspace rather than a single flat file. Fusion has adopted that model:
- Long-term memory: `.fusion/memory/MEMORY.md`
- Daily notes: `.fusion/memory/YYYY-MM-DD.md`
- Dream synthesis: `.fusion/memory/DREAMS.md`
Legacy `.fusion/memory.md` is retained only as a deprecated legacy fallback for migration compatibility.
#### 2.1.2 Pluggable backend abstraction
OpenClaw-style systems use backend abstraction so memory operations are not hardwired to one store. Fusion now implements this directly in `memory-backend.ts` with:
- `MemoryBackend` interface
- built-in backends (`file`, `readonly`, `qmd`)
- registry helpers and runtime resolution via settings
#### 2.1.3 Search over layered memory
OpenClaw-inspired retrieval patterns favor bounded snippets from multiple memory files. Fusion implements this with:
- backend `search?(rootDir, options)` hooks
- qmd-backed search (`QmdMemoryBackend`) when qmd is available
- local file-search fallback over the `.fusion/memory/` workspace
#### 2.1.4 Write behavior and durability
OpenClaw literature often discusses flush-style semantics; Fusion currently uses direct writes:
- `FileMemoryBackend` performs atomic file writes (temp file + rename)
- `QmdMemoryBackend` delegates writes to file backend, then schedules qmd refresh
- no explicit `flush()`/`shutdown()` lifecycle contract is currently required
### 2.2 Fusion Implications
### 2.2 Implications Table
| OpenClaw Concept | Fusion Current State | Contract Implication |
|-----------------|---------------------|----------------------|
| Layered memory files | Implemented (`MEMORY.md`, daily files, `DREAMS.md`) | Canonical path model is `.fusion/memory/` workspace, not a single flat file |
| Legacy compatibility | Retained only as a deprecated migration fallback/alias | Keep `.fusion/memory.md` references compatibility-scoped, not canonical |
| Pluggable backends | Implemented (registry + built-ins) | Contract must document real runtime API (`type`, `capabilities`, `read/write/get/search/exists`) |
| QMD backend | Implemented and default | Document `qmd` as default backend and describe qmd→local fallback behavior |
| Search capability | Implemented (`file` + `qmd`) | Contract search section must match real `search(rootDir, options)` signature and result shape |
| Backend resolution | Implemented via `memoryBackendType` | Contract/settings docs must use `memoryBackendType` and default `qmd` |
### 2.3 OpenClaw Memory Architecture Sources
1. OpenClaw/pi memory layout conventions (layered long-term + daily + synthesis files)
2. Existing Fusion implementation in `memory-backend.ts`, `project-memory.ts`, and `memory-insights.ts`
3. QMD integration pattern already shipped in Fusion (`QmdMemoryBackend`, qmd refresh/install helpers)
|---|---|---|
| Layered memory files | Implemented under `.fusion/memory/` | Canonical source-of-truth is workspace-based, not single-file |
| Path abstraction | Implemented via backend methods that receive `rootDir` (`read(rootDir)`, `write(rootDir, ...)`, etc.) | Backends must resolve paths relative to project root; no global hardcoded absolute paths |
| Pluggable backends | Implemented (`file`, `qmd`, `readonly`, custom) | Contract must document `MemoryBackend` exactly as shipped |
| Capability negotiation | Implemented via boolean-struct `capabilities` | No enum or lifecycle-based capability API |
| Search | Implemented through optional `search(rootDir, options)` hooks | Results are bounded snippets, not full-document dumps |
| Migration compatibility | Implemented via `ensureOpenClawMemoryFiles()` and legacy constants | Legacy `.fusion/memory.md` remains compatibility-only |
---
## 3. Fusion Memory Plugin Contract
### 3.1 Interface Definition
### 3.1 Runtime Interface (Canonical)
The canonical runtime contract lives in `packages/core/src/memory-backend.ts`.
```typescript
```ts
export interface MemoryBackendCapabilities {
readable: boolean;
writable: boolean;
@@ -229,46 +138,6 @@ export interface MemoryBackendCapabilities {
persistent: boolean;
}
export interface MemoryReadResult {
content: string;
exists: boolean;
backend: string;
}
export interface MemoryWriteResult {
success: boolean;
backend: string;
}
export interface MemoryGetOptions {
path: string;
startLine?: number;
lineCount?: number;
}
export interface MemoryGetResult {
path: string;
content: string;
startLine: number;
endLine: number;
totalLines: number;
backend: string;
}
export interface MemorySearchOptions {
query: string;
limit?: number;
}
export interface MemorySearchResult {
path: string;
lineStart: number;
lineEnd: number;
snippet: string;
score: number;
backend: string;
}
export interface MemoryBackend {
readonly type: string;
readonly name: string;
@@ -280,6 +149,17 @@ export interface MemoryBackend {
exists?(rootDir: string): Promise<boolean>;
}
export interface MemoryBackendConfig {
type: string;
options?: Record<string, unknown>;
}
```
This is the active contract. The following are **not** part of current runtime semantics: `initialize()`, `hasCapability()`, `flush()`, `shutdown()`, class-based registry APIs, or lifecycle interfaces.
### 3.2 Error Contract
```ts
export type MemoryBackendErrorCode =
| "NOT_FOUND"
| "READ_ONLY"
@@ -291,420 +171,184 @@ export type MemoryBackendErrorCode =
| "BACKEND_UNAVAILABLE";
export class MemoryBackendError extends Error {
constructor(code: MemoryBackendErrorCode, message: string, backend: string);
readonly code: MemoryBackendErrorCode;
readonly backend: string;
}
```
**Important contract clarifications:**
- The backend identity field is `type` (not `id`).
- Capabilities are declared via `capabilities` object (no `hasCapability()` method).
- Backends are stateless from caller perspective; methods receive `rootDir` per call.
- The legacy `MemoryBackendConfig` type is still exported but is not the active runtime selection API.
### 3.3 Built-In Backends
### 3.2 Capability Negotiation
| Backend Class | Type | Current Behavior |
|---|---|---|
| `FileMemoryBackend` | `file` | Reads/writes canonical `.fusion/memory/MEMORY.md`; supports `exists/get/search`; atomic writes via temp file rename |
| `QmdMemoryBackend` | `qmd` | Delegates read/write to file backend; schedules qmd refresh; uses qmd search first, local layered search fallback |
| `ReadOnlyMemoryBackend` | `readonly` | Read-only; `write()` throws `MemoryBackendError("READ_ONLY", ...)`; `search()` returns empty |
Fusion uses declarative capability flags on each backend instance:
| Capability | Meaning |
|-----------|---------|
| `readable` | Backend can return project memory content |
| `writable` | Backend accepts memory writes |
| `supportsAtomicWrite` | Backend can provide atomic update semantics |
| `hasConflictResolution` | Backend handles concurrent write conflicts internally |
| `persistent` | Data survives process/session restarts |
Call sites inspect `backend.capabilities` directly (for example, write paths reject non-writable backends).
### 3.3 Backend Registry
Current implementation is a module-level registry, not a class/factory API:
```typescript
const backendRegistry = new Map<string, MemoryBackend>();
### 3.4 Registry Contract (Function-Based)
```ts
registerMemoryBackend(backend: MemoryBackend): void
getMemoryBackend(type: string): MemoryBackend | undefined
listMemoryBackendTypes(): string[]
resolveMemoryBackend(settings?: { memoryBackendType?: string }): MemoryBackend
```
Built-ins are registered at module load:
- `file``FileMemoryBackend`
- `readonly``ReadOnlyMemoryBackend`
- `qmd``QmdMemoryBackend`
Backends are stored in a module-level `Map<string, MemoryBackend>`. Built-ins are registered at module load.
### 3.4 FileMemoryBackend
### 3.5 Resolution Contract
`FileMemoryBackend` is the filesystem implementation for the canonical long-term file.
- `type`: `file`
- `name`: `File (.fusion/memory/MEMORY.md)`
- Reads/writes: `.fusion/memory/MEMORY.md`
- Write behavior: atomic temp-file + rename
- Supports: `read`, `write`, `exists`, `get`, `search`
Legacy compatibility behavior tied to file backend ecosystem:
- Legacy `.fusion/memory.md` is retained as a deprecated compatibility constant (`LEGACY_MEMORY_FILE_PATH`)
- OpenClaw bootstrap (`ensureOpenClawMemoryFiles`) can seed `MEMORY.md` from legacy file on first migration
- `get` path normalization recognizes legacy alias paths, but canonical writes remain under `.fusion/memory/`
### 3.5 QmdMemoryBackend and Backend Resolution
`QmdMemoryBackend` is the default backend.
- `type`: `qmd`
- Delegates file read/write to `FileMemoryBackend`
- Schedules background qmd refresh on writes
- Uses qmd search first, then falls back to local layered-file search
Backend selection is resolved via settings key `memoryBackendType`:
```typescript
```ts
export const MEMORY_BACKEND_SETTINGS_KEYS = {
MEMORY_BACKEND_TYPE: "memoryBackendType",
} as const;
export const DEFAULT_MEMORY_BACKEND = "qmd";
export function resolveMemoryBackend(settings?: { memoryBackendType?: string }): MemoryBackend {
const backendType = settings?.memoryBackendType || DEFAULT_MEMORY_BACKEND;
return backendRegistry.get(backendType) || backendRegistry.get(DEFAULT_MEMORY_BACKEND)!;
}
export function resolveMemoryBackend(settings?: { memoryBackendType?: string }): MemoryBackend
```
Resolution chain:
1. Explicit `memoryBackendType` if registered
2. Fallback to default backend (`qmd`)
1. Configured `memoryBackendType` (if registered)
2. `DEFAULT_MEMORY_BACKEND` (`"qmd"`)
### 3.6 Lifecycle Hooks
### 3.6 Layered Memory Operations
There is currently **no lifecycle hook contract** (`initialize`, `flush`, `shutdown`, `onInit`, etc.) in the shipped backend API.
The layered memory workspace helpers in `memory-backend.ts` provide:
Backends are long-lived instances registered in memory, and lifecycle concerns are handled externally by call sites and process lifecycle.
- Workspace bootstrap: `ensureOpenClawMemoryFiles()`
- File listing: `listProjectMemoryFiles()`
- Validated file access: `readProjectMemoryFile()`, `writeProjectMemoryFile()`
- Path helpers: `memoryLongTermPath()`, `dailyMemoryPath()`, `memoryDreamsPath()`
If lifecycle hooks are introduced later, they should be treated as future contract work (not current behavior).
Allowed workspace files are constrained to:
### 3.7 Settings Extension
- `.fusion/memory/MEMORY.md`
- `.fusion/memory/DREAMS.md`
- `.fusion/memory/YYYY-MM-DD.md`
Current memory-related settings contract:
### 3.7 Prompt Instruction Context Contract
```typescript
interface ProjectSettings {
memoryEnabled?: boolean;
memoryBackendType?: string; // file | readonly | qmd | custom
insightExtractionEnabled?: boolean;
insightExtractionSchedule?: string;
insightExtractionMinIntervalMs?: number;
}
```
`resolveMemoryInstructionContext(settings?)` in `project-memory.ts` currently resolves as:
Notes:
- `memoryBackendType` is the active backend selector key.
- Legacy selector/config key names from pre-migration drafts are design artifacts and should not be documented as current settings APIs.
- Defaults are defined in `settings-schema.ts` (`memoryEnabled: true`, `memoryBackendType: "qmd"`).
| Condition | `backendType` | `instructionPathHint` | Behavior |
|---|---|---|---|
| `memoryEnabled === false` | `disabled` | `null` | No memory instructions |
| `memoryBackendType === "file"` | `file` | `.fusion/memory/MEMORY.md` | Explicit path-aware read/write instructions |
| `memoryBackendType === "readonly"` | `readonly` | `null` | Read-only instruction set |
| `memoryBackendType === "qmd"` or unknown | `qmd` | `null` | Backend-aware generic instructions |
### 3.8 File Compatibility & Source-of-Truth Semantics
#### 3.8.1 Canonical source-of-truth
- Canonical long-term memory path: `.fusion/memory/MEMORY.md`
- Canonical workspace root: `.fusion/memory/`
- Legacy `.fusion/memory.md` is a deprecated legacy fallback (migration/alias context)
- There is no persistent dual-write mirror contract in the current runtime API; migration compatibility is handled via bootstrap seeding and legacy path alias handling.
- Canonical layered workspace: `.fusion/memory/`
- Canonical long-term file: `.fusion/memory/MEMORY.md`
- Legacy `.fusion/memory.md`: compatibility-only for migration/legacy alias handling
#### 3.8.2 Return-shape compatibility
#### 3.8.2 Legacy compatibility scope
| Function | Required behavior |
|----------|-------------------|
| `readProjectMemory(rootDir)` | Returns empty string (`""`) when long-term memory is absent |
| `readWorkingMemory(rootDir)` | Returns empty string (`""`) when working memory is absent |
| `readInsightsMemory(rootDir)` | Returns `null` when insights file is absent |
| `GET /api/memory` | Returns `{ content: string }`, using empty string when absent |
Legacy path preservation is limited to compatibility behavior:
#### 3.8.3 File-service constraints
- `LEGACY_MEMORY_FILE_PATH` constant
- Migration seeding in `ensureOpenClawMemoryFiles()` when legacy exists and canonical long-term file is missing
- Transition/alias handling in layered-path validation
Dashboard file APIs and backend-aware helpers must remain project-scoped:
- Long-term memory writes target `.fusion/memory/MEMORY.md`
- Operations must stay inside project root boundary
- Text content is UTF-8 markdown
No legacy dual-write mirror is part of the current contract.
#### 3.8.4 Insights file handling
#### 3.8.3 Dashboard and API path constraints
Insights storage remains file-based today:
- Working memory: `.fusion/memory/MEMORY.md`
- Insights: `.fusion/memory-insights.md`
- Audit: `.fusion/memory-audit.md`
Dashboard memory routes must remain rooted in project-scoped memory APIs:
#### 3.8.5 Prompt instruction compatibility
- `/api/memory` uses backend-aware read/write (`readMemory`, `writeMemory`)
- `/api/memory/file` uses validated layered file operations (`MEMORY.md`, `DREAMS.md`, daily files)
- Read-only backends must reject writes with `READ_ONLY`
`resolveMemoryInstructionContext(settings?)` controls instruction shape:
#### 3.8.4 Prompt compatibility matrix
| Backend Type | `instructionPathHint` | Instruction behavior |
|--------------|------------------------|----------------------|
| `file` | `.fusion/memory/MEMORY.md` | Explicit path-oriented read/write instructions |
| `qmd` (default) | `null` | Backend-aware generic instructions without hardcoded path |
| `readonly` | `null` | Read-only memory instructions (no write directives) |
| `disabled` (`memoryEnabled: false`) | `null` | No memory instructions injected |
| Backend | Read behavior | Write behavior | Instruction style |
|---|---|---|---|
| `qmd` (default) | Delegated file read | Delegated file write + qmd refresh schedule | Generic backend-aware instructions, no fixed path hint |
| `file` | Direct canonical file read | Direct canonical file write | Explicit `.fusion/memory/MEMORY.md` path hint |
| `readonly` | Empty/non-throw read semantics | Throws `READ_ONLY` | Read-only instructions only |
#### 3.8.6 Must-not-break invariants
#### 3.8.5 Insight extraction relationship
1. Memory remains accessible through OpenClaw canonical long-term path (`.fusion/memory/MEMORY.md`) in file-backed flows, and the built-in `file` backend remains available for explicit fallback/use.
2. Legacy `.fusion/memory.md` remains a deprecated legacy fallback and is not reintroduced as canonical storage.
3. Instruction behavior is backend-dependent; file backend can include path hints, qmd/readonly do not require one.
4. `memoryEnabled: false` suppresses memory prompt injection regardless of backend type.
5. Read semantics remain stable (`""` for missing working memory, `null` for missing insights memory).
Insight extraction is a separate subsystem that currently uses:
#### 3.8.7 OpenClaw Layered Memory Layout
- Working source: `.fusion/memory/MEMORY.md`
- Insight output: `.fusion/memory-insights.md`
- Audit output: `.fusion/memory-audit.md`
Canonical project memory uses a directory-based layout under `.fusion/memory/`:
| File | Purpose | Layer |
|------|---------|-------|
| `.fusion/memory/MEMORY.md` | Curated long-term memory (durable decisions, conventions, pitfalls) | long-term |
| `.fusion/memory/YYYY-MM-DD.md` | Append-only daily notes (running observations, open loops) | daily |
| `.fusion/memory/DREAMS.md` | Synthesized patterns/themes from daily notes | dreams |
| `.fusion/memory.md` | **Deprecated** legacy fallback (migration compatibility only) | legacy |
Migration behavior (`ensureOpenClawMemoryFiles()`):
- When `.fusion/memory/MEMORY.md` does not exist but `.fusion/memory.md` does, legacy content is seeded into the new canonical file
- Once `.fusion/memory/MEMORY.md` exists, canonical writes stay in `.fusion/memory/` (no legacy re-canonicalization)
- Legacy paths may still appear as accepted aliases during transition, but are not canonical storage
It is related to, but not equivalent to, backend selection and prompt instruction logic.
---
## 4. Migration Strategy + Compatibility Guardrails
### 4.1 Phased Migration Plan
### 4.1 Migration Phase Status
The migration work has already progressed beyond the original FN-1418/FN-1419/FN-1420 plan.
| Phase | Task(s) | Status |
|---|---|---|
| Core backend contract + built-ins | FN-1418 | Complete |
| Engine backend-aware prompt integration | FN-1419 | Complete |
| Dashboard backend-aware memory integration | FN-1420 | Complete |
| Path/backend reconciliation umbrella | FN-2087 | Complete |
| Reconciliation implementation slices | FN-2131, FN-2132, FN-2133, FN-2134 | Complete |
| Phase | Task(s) | Status | Description |
|-------|---------|--------|-------------|
| 1 | FN-1418 | Complete | Core backend contract and built-ins landed in `@fusion/core` |
| 2 | FN-1419 | Complete | Engine prompt paths moved to backend-aware instruction generation |
| 3 | FN-1420 | Complete | Dashboard memory flows integrated with backend-aware memory model |
| 4 | FN-2087 | Active umbrella | Reconcile remaining migration edges and legacy-path consistency across runtime/docs/tests |
| 5 | FN-2131FN-2134 | Active/queued implementation phases | Execute remaining migration slices under FN-2087 |
### 4.2 Must-Not-Break Invariants
### 4.2 Compatibility Matrix
1. Canonical long-term layered memory remains `.fusion/memory/MEMORY.md`.
2. Legacy `.fusion/memory.md` remains compatibility-only and must not become canonical again.
3. Runtime backend selection remains keyed by `memoryBackendType`.
4. `DEFAULT_MEMORY_BACKEND` remains `"qmd"` unless explicitly changed in code + docs.
5. Prompt instruction behavior remains backend-dependent via `resolveMemoryInstructionContext()`.
6. Missing working-memory reads degrade gracefully to empty string where contract requires.
#### Settings
### 4.3 Resolution Fallback Chain
| Setting | Current behavior | Compatibility expectation |
|---------|------------------|---------------------------|
| `memoryEnabled` | Default `true` | Preserved |
| `memoryEnabled: false` | Suppresses memory prompt injection | Preserved |
| `memoryBackendType` | Default `"qmd"`; supports `file`, `readonly`, `qmd`, custom registered types | Preserved |
| Unknown `memoryBackendType` | Falls back to default backend (`qmd`) | Preserved |
| Insight extraction settings | `insightExtractionEnabled`, `insightExtractionSchedule`, `insightExtractionMinIntervalMs` | Preserved |
1. Use configured backend type if registered.
2. Otherwise fall back to `DEFAULT_MEMORY_BACKEND` (`"qmd"`).
3. For `qmd` search failures/unavailability, fall back to local layered file search.
#### Paths and files
### 4.4 Compatibility Matrix
| Path | Role | Compatibility expectation |
|------|------|---------------------------|
| `.fusion/memory/MEMORY.md` | Canonical long-term working memory | Canonical |
| `.fusion/memory/YYYY-MM-DD.md` | Daily memory | Canonical layered layout |
| `.fusion/memory/DREAMS.md` | Dream/synthesis memory | Canonical layered layout |
| `.fusion/memory.md` | Deprecated legacy fallback path | Compatibility-only (migration/alias), not canonical |
| `.fusion/memory-insights.md` | Insight extraction output | Preserved |
| `.fusion/memory-audit.md` | Extraction audit output | Preserved |
#### Prompt behavior
| Condition | Expected behavior |
|-----------|-------------------|
| `memoryEnabled: true`, backend=`file` | Memory instructions include explicit `.fusion/memory/MEMORY.md` path hint |
| `memoryEnabled: true`, backend=`qmd` | Memory instructions are backend-aware and omit hardcoded file path |
| `memoryEnabled: true`, backend=`readonly` | Memory instructions are read-only (no update directives) |
| `memoryEnabled: false` | No memory instructions injected, regardless of backend |
### 4.3 Must-Not-Break Invariants
1. **Canonical layout invariant:** OpenClaw layered memory workspace (`.fusion/memory/`) remains canonical.
2. **Legacy-path invariant:** `.fusion/memory.md` remains a deprecated legacy fallback; it must not be reintroduced as primary storage.
3. **Backend-selection invariant:** Runtime selection uses `memoryBackendType`, with default `qmd`.
4. **Prompt-context invariant:** Memory instructions vary by backend type; only `file` backend emits an explicit file path hint.
5. **Read-shape invariant:** Missing working memory resolves to `""`; missing insights memory resolves to `null`.
6. **Non-fatal invariant:** Bootstrap and memory-operation failures should not block store startup/settings updates.
### 4.4 Fallback Trigger Conditions
| Condition | Action |
|-----------|--------|
| `memoryBackendType` missing | Use `DEFAULT_MEMORY_BACKEND` (`qmd`) |
| Configured backend type not registered | Fall back to `qmd` |
| QMD search unavailable/fails | Fall back to local layered-file search |
| Backend marked non-writable (`readonly`) | Reject writes with `MemoryBackendError("READ_ONLY", ...)` |
| Read failures marked as backend-unavailable/read-failed in helper wrappers | Return empty content shape where contract requires graceful degradation |
| Concern | Required Behavior |
|---|---|
| Settings persistence | Unknown `memoryBackendType` may be persisted, but runtime still falls back safely |
| Read-only backend | Writes fail with typed `READ_ONLY` error |
| Legacy upgrades | Existing `.fusion/memory.md` can seed canonical `.fusion/memory/MEMORY.md` once during migration |
| Memory file APIs | Must enforce workspace-relative path validation and project root boundaries |
| Prompt generation | Must honor `memoryEnabled` toggle and backend-aware instruction context |
### 4.5 Test Coverage Verification Checklist
| Behavior | Test File | Lines |
|----------|-----------|-------|
| `DEFAULT_MEMORY_BACKEND === "qmd"` and settings key `memoryBackendType` | `packages/core/src/memory-backend.test.ts` | 720, 724 |
| `resolveMemoryBackend()` default + type-specific resolution/fallback | `packages/core/src/memory-backend.test.ts` | 730756 |
| Canonical `MEMORY_FILE_PATH` value | `packages/core/src/project-memory.test.ts` | 4143 |
| `resolveMemoryInstructionContext` backend-specific path hints | `packages/core/src/project-memory.test.ts` | 450485 |
| Backend-aware triage instruction generation | `packages/core/src/project-memory.test.ts` | 493546 |
| Backend-aware execution instruction generation | `packages/core/src/project-memory.test.ts` | 548604 |
| Store init/toggle bootstrap behavior | `packages/core/src/store.test.ts` | 82548336 |
| Triage prompt memory enable/disable + backend branching | `packages/engine/src/triage.test.ts` | 272323 |
| Executor prompt memory enable/disable + backend branching | `packages/engine/src/executor.test.ts` | 24132463 |
Contract-critical behavior is covered by:
### 4.6 Cross-Linking
This specification must remain aligned with:
- [Architecture](./architecture.md) — memory subsystem + storage model
- [Contributing](./contributing.md) — memory file conventions for contributors/agents
- [Settings Reference](./settings-reference.md) — user-facing settings and insight extraction behavior
- [README](./README.md) — documentation index
Related migration tasks:
- FN-1418/FN-1419/FN-1420 — completed foundational migration
- FN-2087 — active migration reconciliation umbrella
- FN-2131/FN-2132/FN-2133/FN-2134 — migration execution phases
- `packages/core/src/memory-backend.test.ts`
- built-in backends (`file`, `qmd`, `readonly`)
- registry helpers and `resolveMemoryBackend()` default/fallback behavior
- layered workspace helpers (`ensureOpenClawMemoryFiles`, list/get/write, path validation)
- `packages/core/src/project-memory.test.ts`
- canonical `MEMORY_FILE_PATH`
- `resolveMemoryInstructionContext()` branching
- triage/execution/reviewer instruction generation per backend
- `packages/core/src/store.test.ts`
- memory bootstrap behavior when memory is enabled/disabled and toggled
- `packages/engine/src/triage.test.ts` and `packages/engine/src/executor.test.ts`
- memory instruction injection behavior by settings/backend
---
## 5. Downstream Task Alignment
### 5.1 Completed foundation (historical)
This contract should remain aligned with:
#### FN-1418 — Implement Core Memory Plugin Infrastructure (**Complete**)
- [Architecture](./architecture.md)
- [Contributing](./contributing.md)
- [Settings Reference](./settings-reference.md)
- [Docs README](./README.md)
Delivered:
- `MemoryBackend` runtime contract in `@fusion/core`
- Built-in backend implementations and registry helpers
- Backend resolution plumbing and tests
#### FN-1419 — Update Engine to Use Backend Abstraction (**Complete**)
Delivered:
- Backend-aware memory instruction wiring in engine prompt builders
- `memoryBackendType` settings integration in runtime selection path
- Regression coverage for `memoryEnabled` and backend branching
#### FN-1420 — Update Dashboard to Use Backend Abstraction (**Complete**)
Delivered:
- Dashboard memory routes/UI aligned with backend-aware memory model
- Memory settings controls and persistence behavior
### 5.2 Active migration reconciliation
#### FN-2087 — Memory path/backend reconciliation umbrella (**Active**)
Purpose:
- Finish migration cleanup and eliminate remaining legacy-path contradictions across runtime prompts, tests, and docs.
#### FN-2131FN-2134 — Implementation slices under FN-2087 (**Active/queued**)
Purpose:
- Execute the remaining migration phases in scoped increments while preserving backward compatibility.
### 5.3 Documentation alignment requirement
This contract is the canonical documentation reference for memory backend semantics. Any future migration step that changes runtime behavior must update this file and keep `architecture.md`, `contributing.md`, and `settings-reference.md` in sync in the same change.
---
## Appendix A: Reference Implementation Notes
### A.1 Module Location
Current memory backend infrastructure lives in `@fusion/core`:
```
packages/core/src/
├── memory-backend.ts # Interface, registry, built-in backends, path helpers
├── project-memory.ts # Bootstrap + prompt instruction wiring
├── memory-insights.ts # Insight extraction + pruning paths
└── index.ts # Re-export surface
```
### A.2 Test Strategy
| Test Type | Coverage |
|-----------|----------|
| Unit tests | Each backend class in isolation |
| Integration tests | Backend with real filesystem |
| Contract tests | All backends satisfy `MemoryBackend` interface |
| Compatibility tests | Ensure file backend matches current behavior exactly |
### A.3 Documentation Updates
After implementation or migration updates:
- Update [Settings Reference](./settings-reference.md) with `memoryBackendType` behavior and defaults
- Update [Architecture](./architecture.md) memory section with backend/runtime behavior changes
- Update AGENTS.md if engine prompt wiring changes
If backend contract behavior changes in source, update these docs in the same change.
---
## Appendix B: Future Extensibility
### B.1 Vector/Semantic Search Backend
A future backend could implement semantic search:
```typescript
class VectorMemoryBackend implements MemoryBackend {
readonly type = "vector";
readonly name = "Vector Search";
readonly capabilities = {
readable: true,
writable: true,
supportsAtomicWrite: false,
hasConflictResolution: false,
persistent: true,
};
async read(rootDir: string): Promise<MemoryReadResult> {
// delegate to file storage, then enrich vector index
}
async write(rootDir: string, content: string): Promise<MemoryWriteResult> {
// persist + reindex embeddings
}
async search(rootDir: string, options: MemorySearchOptions): Promise<MemorySearchResult[]> {
// semantic query over embeddings
}
}
```
### B.2 Hybrid Backend
A hybrid backend could combine file persistence with vector search:
```typescript
class HybridMemoryBackend implements MemoryBackend {
readonly type = "hybrid";
readonly name = "Hybrid File + Semantic Search";
readonly capabilities = {
readable: true,
writable: true,
supportsAtomicWrite: true,
hasConflictResolution: false,
persistent: true,
};
private file = new FileMemoryBackend();
private vector = new VectorMemoryBackend();
async read(rootDir: string): Promise<MemoryReadResult> {
return this.file.read(rootDir);
}
async write(rootDir: string, content: string): Promise<MemoryWriteResult> {
const result = await this.file.write(rootDir, content);
await this.vector.write(rootDir, content);
return { ...result, backend: this.type };
}
async search(rootDir: string, options: MemorySearchOptions): Promise<MemorySearchResult[]> {
// route by search mode, then fallback
}
}
```
---
*Last updated: 2026-04-09*
*Last updated: 2026-04-19*