From 36c21939a94380b69cd70df1d76f158a0bc0b384 Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:33:51 -0800 Subject: [PATCH] fix: add missing memfs init steps in headless mode + refactor (#941) Co-authored-by: Letta --- src/agent/memoryFilesystem.ts | 88 ++++++++++++++++++++++++++++++++++- src/cli/App.tsx | 34 ++------------ src/headless.ts | 55 ++++++---------------- src/index.ts | 48 +++---------------- 4 files changed, 112 insertions(+), 113 deletions(-) diff --git a/src/agent/memoryFilesystem.ts b/src/agent/memoryFilesystem.ts index de0a92a..fbcc47c 100644 --- a/src/agent/memoryFilesystem.ts +++ b/src/agent/memoryFilesystem.ts @@ -2,7 +2,9 @@ * Memory filesystem helpers. * * With git-backed memory, most sync/hash logic is removed. - * This module retains: directory helpers and tree rendering. + * This module retains: directory helpers, tree rendering, and + * the shared memfs initialization logic used by both interactive + * and headless code paths. */ import { existsSync, mkdirSync } from "node:fs"; @@ -129,3 +131,87 @@ export function renderMemoryFilesystemTree( return lines.join("\n"); } + +// ----- Shared memfs initialization ----- + +export interface ApplyMemfsFlagsResult { + /** Whether memfs was enabled, disabled, or unchanged */ + action: "enabled" | "disabled" | "unchanged"; + /** Path to the memory directory (when enabled) */ + memoryDir?: string; + /** Summary from git pull (when pullOnExistingRepo is true and repo already existed) */ + pullSummary?: string; +} + +/** + * Apply --memfs / --no-memfs CLI flags (or /memfs enable) to an agent. + * + * Shared between interactive (index.ts), headless (headless.ts), and + * the /memfs enable command (App.tsx) to avoid duplicating the setup logic. + * + * Steps when enabling: + * 1. Validate Letta Cloud requirement + * 2. Persist memfs setting + * 3. Detach old API-based memory tools + * 4. Update system prompt to include memfs section + * 5. Add git-memory-enabled tag + clone/pull repo + * + * @throws {Error} if Letta Cloud validation fails or git setup fails + */ +export async function applyMemfsFlags( + agentId: string, + memfsFlag: boolean | undefined, + noMemfsFlag: boolean | undefined, + options?: { pullOnExistingRepo?: boolean }, +): Promise { + const { getServerUrl } = await import("./client"); + const { settingsManager } = await import("../settings-manager"); + + // 1. Validate + persist setting + if (memfsFlag) { + const serverUrl = getServerUrl(); + if (!serverUrl.includes("api.letta.com")) { + throw new Error( + "--memfs is only available on Letta Cloud (api.letta.com).", + ); + } + settingsManager.setMemfsEnabled(agentId, true); + } else if (noMemfsFlag) { + settingsManager.setMemfsEnabled(agentId, false); + } + + const isEnabled = settingsManager.isMemfsEnabled(agentId); + + // 2. Detach old API-based memory tools when enabling + if (isEnabled && memfsFlag) { + const { detachMemoryTools } = await import("../tools/toolset"); + await detachMemoryTools(agentId); + } + + // 3. Update system prompt to include/exclude memfs section + if (memfsFlag || noMemfsFlag) { + const { updateAgentSystemPromptMemfs } = await import("./modify"); + await updateAgentSystemPromptMemfs(agentId, isEnabled); + } + + // 4. Add git tag + clone/pull repo + let pullSummary: string | undefined; + if (isEnabled) { + const { addGitMemoryTag, isGitRepo, cloneMemoryRepo, pullMemory } = + await import("./memoryGit"); + await addGitMemoryTag(agentId); + if (!isGitRepo(agentId)) { + await cloneMemoryRepo(agentId); + } else if (options?.pullOnExistingRepo) { + const result = await pullMemory(agentId); + pullSummary = result.summary; + } + } + + const action = memfsFlag ? "enabled" : noMemfsFlag ? "disabled" : "unchanged"; + return { + action, + memoryDir: isEnabled ? getMemoryFilesystemRoot(agentId) : undefined, + pullSummary, + }; +} diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 11b9312..1c17935 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -7331,15 +7331,6 @@ export default function App({ } if (subcommand === "enable") { - // memfs requires Letta Cloud (git memfs not supported on self-hosted) - const serverUrl = getServerUrl(); - if (!serverUrl.includes("api.letta.com")) { - cmd.fail( - "Memory filesystem is only available on Letta Cloud (api.letta.com).", - ); - return { submitted: true }; - } - updateMemorySyncCommand( cmdId, "Enabling memory filesystem...", @@ -7350,30 +7341,13 @@ export default function App({ setCommandRunning(true); try { - // 1. Detach memory tools from agent - const { detachMemoryTools } = await import("../tools/toolset"); - await detachMemoryTools(agentId); - - // 2. Update settings - settingsManager.setMemfsEnabled(agentId, true); - - // 3. Update system prompt to include memfs section - const { updateAgentSystemPromptMemfs } = await import( - "../agent/modify" + const { applyMemfsFlags } = await import( + "../agent/memoryFilesystem" ); - await updateAgentSystemPromptMemfs(agentId, true); - - // 4. Add git-memory-enabled tag and clone repo - const { addGitMemoryTag, isGitRepo, cloneMemoryRepo } = - await import("../agent/memoryGit"); - await addGitMemoryTag(agentId); - if (!isGitRepo(agentId)) { - await cloneMemoryRepo(agentId); - } - const memoryDir = getMemoryFilesystemRoot(agentId); + const result = await applyMemfsFlags(agentId, true, false); updateMemorySyncCommand( cmdId, - `Memory filesystem enabled (git-backed).\nPath: ${memoryDir}`, + `Memory filesystem enabled (git-backed).\nPath: ${result.memoryDir}`, true, msg, ); diff --git a/src/headless.ts b/src/headless.ts index 1e8c5ec..77074ed 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -14,7 +14,7 @@ import { isApprovalPendingError, isInvalidToolCallIdsError, } from "./agent/approval-recovery"; -import { getClient, getServerUrl } from "./agent/client"; +import { getClient } from "./agent/client"; import { setAgentContext, setConversationId } from "./agent/context"; import { createAgent } from "./agent/create"; import { ISOLATED_BLOCK_LABELS } from "./agent/memory"; @@ -678,52 +678,25 @@ export async function handleHeadlessCommand( const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent"; // Apply memfs flag if explicitly specified (memfs is opt-in via /memfs enable or --memfs) - if (memfsFlag) { - // memfs requires Letta Cloud (git memfs not supported on self-hosted) - const serverUrl = getServerUrl(); - if (!serverUrl.includes("api.letta.com")) { - console.error( - "--memfs is only available on Letta Cloud (api.letta.com).", - ); - process.exit(1); - } - settingsManager.setMemfsEnabled(agent.id, true); - } else if (noMemfsFlag) { - settingsManager.setMemfsEnabled(agent.id, false); - } - - // Ensure agent's system prompt includes/excludes memfs section to match setting - if (memfsFlag || noMemfsFlag) { - const { updateAgentSystemPromptMemfs } = await import("./agent/modify"); - await updateAgentSystemPromptMemfs( + try { + const { applyMemfsFlags } = await import("./agent/memoryFilesystem"); + const memfsResult = await applyMemfsFlags( agent.id, - settingsManager.isMemfsEnabled(agent.id), + memfsFlag, + noMemfsFlag, + { pullOnExistingRepo: true }, ); - } - - // Git-backed memory: clone or pull on startup (only if memfs is enabled) - if (settingsManager.isMemfsEnabled(agent.id)) { - try { - const { isGitRepo, cloneMemoryRepo, pullMemory } = await import( - "./agent/memoryGit" - ); - if (!isGitRepo(agent.id)) { - await cloneMemoryRepo(agent.id); - } else { - const result = await pullMemory(agent.id); - if (result.summary.includes("CONFLICT")) { - console.error( - "Memory has merge conflicts. Run in interactive mode to resolve.", - ); - process.exit(1); - } - } - } catch (error) { + if (memfsResult.pullSummary?.includes("CONFLICT")) { console.error( - `Memory git sync failed: ${error instanceof Error ? error.message : String(error)}`, + "Memory has merge conflicts. Run in interactive mode to resolve.", ); process.exit(1); } + } catch (error) { + console.error( + `Memory git sync failed: ${error instanceof Error ? error.message : String(error)}`, + ); + process.exit(1); } // Determine which blocks to isolate for the conversation diff --git a/src/index.ts b/src/index.ts index 4bacbf8..167a3a4 100755 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ import { APIError } from "@letta-ai/letta-client/core/error"; import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents"; import type { Message } from "@letta-ai/letta-client/resources/agents/messages"; import { getResumeData, type ResumeData } from "./agent/check-approval"; -import { getClient, getServerUrl } from "./agent/client"; +import { getClient } from "./agent/client"; import { setAgentContext, setConversationId as setContextConversationId, @@ -1683,46 +1683,12 @@ async function main(): Promise { // Apply memfs flag if explicitly specified (memfs is opt-in via /memfs enable or --memfs) const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent"; - if (memfsFlag) { - // memfs requires Letta Cloud (git memfs not supported on self-hosted) - const serverUrl = getServerUrl(); - if (!serverUrl.includes("api.letta.com")) { - console.error( - "--memfs is only available on Letta Cloud (api.letta.com).", - ); - process.exit(1); - } - settingsManager.setMemfsEnabled(agent.id, true); - } else if (noMemfsFlag) { - settingsManager.setMemfsEnabled(agent.id, false); - } - - // When memfs is being enabled via flag, detach old API-based memory tools - if (settingsManager.isMemfsEnabled(agent.id) && memfsFlag) { - const { detachMemoryTools } = await import("./tools/toolset"); - await detachMemoryTools(agent.id); - } - - // Ensure agent's system prompt includes/excludes memfs section to match setting - if (memfsFlag || noMemfsFlag) { - const { updateAgentSystemPromptMemfs } = await import( - "./agent/modify" - ); - await updateAgentSystemPromptMemfs( - agent.id, - settingsManager.isMemfsEnabled(agent.id), - ); - } - - // Git-backed memory: ensure tag + repo are set up - if (settingsManager.isMemfsEnabled(agent.id)) { - const { addGitMemoryTag, isGitRepo, cloneMemoryRepo } = await import( - "./agent/memoryGit" - ); - await addGitMemoryTag(agent.id); - if (!isGitRepo(agent.id)) { - await cloneMemoryRepo(agent.id); - } + try { + const { applyMemfsFlags } = await import("./agent/memoryFilesystem"); + await applyMemfsFlags(agent.id, memfsFlag, noMemfsFlag); + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); } // Check if we're resuming an existing agent