## Summary Adds the **Stash memory backend** (`memory.backendType=stash`) that connects Fusion's agent memory to the [Stash](https://github.com/Fergana-Labs/stash) product — *knowledge bases for the agent era* ([product site: joinstash.ai](https://joinstash.ai)). Fusion becomes a first-class Stash client: complete chat sessions and finished tasks are captured into Stash, memory is recalled during chat, and Stash sessions are kept in sync with the dashboard (including deletes and archival). **Product:** <https://github.com/Fergana-Labs/stash> · [joinstash.ai](https://joinstash.ai) ## What's included ### 1. Stash memory backend (RUFU-068 / RUFU-121) - New `StashMemoryBackend` (`memory.backendType=stash`) with `stashUrl` / `stashApiKey` settings (global secrets-store `stash-api-key` + per-project override). - **Complete-chat-session capture** keyed by ChatSession id. - Sessions are classified into **per-project folders** (get-or-create, `external_key fusion-<projectId>`, 1h per-process cache) and **soft-deleted with their chat** via `DELETE /api/chat/sessions/:id`. - Per-conversation **memory-focus** read-time scoping (new `0066_chat_session_memory_focus.sql` migration — sequence renumbered 0059→0060→0061→0065→0066 as origin/main claimed the lower numbers); event metadata enriched with `project` / `project_name` / `chat_title`. - Recall queries normalized to single-keyword / explicit-OR ASCII (≤100 chars); shared normalizer export reused by per-turn recall. ### 2. Per-task executor transcript capture (RUFU-122) Finished or failed tasks upload their executor transcript (`agent-log.jsonl`) to Stash as a task session. ### 3. Bulk archive Stash sync (RUFU-125) Archived task-planner chats soft-delete their Stash sessions on bulk archival (paged). The snapshot of doomed session ids is taken *before* the local bulk delete, and the Stash sync runs fire-and-forget so a Stash stall can never delay local archival. ### 4. Per-chat "Preserve to Stash" backfill (RUFU-136) A per-chat action that backfills a chat's full history into Stash, with client-side idempotency and a pre-check that skips already-uploaded content (fail-closed, no duplicate upload on transport failure). - **Session-folder naming fix:** the first project folder is now named "Fusion — <project name>" instead of the bare "Fusion" fallback (the backfill now resolves the central-registry project name, best-effort, never blocking the upload). ### 5. Opt-in semantic (vector) recall (RUFU-126) `stashVectorSearch` setting (default `false` — **zero behavior change until enabled**). For multi-word queries the backend tries Stash's semantic-search endpoint first, then falls back byte-identically to the keyword path. Definitive 404/405/501/503 responses are negatively cached per process. Requires a patched Stash server (new endpoint + `sentence-transformers` + embedding backfill); unpatched servers are transparently bypassed after the first 404. ## Safety - **Opt-in / inert by default:** the default backend remains `qmd`; the Stash backend is inert until `memoryBackendType=stash` + `stashUrl` are set. - All Stash I/O is **best-effort, fail-closed, and non-blocking** — a Stash outage never blocks chat, task completion, or archival. No run-audit content is emitted. ## Testing - Backfill + delete-sync suites (20/20), Stash backend suite (68/68), executor memory / session capture suites, `memory-focus-recalling`, description-guard — all green. - `tsc` clean across core / engine / dashboard. - Live verification: bulk backfill of 21/24 chats completed; the "Preserve to Stash" action is idempotent on re-run. ## Changesets - `@runfusion/fusion` **minor** — Stash memory backend + capture (RUFU-068/121), per-task transcript (RUFU-122), bulk archive sync (RUFU-125), per-chat backfill (RUFU-136), opt-in vector search (RUFU-126) - `@runfusion/fusion` **patch** — backfill session-folder naming fix ## Rebase Note (2026-08-23) Rebased onto `origin/main` `3f448f7292` (v0.77.0-beta.7). Conflicts resolved additively: - `packages/core/src/postgres/schema-applier.ts` + test — upstream's 0062-0065 migrations (task/subtask splitting removal, AI merge review reconciliation, task repository scope, FN-149 review convergence) unioned with this PR's `chat_sessions.memory_focus` migration, which is **renumbered 0065 → 0066** (upstream's FN-149 shipped 0065 canonically on origin/main); `SCHEMA_BASELINE_VERSION` advances to `0066`. - `packages/dashboard/app/components/ChatView.tsx` — upstream's docked chat sidebar resize handlers unioned with the RUFU-136 "Preserve to Stash" backfill handler. - New commit: `settings.memory.*` stash-backend i18n keys added to all 6 secondary locales (RUFU-121/122 parity fix; `pnpm i18n:status` no longer reports any violation introduced by this PR). **Deploy note (operator environments that already ran a pre-rebase build of this PR):** the memory-focus SQL may already be in the schema under ledger row `0065`. Remap that row to `0066` (`UPDATE fusion_schema_migrations SET version = '0066' WHERE version = '0065';`) *before* first boot of a 0066-ceiling binary — otherwise the fresh upstream `0065_fn_149_review_convergence_stage.sql` would be skipped as "already applied". Clean databases (no prior memory-focus row) need no action. **CI note — Lint (lifecycle-column census) is red on the merge base:** `pnpm check:lifecycle-columns --strict` fails identically on pure `origin/main` `3f448f7292` with `packages/core/src/db/legacy-adoption.ts: 0 -> 3` (3 column guards in the U9b legacy-adoption table without a baseline entry or `DELIBERATE-LITERAL` marker). Verified by running the census on a clean origin/main checkout — inherited from the base, not introduced by this PR. Fix belongs upstream; tracked separately. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Stash memory integration with project configuration and optional semantic search. * Added per-chat memory focus controls and a `/focus` command. * Added “Preserve to Stash” for uploading complete chat history. * Added automatic chat, task transcript, and completion-event capture. * Added project-specific Stash session folders and archive/delete synchronization. * **Bug Fixes** * Improved Stash folder naming and handling of missing branches during no-commit tasks. * **Documentation** * Added setup, configuration, integration, vector-search, and performance guidance. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Fusion <noreply@runfusion.ai> Co-authored-by: gsxdsm <gsxdsm@users.noreply.github.com>
401 lines
19 KiB
Markdown
401 lines
19 KiB
Markdown
# Fusion Memory Plugin Contract
|
||
|
||
[← Docs index](./README.md)
|
||
|
||
This document is the source-of-truth contract for Fusion memory backend behavior after the FN-2087 migration series.
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Current Fusion Memory Baseline](#1-current-fusion-memory-baseline)
|
||
2. [OpenClaw Research Findings](#2-openclaw-research-findings)
|
||
3. [Fusion Memory Plugin Contract](#3-fusion-memory-plugin-contract)
|
||
4. [Migration Strategy + Compatibility Guardrails](#4-migration-strategy--compatibility-guardrails)
|
||
5. [Downstream Task Alignment](#5-downstream-task-alignment)
|
||
|
||
---
|
||
|
||
## 1. Current Fusion Memory Baseline
|
||
|
||
### 1.1 Two Memory Systems (Distinct Responsibilities)
|
||
|
||
Fusion currently has two related but distinct memory systems:
|
||
|
||
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
|
||
|
||
### 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` |
|
||
| `DEFAULT_MEMORY_BACKEND` | `qmd` | `memory-backend.ts` |
|
||
| `MEMORY_WORKING_PATH` | `.fusion/memory/MEMORY.md` | `memory-insights.ts` |
|
||
| `MEMORY_INSIGHTS_PATH` | `.fusion/memory/memory-insights.md` | `memory-insights.ts` |
|
||
| `MEMORY_AUDIT_PATH` | `.fusion/memory/memory-audit.md` | `memory-insights.ts` |
|
||
|
||
### 1.3 Exported Surface (Post-Migration)
|
||
|
||
#### `project-memory.ts` exports
|
||
|
||
| Export | Purpose |
|
||
|---|---|
|
||
| `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 |
|
||
| Planning instruction builder | Planning prompt memory instructions |
|
||
| `buildExecutionMemoryInstructions()` | Executor prompt memory instructions |
|
||
| `buildReviewerMemoryInstructions()` | Reviewer prompt memory instructions |
|
||
| `readProjectMemory()` | Direct canonical long-term file read |
|
||
|
||
#### `memory-backend.ts` key exports
|
||
|
||
| 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 |
|
||
| `listProjectMemoryFiles()`, `readProjectMemoryFile()`, `writeProjectMemoryFile()` | Validated layered file operations |
|
||
|
||
### 1.4 Settings Baseline
|
||
|
||
`ProjectSettings` fields that govern memory behavior:
|
||
|
||
- `memoryEnabled?: boolean` (default `true`)
|
||
- `memoryBackendType?: string` (default `"qmd"`)
|
||
- `insightExtractionEnabled?: boolean` (default `false`)
|
||
- `insightExtractionSchedule?: string` (default `"0 2 * * *"`)
|
||
- `insightExtractionMinIntervalMs?: number` (default `86400000`)
|
||
|
||
Runtime backend resolution uses an internal `MemorySettings` shape with `memoryBackendType` and optional additional keys.
|
||
|
||
### 1.5 Key Invariants
|
||
|
||
1. Canonical layered long-term memory file is **`.fusion/memory/MEMORY.md`**.
|
||
2. Runtime memory APIs only read/write canonical layered files under `.fusion/memory/`.
|
||
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
|
||
|
||
Fusion adopted OpenClaw-style layered memory while keeping a concrete TypeScript runtime contract.
|
||
|
||
### 2.1 What Was Adopted
|
||
|
||
- Layered workspace (`MEMORY.md`, daily files, `DREAMS.md`)
|
||
- Backend abstraction with explicit capability declarations
|
||
- Search over layered files with bounded snippets
|
||
- Migration safety via strict canonical path validation for layered files
|
||
|
||
### 2.2 Implications Table
|
||
|
||
| OpenClaw Concept | Fusion Current State | Contract Implication |
|
||
|---|---|---|
|
||
| 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 |
|
||
| Path migration completion | Legacy top-level path support removed from runtime APIs | Canonical source-of-truth is `.fusion/memory/` only |
|
||
|
||
---
|
||
|
||
## 3. Fusion Memory Plugin Contract
|
||
|
||
### 3.1 Runtime Interface (Canonical)
|
||
|
||
```ts
|
||
export interface MemoryBackendCapabilities {
|
||
readable: boolean;
|
||
writable: boolean;
|
||
supportsAtomicWrite: boolean;
|
||
hasConflictResolution: boolean;
|
||
persistent: boolean;
|
||
}
|
||
|
||
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>;
|
||
}
|
||
|
||
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"
|
||
| "READ_FAILED"
|
||
| "WRITE_FAILED"
|
||
| "UNSUPPORTED"
|
||
| "CONFLICT"
|
||
| "QUOTA_EXCEEDED"
|
||
| "BACKEND_UNAVAILABLE";
|
||
|
||
export class MemoryBackendError extends Error {
|
||
readonly code: MemoryBackendErrorCode;
|
||
readonly backend: string;
|
||
}
|
||
```
|
||
|
||
### 3.3 Built-In Backends
|
||
|
||
| 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 |
|
||
|
||
#### 3.3.1 Stash vector/semantic search (RUFU-126)
|
||
|
||
`StashMemoryBackend` supports an **opt-in vector (semantic) recall path** for
|
||
multi-word queries, layered in front of the RUFU-121 keyword path. Decision
|
||
provenance D1–D5: `docs/research/stash-vector-search-evaluation.md`.
|
||
|
||
**Endpoint contract.** `GET /api/v1/me/sessions/events/semantic-search` with
|
||
`q=<raw trimmed query, capped at 200 chars>` (NOT RUFU-121-normalized — the
|
||
embedder tokenizes on its own) and `limit=<same 1..20 clamp as the keyword
|
||
path>`. Response: the standard `HistoryEventListResponse` envelope
|
||
(`{ events: [...], has_more }`); each event carries `id`, `session_id`,
|
||
`content`/`snippet`, and a `rank` (cosine similarity, 0..1) from Stash.
|
||
|
||
**Flag (default off).** Per-project `stashVectorSearch` boolean, schema-only
|
||
(no UI row, consistent with `stashUrl`/`stashApiKey`), default `false`. Threaded
|
||
through `resolveMemoryBackend` into the materialized backend; the shared
|
||
registry default instance always stays off. Default-off = zero behavior
|
||
change until an operator enables it.
|
||
|
||
**Multi-word-only rule (D2).** The vector attempt runs only when the flag is
|
||
on AND the trimmed raw query has ≥2 whitespace-separated tokens. Single-word
|
||
queries stay keyword-only (exact-token FTS is the best single-token baseline).
|
||
|
||
**Fallback + negative-cache semantics.** On ANY vector failure — network
|
||
error/timeout, non-2xx, malformed body, or an empty vector result list —
|
||
`search()` falls through to the RUFU-121 keyword path **byte-identical**
|
||
(normalized `q`, legacy empty-query URL, limit cap, fail-closed `[]`). A
|
||
per-process negative capability cache (baseUrl-keyed, TTL 1h, test seam
|
||
`__resetVectorCapabilityCacheForTests`) suppresses further vector attempts
|
||
only after *definitive* no-vector responses — 404 (unpatched server), 405,
|
||
501, 503 (embedder unconfigured); 422/500 and network errors are never
|
||
cached, so the vector path retries on the next call.
|
||
|
||
**Score-scale caveat (D5).** Vector `score` = response `rank` (cosine
|
||
similarity, 0..1; missing/non-finite → 1.0); the keyword path keeps
|
||
positional scores (2.0 first hit, 1.0 thereafter). The two scales differ —
|
||
client-side min-score filters must treat score scales per-backend.
|
||
|
||
**Upstream dependency.** The endpoint ships in a local Stash branch
|
||
(`fusion-rufu-126-sessions-semantic-search`, plus `sentence-transformers` in
|
||
requirements and a history-event embedding backfill task) — **not yet merged
|
||
or deployed**. Against an unpatched server the vector path 404s and falls
|
||
back transparently (once negatively cached per process). Operator rollout
|
||
steps (image rebuild with the embedder, backfill, verification, flag
|
||
enablement) are checklist form in
|
||
`docs/research/stash-vector-search-evaluation.md`.
|
||
|
||
### 3.4 Registry Contract (Function-Based)
|
||
|
||
```ts
|
||
registerMemoryBackend(backend: MemoryBackend): void
|
||
getMemoryBackend(type: string): MemoryBackend | undefined
|
||
listMemoryBackendTypes(): string[]
|
||
```
|
||
|
||
Backends are stored in a module-level `Map<string, MemoryBackend>`. Built-ins are registered at module load.
|
||
|
||
### 3.5 Resolution Contract
|
||
|
||
```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
|
||
```
|
||
|
||
Resolution chain:
|
||
1. Configured `memoryBackendType` (if registered)
|
||
2. `DEFAULT_MEMORY_BACKEND` (`"qmd"`)
|
||
|
||
### 3.6 Layered Memory Operations
|
||
|
||
The layered memory workspace helpers in `memory-backend.ts` provide:
|
||
|
||
- Workspace bootstrap: `ensureOpenClawMemoryFiles()`
|
||
- File listing: `listProjectMemoryFiles()`
|
||
- Validated file access: `readProjectMemoryFile()`, `writeProjectMemoryFile()`
|
||
- Path helpers: `memoryLongTermPath()`, `dailyMemoryPath()`, `memoryDreamsPath()`
|
||
|
||
Allowed workspace files are constrained to:
|
||
|
||
- `.fusion/memory/MEMORY.md`
|
||
- `.fusion/memory/DREAMS.md`
|
||
- `.fusion/memory/YYYY-MM-DD.md`
|
||
|
||
### 3.7 Prompt Instruction Context Contract
|
||
|
||
`resolveMemoryInstructionContext(settings?)` in `project-memory.ts` currently resolves as:
|
||
|
||
| 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 layered workspace: `.fusion/memory/`
|
||
- Canonical long-term file: `.fusion/memory/MEMORY.md`
|
||
- Runtime path validation rejects non-layered legacy requests
|
||
|
||
#### 3.8.2 QMD stale-index normalization scope
|
||
|
||
QMD search result normalization may remap stale indexed paths to canonical layered paths so search results remain consumable. This normalization does not re-enable legacy read/write APIs.
|
||
|
||
#### 3.8.3 Dashboard and API path constraints
|
||
|
||
Dashboard memory routes must remain rooted in project-scoped memory APIs:
|
||
|
||
- `/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`
|
||
|
||
#### 3.8.4 Prompt compatibility matrix
|
||
|
||
| 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.5 Insight extraction relationship
|
||
|
||
Insight extraction is a separate subsystem that currently uses:
|
||
|
||
- Working source: `.fusion/memory/MEMORY.md`
|
||
- Insight output: `.fusion/memory/memory-insights.md`
|
||
- Audit output: `.fusion/memory/memory-audit.md`
|
||
|
||
It is related to, but not equivalent to, backend selection and prompt instruction logic.
|
||
|
||
---
|
||
|
||
## 4. Migration Strategy + Compatibility Guardrails
|
||
|
||
### 4.1 Migration Phase Status
|
||
|
||
| 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 |
|
||
|
||
### 4.2 Must-Not-Break Invariants
|
||
|
||
1. Canonical long-term layered memory remains `.fusion/memory/MEMORY.md`.
|
||
2. Legacy top-level memory requests are not part of the runtime API contract.
|
||
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.
|
||
|
||
### 4.3 Resolution Fallback Chain
|
||
|
||
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.
|
||
|
||
### 4.4 Compatibility Matrix
|
||
|
||
| 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 | Runtime memory APIs ignore legacy top-level paths and operate on layered files only |
|
||
| 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
|
||
|
||
Contract-critical behavior is covered by:
|
||
|
||
- `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 long-term path bootstrap/read behavior
|
||
- `resolveMemoryInstructionContext()` branching
|
||
- planning/execution/reviewer instruction generation per backend
|
||
- `packages/core/src/store.test.ts`
|
||
- memory bootstrap behavior when memory is enabled/disabled and toggled
|
||
- Engine planning and executor tests
|
||
- memory instruction injection behavior by settings/backend
|
||
|
||
---
|
||
|
||
## 5. Downstream Task Alignment
|
||
|
||
This contract should remain aligned with:
|
||
|
||
- [Architecture](./architecture.md)
|
||
- [Contributing](./contributing.md)
|
||
- [Settings Reference](./settings-reference.md)
|
||
- [Docs README](./README.md)
|
||
|
||
If backend contract behavior changes in source, update these docs in the same change.
|
||
|
||
---
|
||
|
||
*Last updated: 2026-04-19*
|
||
|
||
## Recall layer
|
||
|
||
The project-scoped recall store records durable `decision`, `preference`, and `solution` entries with content, tags, source provenance (`taskId`, `agentId`, `sessionId`, and origin), timestamps, and optional knowledge-graph node ids. `appendRecall` normalizes content (trim/lowercase/whitespace collapse/trailing punctuation removal) and rejects an exact normalized hash or Jaccard token similarity of at least `0.9` among the 200 most-recent same-kind records. That candidate window is intentionally bounded: an older exact twin is not visible to the in-memory classifier.
|
||
|
||
Writes hold a transaction advisory lock keyed by `(project, kind)` across candidate lookup and insert, which serializes the read-then-write near-duplicate decision without contending unrelated kinds or projects. The named `(project_id, kind, content_hash)` constraint is only an exact-hash backstop: `ON CONFLICT DO NOTHING` keeps the transaction usable for its in-transaction re-read when an exact twin is outside the bounded window (or a bypassing importer races the write). A raising unique insert would abort the transaction, and the constraint cannot catch near duplicates.
|
||
|
||
`searchRecall` uses deterministic keyword scoring and one shared `clampRecallSearchLimit` (default 10, maximum 50) for keyword, vector, degradation, and list paths. A caller may supply a per-call `RecallVectorSearchProvider` to rank—never fetch, write, or filter—the already project-scoped, kind/tag-filtered candidate set. There is no provider registry, setting, default implementation, or embedding dependency. `mode: "vector"` is returned only for a successful provider result containing a resolvable candidate; missing, throwing, empty, or unknown-only providers degrade to keyword mode while `capabilities.vector` remains true when a provider was supplied. Provider limits are advisory: the store discards unknown ids, keeps each duplicate id's highest score, ranks, then applies the same clamped limit after ranking.
|
||
|
||
Prompt builders may append a `### Recalled Context` section capped at 800 UTF-8 bytes; the budget includes its separator, heading, lines, and trailing newline, and never truncates pre-existing instructions. This task adds no MCP/tool surface, automatic capture, consolidation, agent pre-steering, or knowledge-graph integration; those remain later work.
|