fix: add missing memfs init steps in headless mode + refactor (#941)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
48
src/index.ts
48
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<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
|
||||
|
||||
Reference in New Issue
Block a user