feat: sync memory with filesystem tree (#685)

Co-authored-by: Letta <noreply@letta.com>
Co-authored-by: cpacker <packercharles@gmail.com>
This commit is contained in:
Kevin Lin
2026-01-26 21:48:57 -08:00
committed by GitHub
parent 57f99b906b
commit 7ab97e404d
16 changed files with 1815 additions and 249 deletions

View File

@@ -15,9 +15,9 @@ export const GLOBAL_BLOCK_LABELS = ["persona", "human"] as const;
* Block labels that are stored per-project (local to the current directory).
*/
export const PROJECT_BLOCK_LABELS = [
"project",
"skills",
"loaded_skills",
"memory_filesystem",
] as const;
/**
@@ -37,7 +37,11 @@ export type MemoryBlockLabel = (typeof MEMORY_BLOCK_LABELS)[number];
* Block labels that should be read-only (agent cannot modify via memory tools).
* These blocks are managed by specific tools (e.g., Skill tool for skills/loaded_skills).
*/
export const READ_ONLY_BLOCK_LABELS = ["skills", "loaded_skills"] as const;
export const READ_ONLY_BLOCK_LABELS = [
"skills",
"loaded_skills",
"memory_filesystem",
] as const;
/**
* Block labels that should be isolated per-conversation.

View File

@@ -0,0 +1,753 @@
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync } from "node:fs";
import { readdir, readFile, unlink, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join, relative } from "node:path";
import type { Block } from "@letta-ai/letta-client/resources/agents/blocks";
import { getClient } from "./client";
export const MEMORY_FILESYSTEM_BLOCK_LABEL = "memory_filesystem";
export const MEMORY_FS_ROOT = ".letta";
export const MEMORY_FS_AGENTS_DIR = "agents";
export const MEMORY_FS_MEMORY_DIR = "memory";
export const MEMORY_SYSTEM_DIR = "system";
export const MEMORY_USER_DIR = "user";
export const MEMORY_FS_STATE_FILE = ".sync-state.json";
const MANAGED_BLOCK_LABELS = new Set([MEMORY_FILESYSTEM_BLOCK_LABEL]);
type SyncState = {
systemBlocks: Record<string, string>;
systemFiles: Record<string, string>;
userBlocks: Record<string, string>;
userFiles: Record<string, string>;
userBlockIds: Record<string, string>;
lastSync: string | null;
};
export type MemorySyncConflict = {
label: string;
blockValue: string | null;
fileValue: string | null;
};
export type MemorySyncResult = {
updatedBlocks: string[];
createdBlocks: string[];
deletedBlocks: string[];
updatedFiles: string[];
createdFiles: string[];
deletedFiles: string[];
conflicts: MemorySyncConflict[];
};
export type MemorySyncResolution = {
label: string;
resolution: "file" | "block";
};
export function getMemoryFilesystemRoot(
agentId: string,
homeDir: string = homedir(),
): string {
return join(
homeDir,
MEMORY_FS_ROOT,
MEMORY_FS_AGENTS_DIR,
agentId,
MEMORY_FS_MEMORY_DIR,
);
}
export function getMemorySystemDir(
agentId: string,
homeDir: string = homedir(),
): string {
return join(getMemoryFilesystemRoot(agentId, homeDir), MEMORY_SYSTEM_DIR);
}
export function getMemoryUserDir(
agentId: string,
homeDir: string = homedir(),
): string {
return join(getMemoryFilesystemRoot(agentId, homeDir), MEMORY_USER_DIR);
}
function getMemoryStatePath(
agentId: string,
homeDir: string = homedir(),
): string {
return join(getMemoryFilesystemRoot(agentId, homeDir), MEMORY_FS_STATE_FILE);
}
export function ensureMemoryFilesystemDirs(
agentId: string,
homeDir: string = homedir(),
): void {
const root = getMemoryFilesystemRoot(agentId, homeDir);
const systemDir = getMemorySystemDir(agentId, homeDir);
const userDir = getMemoryUserDir(agentId, homeDir);
if (!existsSync(root)) {
mkdirSync(root, { recursive: true });
}
if (!existsSync(systemDir)) {
mkdirSync(systemDir, { recursive: true });
}
if (!existsSync(userDir)) {
mkdirSync(userDir, { recursive: true });
}
}
function hashContent(content: string): string {
return createHash("sha256").update(content).digest("hex");
}
function loadSyncState(
agentId: string,
homeDir: string = homedir(),
): SyncState {
const statePath = getMemoryStatePath(agentId, homeDir);
if (!existsSync(statePath)) {
return {
systemBlocks: {},
systemFiles: {},
userBlocks: {},
userFiles: {},
userBlockIds: {},
lastSync: null,
};
}
try {
const raw = readFileSync(statePath, "utf-8");
const parsed = JSON.parse(raw) as Partial<SyncState> & {
blocks?: Record<string, string>;
files?: Record<string, string>;
};
return {
systemBlocks: parsed.systemBlocks || parsed.blocks || {},
systemFiles: parsed.systemFiles || parsed.files || {},
userBlocks: parsed.userBlocks || {},
userFiles: parsed.userFiles || {},
userBlockIds: parsed.userBlockIds || {},
lastSync: parsed.lastSync || null,
};
} catch {
return {
systemBlocks: {},
systemFiles: {},
userBlocks: {},
userFiles: {},
userBlockIds: {},
lastSync: null,
};
}
}
async function saveSyncState(
state: SyncState,
agentId: string,
homeDir: string = homedir(),
) {
const statePath = getMemoryStatePath(agentId, homeDir);
await writeFile(statePath, JSON.stringify(state, null, 2));
}
async function scanMdFiles(dir: string, baseDir = dir): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const results: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...(await scanMdFiles(fullPath, baseDir)));
} else if (entry.isFile() && entry.name.endsWith(".md")) {
results.push(relative(baseDir, fullPath));
}
}
return results;
}
function labelFromRelativePath(relativePath: string): string {
const normalized = relativePath.replace(/\\/g, "/");
return normalized.replace(/\.md$/, "");
}
async function readMemoryFiles(
dir: string,
): Promise<Map<string, { content: string; path: string }>> {
const files = await scanMdFiles(dir);
const entries = new Map<string, { content: string; path: string }>();
for (const relativePath of files) {
const label = labelFromRelativePath(relativePath);
const fullPath = join(dir, relativePath);
const content = await readFile(fullPath, "utf-8");
entries.set(label, { content, path: fullPath });
}
return entries;
}
async function ensureFilePath(filePath: string) {
const parent = dirname(filePath);
if (!existsSync(parent)) {
mkdirSync(parent, { recursive: true });
}
}
async function writeMemoryFile(dir: string, label: string, content: string) {
const filePath = join(dir, `${label}.md`);
await ensureFilePath(filePath);
await writeFile(filePath, content, "utf-8");
}
async function deleteMemoryFile(dir: string, label: string) {
const filePath = join(dir, `${label}.md`);
if (existsSync(filePath)) {
await unlink(filePath);
}
}
async function fetchAgentBlocks(agentId: string): Promise<Block[]> {
const client = await getClient();
const blocksResponse = await client.agents.blocks.list(agentId);
const blocks = Array.isArray(blocksResponse)
? blocksResponse
: (blocksResponse as { items?: Block[] }).items ||
(blocksResponse as { blocks?: Block[] }).blocks ||
[];
return blocks;
}
export function renderMemoryFilesystemTree(
systemLabels: string[],
userLabels: string[],
): string {
type TreeNode = { children: Map<string, TreeNode>; isFile: boolean };
const makeNode = (): TreeNode => ({ children: new Map(), isFile: false });
const root = makeNode();
const insertPath = (base: string, label: string) => {
const parts = [base, ...label.split("/")];
let current = root;
for (const [i, partName] of parts.entries()) {
const part = i === parts.length - 1 ? `${partName}.md` : partName;
if (!current.children.has(part)) {
current.children.set(part, makeNode());
}
current = current.children.get(part) as TreeNode;
if (i === parts.length - 1) {
current.isFile = true;
}
}
};
for (const label of systemLabels) {
insertPath(MEMORY_SYSTEM_DIR, label);
}
for (const label of userLabels) {
insertPath(MEMORY_USER_DIR, label);
}
if (!root.children.has(MEMORY_SYSTEM_DIR)) {
root.children.set(MEMORY_SYSTEM_DIR, makeNode());
}
if (!root.children.has(MEMORY_USER_DIR)) {
root.children.set(MEMORY_USER_DIR, makeNode());
}
const sortedEntries = (node: TreeNode) => {
const entries = Array.from(node.children.entries());
return entries.sort(([nameA, nodeA], [nameB, nodeB]) => {
if (nodeA.isFile !== nodeB.isFile) {
return nodeA.isFile ? 1 : -1;
}
return nameA.localeCompare(nameB);
});
};
const lines: string[] = ["/memory/"];
const render = (node: TreeNode, prefix: string) => {
const entries = sortedEntries(node);
entries.forEach(([name, child], index) => {
const isLast = index === entries.length - 1;
const branch = isLast ? "└──" : "├──";
lines.push(`${prefix}${branch} ${name}${child.isFile ? "" : "/"}`);
if (child.children.size > 0) {
const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
render(child, nextPrefix);
}
});
};
render(root, "");
return lines.join("\n");
}
function buildStateHashes(
systemBlocks: Map<string, { value: string }>,
systemFiles: Map<string, { content: string }>,
userBlocks: Map<string, { value: string }>,
userFiles: Map<string, { content: string }>,
userBlockIds: Record<string, string>,
): SyncState {
const systemBlockHashes: Record<string, string> = {};
const systemFileHashes: Record<string, string> = {};
const userBlockHashes: Record<string, string> = {};
const userFileHashes: Record<string, string> = {};
systemBlocks.forEach((block, label) => {
systemBlockHashes[label] = hashContent(block.value || "");
});
systemFiles.forEach((file, label) => {
systemFileHashes[label] = hashContent(file.content || "");
});
userBlocks.forEach((block, label) => {
userBlockHashes[label] = hashContent(block.value || "");
});
userFiles.forEach((file, label) => {
userFileHashes[label] = hashContent(file.content || "");
});
return {
systemBlocks: systemBlockHashes,
systemFiles: systemFileHashes,
userBlocks: userBlockHashes,
userFiles: userFileHashes,
userBlockIds,
lastSync: new Date().toISOString(),
};
}
export async function syncMemoryFilesystem(
agentId: string,
options: { homeDir?: string; resolutions?: MemorySyncResolution[] } = {},
): Promise<MemorySyncResult> {
const homeDir = options.homeDir ?? homedir();
ensureMemoryFilesystemDirs(agentId, homeDir);
const systemDir = getMemorySystemDir(agentId, homeDir);
const userDir = getMemoryUserDir(agentId, homeDir);
const systemFiles = await readMemoryFiles(systemDir);
const userFiles = await readMemoryFiles(userDir);
systemFiles.delete(MEMORY_FILESYSTEM_BLOCK_LABEL);
const attachedBlocks = await fetchAgentBlocks(agentId);
const systemBlockMap = new Map(
attachedBlocks
.filter((block) => block.label)
.map((block) => [block.label as string, block]),
);
systemBlockMap.delete(MEMORY_FILESYSTEM_BLOCK_LABEL);
const lastState = loadSyncState(agentId, homeDir);
const conflicts: MemorySyncConflict[] = [];
const updatedBlocks: string[] = [];
const createdBlocks: string[] = [];
const deletedBlocks: string[] = [];
const updatedFiles: string[] = [];
const createdFiles: string[] = [];
const deletedFiles: string[] = [];
const resolutions = new Map(
(options.resolutions ?? []).map((resolution) => [
resolution.label,
resolution,
]),
);
const client = await getClient();
const userBlockIds = { ...lastState.userBlockIds };
const userBlockMap = new Map<string, Block>();
for (const [label, blockId] of Object.entries(userBlockIds)) {
try {
const block = await client.blocks.retrieve(blockId);
userBlockMap.set(label, block as Block);
} catch {
delete userBlockIds[label];
}
}
const systemLabels = new Set<string>([
...Array.from(systemFiles.keys()),
...Array.from(systemBlockMap.keys()),
...Object.keys(lastState.systemBlocks),
...Object.keys(lastState.systemFiles),
]);
for (const label of Array.from(systemLabels).sort()) {
if (MANAGED_BLOCK_LABELS.has(label)) {
continue;
}
const fileEntry = systemFiles.get(label);
const blockEntry = systemBlockMap.get(label);
const fileHash = fileEntry ? hashContent(fileEntry.content) : null;
const blockHash = blockEntry ? hashContent(blockEntry.value || "") : null;
const lastFileHash = lastState.systemFiles[label] || null;
const lastBlockHash = lastState.systemBlocks[label] || null;
const fileChanged = fileHash !== lastFileHash;
const blockChanged = blockHash !== lastBlockHash;
const resolution = resolutions.get(label);
if (fileEntry && !blockEntry) {
if (lastBlockHash && !fileChanged) {
// Block was deleted elsewhere; delete file.
await deleteMemoryFile(systemDir, label);
deletedFiles.push(label);
continue;
}
// Create block from file
const createdBlock = await client.blocks.create({
label,
value: fileEntry.content,
description: `Memory block: ${label}`,
limit: 20000,
});
if (createdBlock.id) {
await client.agents.blocks.attach(createdBlock.id, {
agent_id: agentId,
});
}
createdBlocks.push(label);
continue;
}
if (!fileEntry && blockEntry) {
if (lastFileHash && !blockChanged) {
// File deleted, block unchanged -> delete block
if (blockEntry.id) {
await client.agents.blocks.detach(blockEntry.id, {
agent_id: agentId,
});
}
deletedBlocks.push(label);
continue;
}
// Create file from block
await writeMemoryFile(systemDir, label, blockEntry.value || "");
createdFiles.push(label);
continue;
}
if (!fileEntry || !blockEntry) {
continue;
}
if (fileChanged && blockChanged && !resolution) {
conflicts.push({
label,
blockValue: blockEntry.value || "",
fileValue: fileEntry.content,
});
continue;
}
if (resolution?.resolution === "file") {
await client.agents.blocks.update(label, {
agent_id: agentId,
value: fileEntry.content,
});
updatedBlocks.push(label);
continue;
}
if (resolution?.resolution === "block") {
await writeMemoryFile(systemDir, label, blockEntry.value || "");
updatedFiles.push(label);
continue;
}
if (fileChanged && !blockChanged) {
await client.agents.blocks.update(label, {
agent_id: agentId,
value: fileEntry.content,
});
updatedBlocks.push(label);
continue;
}
if (!fileChanged && blockChanged) {
await writeMemoryFile(systemDir, label, blockEntry.value || "");
updatedFiles.push(label);
}
}
const userLabels = new Set<string>([
...Array.from(userFiles.keys()),
...Array.from(userBlockMap.keys()),
...Object.keys(lastState.userBlocks),
...Object.keys(lastState.userFiles),
]);
for (const label of Array.from(userLabels).sort()) {
const fileEntry = userFiles.get(label);
const blockEntry = userBlockMap.get(label);
const fileHash = fileEntry ? hashContent(fileEntry.content) : null;
const blockHash = blockEntry ? hashContent(blockEntry.value || "") : null;
const lastFileHash = lastState.userFiles[label] || null;
const lastBlockHash = lastState.userBlocks[label] || null;
const fileChanged = fileHash !== lastFileHash;
const blockChanged = blockHash !== lastBlockHash;
const resolution = resolutions.get(label);
if (fileEntry && !blockEntry) {
if (lastBlockHash && !fileChanged) {
// Block was deleted elsewhere; delete file.
await deleteMemoryFile(userDir, label);
deletedFiles.push(label);
delete userBlockIds[label];
continue;
}
const createdBlock = await client.blocks.create({
label,
value: fileEntry.content,
description: `Memory block: ${label}`,
limit: 20000,
});
if (createdBlock.id) {
userBlockIds[label] = createdBlock.id;
userBlockMap.set(label, createdBlock as Block);
}
createdBlocks.push(label);
continue;
}
if (!fileEntry && blockEntry) {
if (lastFileHash && !blockChanged) {
// File deleted, block unchanged -> delete block
if (blockEntry.id) {
await client.blocks.delete(blockEntry.id);
}
deletedBlocks.push(label);
delete userBlockIds[label];
continue;
}
await writeMemoryFile(userDir, label, blockEntry.value || "");
createdFiles.push(label);
continue;
}
if (!fileEntry || !blockEntry) {
continue;
}
if (fileChanged && blockChanged && !resolution) {
conflicts.push({
label,
blockValue: blockEntry.value || "",
fileValue: fileEntry.content,
});
continue;
}
if (resolution?.resolution === "file") {
if (blockEntry.id) {
await client.blocks.update(blockEntry.id, {
value: fileEntry.content,
label,
});
}
updatedBlocks.push(label);
continue;
}
if (resolution?.resolution === "block") {
await writeMemoryFile(userDir, label, blockEntry.value || "");
updatedFiles.push(label);
continue;
}
if (fileChanged && !blockChanged) {
if (blockEntry.id) {
await client.blocks.update(blockEntry.id, {
value: fileEntry.content,
label,
});
}
updatedBlocks.push(label);
continue;
}
if (!fileChanged && blockChanged) {
await writeMemoryFile(userDir, label, blockEntry.value || "");
updatedFiles.push(label);
}
}
if (conflicts.length === 0) {
const updatedBlocksList = await fetchAgentBlocks(agentId);
const updatedSystemBlockMap = new Map(
updatedBlocksList
.filter(
(block) =>
block.label && block.label !== MEMORY_FILESYSTEM_BLOCK_LABEL,
)
.map((block) => [block.label as string, { value: block.value || "" }]),
);
const updatedSystemFilesMap = await readMemoryFiles(systemDir);
updatedSystemFilesMap.delete(MEMORY_FILESYSTEM_BLOCK_LABEL);
const updatedUserFilesMap = await readMemoryFiles(userDir);
const refreshedUserBlocks = new Map<string, { value: string }>();
for (const [label, blockId] of Object.entries(userBlockIds)) {
try {
const block = await client.blocks.retrieve(blockId);
refreshedUserBlocks.set(label, { value: block.value || "" });
} catch {
delete userBlockIds[label];
}
}
const nextState = buildStateHashes(
updatedSystemBlockMap,
updatedSystemFilesMap,
refreshedUserBlocks,
updatedUserFilesMap,
userBlockIds,
);
await saveSyncState(nextState, agentId, homeDir);
}
return {
updatedBlocks,
createdBlocks,
deletedBlocks,
updatedFiles,
createdFiles,
deletedFiles,
conflicts,
};
}
export async function updateMemoryFilesystemBlock(
agentId: string,
homeDir: string = homedir(),
) {
const systemDir = getMemorySystemDir(agentId, homeDir);
const userDir = getMemoryUserDir(agentId, homeDir);
const systemFiles = await readMemoryFiles(systemDir);
const userFiles = await readMemoryFiles(userDir);
const tree = renderMemoryFilesystemTree(
Array.from(systemFiles.keys()).filter(
(label) => label !== MEMORY_FILESYSTEM_BLOCK_LABEL,
),
Array.from(userFiles.keys()),
);
const client = await getClient();
await client.agents.blocks.update(MEMORY_FILESYSTEM_BLOCK_LABEL, {
agent_id: agentId,
value: tree,
});
await writeMemoryFile(systemDir, MEMORY_FILESYSTEM_BLOCK_LABEL, tree);
}
export async function ensureMemoryFilesystemBlock(agentId: string) {
const client = await getClient();
const blocks = await fetchAgentBlocks(agentId);
const exists = blocks.some(
(block) => block.label === MEMORY_FILESYSTEM_BLOCK_LABEL,
);
if (exists) {
return;
}
const createdBlock = await client.blocks.create({
label: MEMORY_FILESYSTEM_BLOCK_LABEL,
value: "/memory/",
description: "Filesystem view of memory blocks",
limit: 20000,
read_only: true,
});
if (createdBlock.id) {
await client.agents.blocks.attach(createdBlock.id, { agent_id: agentId });
}
}
export async function refreshMemoryFilesystemTree(
agentId: string,
homeDir: string = homedir(),
) {
ensureMemoryFilesystemDirs(agentId, homeDir);
await updateMemoryFilesystemBlock(agentId, homeDir);
}
export async function collectMemorySyncConflicts(
agentId: string,
homeDir: string = homedir(),
): Promise<MemorySyncConflict[]> {
const result = await syncMemoryFilesystem(agentId, { homeDir });
return result.conflicts;
}
export function formatMemorySyncSummary(result: MemorySyncResult): string {
const lines = ["Memory filesystem sync complete:"];
const pushCount = (label: string, count: number) => {
if (count > 0) {
lines.push(`${label}: ${count}`);
}
};
pushCount("Blocks updated", result.updatedBlocks.length);
pushCount("Blocks created", result.createdBlocks.length);
pushCount("Blocks deleted", result.deletedBlocks.length);
pushCount("Files updated", result.updatedFiles.length);
pushCount("Files created", result.createdFiles.length);
pushCount("Files deleted", result.deletedFiles.length);
if (result.conflicts.length > 0) {
lines.push(`⎿ Conflicts: ${result.conflicts.length}`);
}
return lines.join("\n");
}
/**
* Detach the memory_filesystem block from an agent.
* Used when disabling memfs.
*/
export async function detachMemoryFilesystemBlock(
agentId: string,
): Promise<void> {
const client = await getClient();
const blocks = await fetchAgentBlocks(agentId);
const memfsBlock = blocks.find(
(block) => block.label === MEMORY_FILESYSTEM_BLOCK_LABEL,
);
if (memfsBlock?.id) {
await client.agents.blocks.detach(memfsBlock.id, { agent_id: agentId });
}
}

View File

@@ -266,3 +266,49 @@ export async function updateAgentSystemPrompt(
};
}
}
/**
* Updates an agent's system prompt to include or exclude the memfs addon section.
*
* @param agentId - The agent ID to update
* @param enableMemfs - Whether to enable (add) or disable (remove) the memfs addon
* @returns Result with success status and message
*/
export async function updateAgentSystemPromptMemfs(
agentId: string,
enableMemfs: boolean,
): Promise<SystemPromptUpdateResult> {
try {
const client = await getClient();
const agent = await client.agents.retrieve(agentId);
let currentSystemPrompt = agent.system || "";
const { SYSTEM_PROMPT_MEMFS_ADDON } = await import("./promptAssets");
// Remove any existing memfs addon section (to avoid duplicates)
// Look for the "## Memory Filesystem" header
const memfsHeaderRegex = /\n## Memory Filesystem[\s\S]*?(?=\n# |$)/;
currentSystemPrompt = currentSystemPrompt.replace(memfsHeaderRegex, "");
// If enabling, append the memfs addon
if (enableMemfs) {
currentSystemPrompt = `${currentSystemPrompt}${SYSTEM_PROMPT_MEMFS_ADDON}`;
}
await client.agents.update(agentId, {
system: currentSystemPrompt,
});
return {
success: true,
message: enableMemfs
? "System prompt updated to include Memory Filesystem section"
: "System prompt updated to remove Memory Filesystem section",
};
} catch (error) {
return {
success: false,
message: `Failed to update system prompt memfs: ${error instanceof Error ? error.message : String(error)}`,
};
}
}

View File

@@ -12,6 +12,7 @@ import lettaCodexPrompt from "./prompts/letta_codex.md";
import lettaGeminiPrompt from "./prompts/letta_gemini.md";
import loadedSkillsPrompt from "./prompts/loaded_skills.mdx";
import memoryCheckReminder from "./prompts/memory_check_reminder.txt";
import memoryFilesystemPrompt from "./prompts/memory_filesystem.mdx";
import personaPrompt from "./prompts/persona.mdx";
import personaClaudePrompt from "./prompts/persona_claude.mdx";
import personaKawaiiPrompt from "./prompts/persona_kawaii.mdx";
@@ -24,8 +25,10 @@ import skillUnloadReminder from "./prompts/skill_unload_reminder.txt";
import skillsPrompt from "./prompts/skills.mdx";
import stylePrompt from "./prompts/style.mdx";
import systemPrompt from "./prompts/system_prompt.txt";
import systemPromptMemfsAddon from "./prompts/system_prompt_memfs.txt";
export const SYSTEM_PROMPT = systemPrompt;
export const SYSTEM_PROMPT_MEMFS_ADDON = systemPromptMemfsAddon;
export const PLAN_MODE_REMINDER = planModeReminder;
export const SKILL_UNLOAD_REMINDER = skillUnloadReminder;
export const SKILL_CREATOR_PROMPT = skillCreatorModePrompt;
@@ -43,6 +46,7 @@ export const MEMORY_PROMPTS: Record<string, string> = {
"project.mdx": projectPrompt,
"skills.mdx": skillsPrompt,
"loaded_skills.mdx": loadedSkillsPrompt,
"memory_filesystem.mdx": memoryFilesystemPrompt,
"style.mdx": stylePrompt,
};

View File

@@ -0,0 +1,7 @@
---
label: memory_filesystem
description: Filesystem view of memory blocks (system + user)
limit: 20000
---
/memory/

View File

@@ -39,4 +39,4 @@ How to use Skills:
- Then, navigate and discover additional linked files in its directory as needed. Don't load additional files immediately, only load them when needed.
- When the task is completed, unload irrelevant skills using the Skill tool with `command: "unload"`.
- After creating a new skill, use `command: "refresh"` to re-scan the skills directory and update the available skills list.
IMPORTANT: Always unload irrelevant skills using the Skill tool to free up context space.
IMPORTANT: Always unload irrelevant skills using the Skill tool to free up context space.

View File

@@ -0,0 +1,34 @@
## Memory Filesystem
Your memory blocks are synchronized with a filesystem tree at `~/.letta/agents/<agent-id>/memory/`. This provides:
- **Persistent storage**: Memory edits survive server restarts and can be version-controlled
- **Two-way sync**: Changes to files sync to memory blocks, and vice versa
- **Visibility**: A `memory_filesystem` block shows the tree structure of all memory files
### Structure
```
~/.letta/agents/<agent-id>/memory/
├── system/ # System prompt memory blocks (attached to agent)
│ ├── persona/ # Your identity and approach
│ ├── human.md # What you know about the user
│ └── ...
├── user/ # User notes (detached blocks, not in system prompt)
│ └── ...
└── .sync-state.json # Internal sync state (do not edit)
```
### System vs User
- **system/**: Memory blocks attached to your system prompt. These influence your behavior and are always loaded.
- **user/**: Detached blocks for reference/notes. Created as blocks but NOT attached to the agent (similar to the "note" tool pattern).
### Sync Behavior
- **Startup**: Automatic sync when the CLI starts
- **After memory edits**: Automatic sync after using memory tools
- **Manual**: Run `/memory-sync` to sync on demand
- **Conflicts**: If both file and block changed, you'll be prompted to choose which version to keep
### How It Works
1. Each `.md` file path maps to a block label (e.g., `system/persona/git_safety.md` → label `persona/git_safety`)
2. File content syncs with block `value`
3. Changes detected via content hashing
4. The `memory_filesystem` block auto-updates with the tree view