From c5033666b4a336629f3cd03c0689d87452f42a12 Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:59:45 -0800 Subject: [PATCH] perf: eliminate redundant API calls on startup (#1067) Co-authored-by: cpacker Co-authored-by: Letta --- src/agent/memoryFilesystem.ts | 25 +++++++++- src/agent/memoryGit.ts | 7 ++- src/headless.ts | 17 ------- src/index.ts | 90 ++++++++++++++++++----------------- 4 files changed, 76 insertions(+), 63 deletions(-) diff --git a/src/agent/memoryFilesystem.ts b/src/agent/memoryFilesystem.ts index f491485..164ea65 100644 --- a/src/agent/memoryFilesystem.ts +++ b/src/agent/memoryFilesystem.ts @@ -222,6 +222,26 @@ export async function applyMemfsFlags( if (isEnabled && (memfsFlag || shouldAutoEnableFromTag)) { const { detachMemoryTools } = await import("../tools/toolset"); await detachMemoryTools(agentId); + + // Migration (LET-7353): Remove legacy skills/loaded_skills blocks. + // These blocks are no longer used — skills are now injected via system reminders. + const { getClient } = await import("./client"); + const client = await getClient(); + for (const label of ["skills", "loaded_skills"]) { + try { + const block = await client.agents.blocks.retrieve(label, { + agent_id: agentId, + }); + if (block) { + await client.agents.blocks.detach(block.id, { + agent_id: agentId, + }); + await client.blocks.delete(block.id); + } + } catch { + // Block doesn't exist or already removed, skip + } + } } // Keep server-side state aligned with explicit disable. @@ -235,7 +255,10 @@ export async function applyMemfsFlags( if (isEnabled) { const { addGitMemoryTag, isGitRepo, cloneMemoryRepo, pullMemory } = await import("./memoryGit"); - await addGitMemoryTag(agentId); + await addGitMemoryTag( + agentId, + options?.agentTags ? { tags: options.agentTags } : undefined, + ); if (!isGitRepo(agentId)) { await cloneMemoryRepo(agentId); } else if (options?.pullOnExistingRepo) { diff --git a/src/agent/memoryGit.ts b/src/agent/memoryGit.ts index 530406b..284de96 100644 --- a/src/agent/memoryGit.ts +++ b/src/agent/memoryGit.ts @@ -453,10 +453,13 @@ export async function getMemoryGitStatus( * Add the git-memory-enabled tag to an agent. * This triggers the backend to create the git repo. */ -export async function addGitMemoryTag(agentId: string): Promise { +export async function addGitMemoryTag( + agentId: string, + prefetchedAgent?: { tags?: string[] | null }, +): Promise { const client = await getClient(); try { - const agent = await client.agents.retrieve(agentId); + const agent = prefetchedAgent ?? (await client.agents.retrieve(agentId)); const tags = agent.tags || []; if (!tags.includes(GIT_MEMORY_ENABLED_TAG)) { await client.agents.update(agentId, { diff --git a/src/headless.ts b/src/headless.ts index a60e1ca..ec63ddb 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -1047,23 +1047,6 @@ export async function handleHeadlessCommand( }); } - // Migration (LET-7353): Remove legacy skills/loaded_skills blocks - for (const label of ["skills", "loaded_skills"]) { - try { - const block = await client.agents.blocks.retrieve(label, { - agent_id: agent.id, - }); - if (block) { - await client.agents.blocks.detach(block.id, { - agent_id: agent.id, - }); - await client.blocks.delete(block.id); - } - } catch { - // Block doesn't exist or already removed, skip - } - } - // Set agent context for tools that need it (e.g., Skill tool, Task tool) setAgentContext(agent.id, skillsDirectory, resolvedSkillSources); diff --git a/src/index.ts b/src/index.ts index f38be75..ebe2920 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1074,6 +1074,10 @@ async function main(): Promise { const [selectedGlobalAgentId, setSelectedGlobalAgentId] = useState< string | null >(null); + // Cache agent object from Phase 1 validation to avoid redundant re-fetch in Phase 2 + const [validatedAgent, setValidatedAgent] = useState( + null, + ); // Track agent and conversation for conversation selector (--resume flag) const [resumeAgentId, setResumeAgentId] = useState(null); const [resumeAgentName, setResumeAgentName] = useState(null); @@ -1384,11 +1388,13 @@ async function main(): Promise { } // Step 1: Check local project LRU (session helpers centralize legacy fallback) + // Cache the retrieved agent to avoid redundant re-fetch in init() const localAgentId = settingsManager.getLocalLastAgentId(process.cwd()); let localAgentExists = false; + let cachedAgent: AgentState | null = null; if (localAgentId) { try { - await client.agents.retrieve(localAgentId); + cachedAgent = await client.agents.retrieve(localAgentId); localAgentExists = true; } catch { setFailedAgentMessage( @@ -1402,7 +1408,7 @@ async function main(): Promise { let globalAgentExists = false; if (globalAgentId && globalAgentId !== localAgentId) { try { - await client.agents.retrieve(globalAgentId); + cachedAgent = await client.agents.retrieve(globalAgentId); globalAgentExists = true; } catch { // Global agent doesn't exist either @@ -1432,6 +1438,9 @@ async function main(): Promise { switch (target.action) { case "resume": setSelectedGlobalAgentId(target.agentId); + if (cachedAgent && cachedAgent.id === target.agentId) { + setValidatedAgent(cachedAgent); + } // Don't set selectedConversationId — DEFAULT PATH uses default conv. // Conversation restoration is handled by --continue path instead. setLoadingState("assembling"); @@ -1695,10 +1704,13 @@ async function main(): Promise { // Priority 4: Try to resume from project settings LRU (.letta/settings.local.json) // Note: If LRU retrieval failed in early validation, we already showed selector and returned - // This block handles the case where we have a valid resumingAgentId from early validation + // Use cached agent from Phase 1 validation when available to avoid redundant API call if (!agent && resumingAgentId) { try { - agent = await client.agents.retrieve(resumingAgentId); + agent = + validatedAgent && validatedAgent.id === resumingAgentId + ? validatedAgent + : await client.agents.retrieve(resumingAgentId); } catch (error) { // Agent disappeared between validation and now - show selector console.error( @@ -1746,38 +1758,19 @@ async function main(): Promise { settingsManager.updateLocalProjectSettings({ lastAgent: agent.id }); settingsManager.updateSettings({ lastAgent: agent.id }); - // Migration (LET-7353): Remove legacy skills/loaded_skills blocks - // These blocks are no longer used - skills are now injected via system reminders - for (const label of ["skills", "loaded_skills"]) { - try { - const block = await client.agents.blocks.retrieve(label, { - agent_id: agent.id, - }); - if (block) { - await client.agents.blocks.detach(block.id, { - agent_id: agent.id, - }); - await client.blocks.delete(block.id); - } - } catch { - // Block doesn't exist or already removed, skip - } - } - // Set agent context for tools that need it (e.g., Skill tool) setAgentContext(agent.id, skillsDirectory, resolvedSkillSources); - // Apply memfs flags and auto-enable from server tag when local settings are missing. + // Start memfs sync early — awaited in parallel with getResumeData below const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent"; - try { - const { applyMemfsFlags } = await import("./agent/memoryFilesystem"); - await applyMemfsFlags(agent.id, memfsFlag, noMemfsFlag, { - agentTags: agent.tags, - }); - } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - } + const agentId = agent.id; + const agentTags = agent.tags ?? undefined; + const memfsSyncPromise = import("./agent/memoryFilesystem").then( + ({ applyMemfsFlags }) => + applyMemfsFlags(agentId, memfsFlag, noMemfsFlag, { + agentTags, + }), + ); // Check if we're resuming an existing agent // We're resuming if: @@ -1851,12 +1844,10 @@ async function main(): Promise { setResumedExistingConversation(true); try { // Load message history and pending approvals from the conversation - // Re-fetch agent to get fresh message_ids for accurate pending approval detection setLoadingState("checking"); - const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, - freshAgent, + agent, specifiedConversationId, ); setResumeData(data); @@ -1890,12 +1881,10 @@ async function main(): Promise { // If it no longer exists, fall back to creating new try { // Load message history and pending approvals from the conversation - // Re-fetch agent to get fresh message_ids for accurate pending approval detection setLoadingState("checking"); - const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, - freshAgent, + agent, lastSession.conversationId, ); // Only set state after validation succeeds @@ -1933,10 +1922,9 @@ async function main(): Promise { // User selected a specific conversation from the --resume selector try { setLoadingState("checking"); - const freshAgent = await client.agents.retrieve(agent.id); const data = await getResumeData( client, - freshAgent, + agent, selectedConversationId, ); conversationIdToUse = selectedConversationId; @@ -1963,14 +1951,29 @@ async function main(): Promise { // Default (including --new-agent): use the agent's "default" conversation conversationIdToUse = "default"; - // Load message history from the default conversation + // Load message history and memfs sync in parallel — they're independent setLoadingState("checking"); - const freshAgent = await client.agents.retrieve(agent.id); - const data = await getResumeData(client, freshAgent, "default"); + const [data] = await Promise.all([ + getResumeData(client, agent, "default"), + memfsSyncPromise.catch((error) => { + console.error( + error instanceof Error ? error.message : String(error), + ); + process.exit(1); + }), + ]); setResumeData(data); setResumedExistingConversation(true); } + // Ensure memfs sync completed (already resolved for default path via Promise.all above) + try { + await memfsSyncPromise; + } catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } + // Save the session (agent + conversation) to settings // Skip for subagents - they shouldn't pollute the LRU settings if (!isSubagent) { @@ -2011,6 +2014,7 @@ async function main(): Promise { fromAfFile, loadingState, selectedGlobalAgentId, + validatedAgent, shouldContinue, resumeAgentId, selectedConversationId,