From baccc408ef29f78ed4f3a70819f60b8d7823d302 Mon Sep 17 00:00:00 2001 From: Ani Date: Thu, 19 Mar 2026 23:15:32 -0400 Subject: [PATCH] Add MemFS self-hosted fix documentation - reference/memfs-selfhosted-fix.patch: Implementation patch - reference/PR-memfs-selfhosted.md: PR description - E: Local-first initialization - F: Configurable backend support - Addresses subagent spawn failures on self-hosted --- reference/PR-memfs-selfhosted.md | 86 ++++++++++++++++++++++++++ reference/memfs-selfhosted-fix.patch | 91 ++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 reference/PR-memfs-selfhosted.md create mode 100644 reference/memfs-selfhosted-fix.patch diff --git a/reference/PR-memfs-selfhosted.md b/reference/PR-memfs-selfhosted.md new file mode 100644 index 0000000..970a10a --- /dev/null +++ b/reference/PR-memfs-selfhosted.md @@ -0,0 +1,86 @@ +# PR: Fix MemFS for Self-Hosted and Configurable Backends + +## Problem + +Subagents fail to spawn on self-hosted Letta servers because `applyMemfsFlags` tries to `git clone` from `http://localhost:8283/v1/git/{agent_id}/state.git`, which returns HTTP 501 (Not Implemented) on self-hosted. + +**Error:** +``` +fatal: unable to access 'http://10.10.20.19:8283/v1/git/.../state.git/': +The requested URL returned error: 501 +``` + +## Root Cause + +The current code only checks `isLettaCloud()` but doesn't handle: +1. Self-hosted servers without git endpoint +2. Future Gitea/Codeberg backends +3. Graceful fallback when remote is unavailable + +## Solution + +### E: Local-First Initialization +- Always initialize local git repo first +- Try to pull/clone remote only if available +- Gracefully continue with local state if remote fails + +### F: Configurable Backend +- Add `LETTABOT_MEMFS_BACKEND` environment variable: + - `"cloud"` - Force Letta Cloud mode + - `"local"` - Force local-only (for Gitea/Codeberg) + - `"auto"` - Auto-detect (default) + - `"none"` - Disable remote sync + +## Changes + +### 1. `src/agent/memoryGit.ts` +- Add `initLocalMemoryRepo(agentId)` function +- Initializes local git repo with proper config +- Creates initial directory structure +- Installs pre-commit hooks + +### 2. `src/agent/memoryFilesystem.ts` +- Modify `applyMemfsFlags` to use local-first approach +- Check `LETTABOT_MEMFS_BACKEND` env var +- Handle 501/404 errors gracefully +- Continue with local state if remote unavailable + +## Usage + +### For Self-Hosted (current fix): +```bash +# No changes needed - auto-detects and uses local mode +export LETTABOT_MEMFS=true +lettabot start +``` + +### For Gitea Backend (future): +```bash +export LETTABOT_MEMFS=true +export LETTABOT_MEMFS_BACKEND=local +export LETTABOT_MEMFS_REMOTE=https://gitea.example.com/ani/memory.git +lettabot start +``` + +### For Letta Cloud (unchanged): +```bash +export LETTABOT_MEMFS=true +# Works as before with cloud git endpoint +``` + +## Testing + +1. **Self-hosted:** Subagents now spawn successfully +2. **Local repo created:** `~/.letta/agents/{id}/memory/.git/` +3. **Graceful degradation:** Works without remote sync + +## Backwards Compatibility + +- Letta Cloud users: No change +- Self-hosted users: Now works (was broken) +- Future Gitea/Codeberg: Supported via `LETTABOT_MEMFS_BACKEND` + +## Related + +Fixes subagent spawn failures on self-hosted servers. +Enables future Gitea/Codeberg integration. diff --git a/reference/memfs-selfhosted-fix.patch b/reference/memfs-selfhosted-fix.patch new file mode 100644 index 0000000..c63749f --- /dev/null +++ b/reference/memfs-selfhosted-fix.patch @@ -0,0 +1,91 @@ +--- a/src/agent/memoryGit.ts ++++ b/src/agent/memoryGit.ts +@@ -308,6 +308,45 @@ export function isGitRepo(agentId: string): boolean { + return existsSync(join(getMemoryRepoDir(agentId), ".git")); + } + ++/** ++ * Initialize a local memory repository without remote. ++ * Used for self-hosted or Gitea backends where cloud git endpoint isn't available. ++ */ ++export async function initLocalMemoryRepo(agentId: string): Promise { ++ const dir = getMemoryRepoDir(agentId); ++ ++ debugLog("memfs-git", `Initializing local repo at ${dir}`); ++ ++ if (!existsSync(dir)) { ++ mkdirSync(dir, { recursive: true }); ++ } ++ ++ // Initialize git repo ++ await runGit(dir, ["init"], undefined); ++ ++ // Configure user (required for commits) ++ await runGit(dir, ["config", "user.email", "ani@localhost"], undefined); ++ await runGit(dir, ["config", "user.name", "Ani"], undefined); ++ ++ // Create initial structure ++ const systemDir = join(dir, "system"); ++ if (!existsSync(systemDir)) { ++ mkdirSync(systemDir, { recursive: true }); ++ } ++ ++ // Install pre-commit hook ++ installPreCommitHook(dir); ++ ++ debugLog("memfs-git", "Local memory repo initialized"); ++} ++ + /** + * Clone the agent's state repo into the memory directory. + * +--- a/src/agent/memoryFilesystem.ts ++++ b/src/agent/memoryFilesystem.ts +@@ -250,13 +250,40 @@ export async function applyMemfsFlags( + // 4. Add git tag + clone/pull repo. + let pullSummary: string | undefined; + if (isEnabled) { +- const { addGitMemoryTag, isGitRepo, cloneMemoryRepo, pullMemory } = ++ const { addGitMemoryTag, isGitRepo, cloneMemoryRepo, pullMemory, initLocalMemoryRepo } = + await import("./memoryGit"); + await addGitMemoryTag( + agentId, + options?.agentTags ? { tags: options.agentTags } : undefined, + ); +- if (!isGitRepo(agentId) && (await isLettaCloud())) { +- await cloneMemoryRepo(agentId); ++ ++ // E: Local-first initialization ++ // F: Configurable backend support ++ const memfsBackend = process.env.LETTABOT_MEMFS_BACKEND || "auto"; ++ const isCloud = await isLettaCloud(); ++ ++ if (!isGitRepo(agentId)) { ++ if (memfsBackend === "none" || memfsBackend === "local") { ++ // Local-only mode (self-hosted, Gitea, Codeberg) ++ await initLocalMemoryRepo(agentId); ++ } else if (memfsBackend === "cloud" || (memfsBackend === "auto" && isCloud)) { ++ // Cloud mode (Letta Cloud) ++ await cloneMemoryRepo(agentId); ++ } else { ++ // Auto mode, self-hosted detected - use local init ++ await initLocalMemoryRepo(agentId); ++ } + } else if (options?.pullOnExistingRepo) { +- const result = await pullMemory(agentId); +- pullSummary = result.summary; ++ // Try to pull, but don't fail if remote unavailable ++ try { ++ const result = await pullMemory(agentId); ++ pullSummary = result.summary; ++ } catch (error) { ++ if (error.message?.includes("501") || error.message?.includes("404")) { ++ // Remote not available - continue with local ++ console.log("[memfs] Remote unavailable, using local state"); ++ } else { ++ throw error; ++ } ++ } + } + } + \ No newline at end of file