fix: add missing memfs init steps in headless mode + refactor (#941)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Devansh Jain
2026-02-12 17:33:51 -08:00
committed by GitHub
parent 5a696e6116
commit 36c21939a9
4 changed files with 112 additions and 113 deletions

View File

@@ -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<ApplyMemfsFlagsResult> {
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,
};
}

View File

@@ -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,
);

View File

@@ -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

View File

@@ -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<void> {
// 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