Skip to content

feat(memory_fs): optional markdown memory file system export#435

Open
sairin1202 wants to merge 2 commits into
mainfrom
feat/memory-file-artifacts
Open

feat(memory_fs): optional markdown memory file system export#435
sairin1202 wants to merge 2 commits into
mainfrom
feat/memory-file-artifacts

Conversation

@sairin1202

Copy link
Copy Markdown
Contributor

Summary

Adds a read-only, opt-in artifact layer that renders the structured memory store into browsable markdown files, treating the memory model as an actual file system. This is the first, fully-additive foundation step toward the file-system-as-memory direction (it does not change any memorize/retrieve/CRUD behavior or public API).

Generated artifacts:

  • MEMORY.md — top-level overview of all folders (categories)
  • index.md — source-file description index (one entry per Resource)
  • categories/<slug>.md — per-folder summary file (e.g. preferences.md)
  • skill.md — standalone aggregation of skill-type memories

How it works

  • New memu.memory_fs.MemoryFileExporter walks folders (MemoryCategory), files (MemoryItem), and sources (Resource) and renders them to disk. It is read-only against the database.
  • Diff detection via a sidecar .memufs_manifest.json of per-file content hashes — each export only rewrites artifacts whose rendered content changed, and prunes stale ones. No database schema change required.
  • Rendered content avoids volatile values (e.g. "now" timestamps), so an unchanged store re-exports as a verified no-op.
  • New entrypoint MemoryService.export_memory_files(user=...), gated by memory_files_config.enabled (default off), with scope filtering via the user model and writes serialized through a per-service asyncio.Lock.

Design notes / deferred decisions

This PR deliberately keeps scope tight. The following were fixed with conservative defaults and can be iterated later:

  • MemoryItem extraction/synthesis is unchanged (skill.md aggregates existing skill items rather than replacing the item pipeline).
  • diff state lives in a sidecar manifest, not on Resource (avoids cross-backend schema churn).
  • category set reuses existing category config.

Test plan

  • uv run python -m pytest tests/test_memory_files.py -v (6 passed): artifact contents, diff idempotency, stale pruning, user-scope isolation, disabled-guard, manifest round-trip
  • Full suite: 85 passed, 1 skipped (skip = postgres; test_lazyllm excluded — optional dep not installed, pre-existing)
  • uv run ruff check and uv run mypy clean on changed files

Made with Cursor

Add a read-only, opt-in artifact layer that renders the structured memory
store into browsable markdown files (MEMORY.md, index.md, skill.md, and
categories/<slug>.md), treating the memory model as an actual file system.

- New `memu.memory_fs.MemoryFileExporter` walks folders (categories), files
  (items), and sources (resources) and renders them to disk.
- Diff detection via a sidecar `.memufs_manifest.json` of per-file content
  hashes, so each export only rewrites changed artifacts — no DB schema change.
- `MemoryService.export_memory_files(user=...)` entrypoint, gated by
  `memory_files_config.enabled` (default off), serialized via a per-service lock.
- Rendered content avoids volatile values so an unchanged store re-exports as
  a no-op; stale artifacts are pruned.

Fully additive: no change to memorize/retrieve/CRUD behavior or public API.

Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings June 18, 2026 03:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an opt-in, read-only “memory file system” export path to render the structured memory store (categories/resources/items) into a browsable set of markdown artifacts on disk, wired through MemoryService and documented in the architecture guide.

Changes:

  • Introduces memu.memory_fs.MemoryFileExporter with manifest-based diffing/pruning to write MEMORY.md, index.md, categories/<slug>.md, and skill.md.
  • Adds MemoryFilesConfig and a new MemoryService.export_memory_files(...) entrypoint gated by memory_files_config.enabled and serialized via an asyncio.Lock.
  • Adds a focused test suite covering contents, idempotency, pruning, scoping, disabled guard, and manifest round-trips.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/test_memory_files.py Adds tests validating artifact contents, idempotency, pruning, scope filtering, disabled behavior, and manifest handling.
src/memu/memory_fs/exporter.py Implements the exporter, markdown rendering, and manifest-based sync/prune logic.
src/memu/memory_fs/__init__.py Exposes MemoryFileExporter, ExportResult, and slugify as the package API.
src/memu/app/settings.py Introduces MemoryFilesConfig (enabled flag + output directory).
src/memu/app/service.py Wires config + exporter and adds export_memory_files() to MemoryService.
docs/architecture.md Documents the new memory file system export feature and its operational characteristics.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/memu/memory_fs/exporter.py Outdated
Comment on lines +138 to +142
def _item_counts_per_category(items: list[MemoryItem], *, database: Database) -> dict[str, int]:
in_scope_ids = {item.id for item in items}
counts: dict[str, int] = {}
relations = getattr(database, "relations", None) or []
for relation in relations:
Comment thread src/memu/memory_fs/exporter.py Outdated
Comment on lines +151 to +154
"---",
f"name: {category.name}",
f"description: {self._inline(description)}",
f"updated_at: {category.updated_at.isoformat()}",
Comment on lines +231 to +237
for rel_path in manifest:
if rel_path in new_manifest:
continue
stale = self.output_dir / rel_path
if stale.exists():
stale.unlink()
result.removed.append(rel_path)
Comment thread src/memu/memory_fs/exporter.py Outdated
Comment on lines +176 to +180
slug = slug_by_category[category.id]
description = self._inline((category.description or "").strip())
count = item_counts.get(category.id, 0)
suffix = f" — {description}" if description else ""
lines.append(f"- [{category.name}]({CATEGORIES_DIRNAME}/{slug}.md){suffix} ({count} items)")
Rework the exporter so output matches the canonical memory tree
(INDEX.md / MEMORY.md / skill/<name>/SKILL.md) and the description-trunk model:

- Every source becomes one multimodal description (the shared trunk); INDEX,
  MEMORY, and SKILL are three sibling bypasses (none upstream of another).
- INDEX.md: navigable table of contents (folders, skills, per-source
  descriptions) that links out instead of duplicating summaries.
- MEMORY.md: living memory aggregated from category (folder) summaries.
- skill/<skill_name>/SKILL.md: each skill-type item as a standalone doc; folder
  name parsed from frontmatter `name:` (heading / short-id fallbacks).
- Prune now-empty skill dirs when a skill is removed; default output dir -> ./data/memory.

Still read-only, diff-driven via the sidecar manifest, and disabled by default.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants