fix: make memfs opt-in and cloud-only (#915)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-02-11 13:22:49 -08:00
committed by GitHub
parent 94376a3233
commit 77b4746dc2
4 changed files with 72 additions and 33 deletions

View File

@@ -433,3 +433,25 @@ export async function addGitMemoryTag(agentId: string): Promise<void> {
);
}
}
/**
* Remove the git-memory-enabled tag from an agent.
*/
export async function removeGitMemoryTag(agentId: string): Promise<void> {
const client = await getClient();
try {
const agent = await client.agents.retrieve(agentId);
const tags = agent.tags || [];
if (tags.includes(GIT_MEMORY_ENABLED_TAG)) {
await client.agents.update(agentId, {
tags: tags.filter((t) => t !== GIT_MEMORY_ENABLED_TAG),
});
debugLog("memfs-git", `Removed ${GIT_MEMORY_ENABLED_TAG} tag`);
}
} catch (err) {
debugWarn(
"memfs-git",
`Failed to remove git-memory tag: ${err instanceof Error ? err.message : String(err)}`,
);
}
}

View File

@@ -6877,6 +6877,15 @@ 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...",
@@ -7041,9 +7050,25 @@ export default function App({
// 3. Update settings
settingsManager.setMemfsEnabled(agentId, false);
// 4. Remove git-memory-enabled tag from agent
const { removeGitMemoryTag } = await import("../agent/memoryGit");
await removeGitMemoryTag(agentId);
// 5. Move local memory dir to /tmp (backup, not delete)
let backupInfo = "";
const memoryDir = getMemoryFilesystemRoot(agentId);
if (existsSync(memoryDir)) {
const backupDir = join(
tmpdir(),
`letta-memfs-disable-${agentId}-${Date.now()}`,
);
renameSync(memoryDir, backupDir);
backupInfo = `\nLocal files backed up to ${backupDir}`;
}
updateMemorySyncCommand(
cmdId,
"Memory filesystem disabled. Memory tool re-attached.\nFiles on disk have been kept.",
`Memory filesystem disabled. Memory tool re-attached.${backupInfo}`,
true,
msg,
);

View File

@@ -14,7 +14,7 @@ import {
isApprovalPendingError,
isInvalidToolCallIdsError,
} from "./agent/approval-recovery";
import { getClient } from "./agent/client";
import { getClient, getServerUrl } from "./agent/client";
import { setAgentContext, setConversationId } from "./agent/context";
import { createAgent } from "./agent/create";
import { ISOLATED_BLOCK_LABELS } from "./agent/memory";
@@ -236,7 +236,6 @@ export async function handleHeadlessCommand(
// Resolve agent (same logic as interactive mode)
let agent: AgentState | null = null;
let isNewlyCreatedAgent = false;
let specifiedAgentId = values.agent as string | undefined;
let specifiedConversationId = values.conversation as string | undefined;
const useDefaultConv = values.default as boolean | undefined;
@@ -542,7 +541,6 @@ export async function handleHeadlessCommand(
}
agent = result.agent;
isNewlyCreatedAgent = true;
// Display extracted skills summary
if (result.skills && result.skills.length > 0) {
@@ -584,7 +582,6 @@ export async function handleHeadlessCommand(
};
const result = await createAgent(createOptions);
agent = result.agent;
isNewlyCreatedAgent = true;
}
// Priority 4: Try to resume from project settings (.letta/settings.local.json)
@@ -681,27 +678,23 @@ export async function handleHeadlessCommand(
const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent";
// Apply memfs flag if specified, or enable by default for new agents
// In headless mode, also enable for --agent since users expect full functionality
// 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);
} else if (isNewlyCreatedAgent && !isSubagent) {
// Enable memfs by default for newly created agents (but not subagents)
settingsManager.setMemfsEnabled(agent.id, true);
} else if (specifiedAgentId && !isSubagent) {
// Enable memfs by default when using --agent in headless mode
settingsManager.setMemfsEnabled(agent.id, true);
}
// Ensure agent's system prompt includes/excludes memfs section to match setting
if (
memfsFlag ||
noMemfsFlag ||
(isNewlyCreatedAgent && !isSubagent) ||
(specifiedAgentId && !isSubagent)
) {
if (memfsFlag || noMemfsFlag) {
const { updateAgentSystemPromptMemfs } = await import("./agent/modify");
await updateAgentSystemPromptMemfs(
agent.id,

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 } from "./agent/client";
import { getClient, getServerUrl } from "./agent/client";
import {
setAgentContext,
setConversationId as setContextConversationId,
@@ -1477,7 +1477,6 @@ async function main(): Promise<void> {
const { getModelUpdateArgs } = await import("./agent/model");
let agent: AgentState | null = null;
let isNewlyCreatedAgent = false;
// Priority 1: Import from AgentFile template (local file or registry)
if (fromAfFile) {
@@ -1505,7 +1504,6 @@ async function main(): Promise<void> {
}
agent = result.agent;
isNewlyCreatedAgent = true;
setAgentProvenance({
isNew: true,
blocks: [],
@@ -1608,7 +1606,6 @@ async function main(): Promise<void> {
baseTools,
);
agent = result.agent;
isNewlyCreatedAgent = true;
setAgentProvenance(result.provenance);
}
@@ -1686,28 +1683,30 @@ async function main(): Promise<void> {
// Set agent context for tools that need it (e.g., Skill tool)
setAgentContext(agent.id, skillsDirectory);
// Apply memfs flag if specified, or enable by default for new agents
// 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);
} else if (isNewlyCreatedAgent && !isSubagent) {
// Enable memfs by default for newly created agents (but not subagents)
settingsManager.setMemfsEnabled(agent.id, true);
}
// When memfs is being enabled, detach old API-based memory tools
if (
settingsManager.isMemfsEnabled(agent.id) &&
(memfsFlag || (isNewlyCreatedAgent && !isSubagent))
) {
// 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 || (isNewlyCreatedAgent && !isSubagent)) {
if (memfsFlag || noMemfsFlag) {
const { updateAgentSystemPromptMemfs } = await import(
"./agent/modify"
);