perf: eliminate redundant API calls on startup (#1067)

Co-authored-by: cpacker <packercharles@gmail.com>
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Kian Jones
2026-02-20 16:59:45 -08:00
committed by GitHub
parent 539ce175e5
commit c5033666b4
4 changed files with 76 additions and 63 deletions

View File

@@ -222,6 +222,26 @@ export async function applyMemfsFlags(
if (isEnabled && (memfsFlag || shouldAutoEnableFromTag)) { if (isEnabled && (memfsFlag || shouldAutoEnableFromTag)) {
const { detachMemoryTools } = await import("../tools/toolset"); const { detachMemoryTools } = await import("../tools/toolset");
await detachMemoryTools(agentId); 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. // Keep server-side state aligned with explicit disable.
@@ -235,7 +255,10 @@ export async function applyMemfsFlags(
if (isEnabled) { if (isEnabled) {
const { addGitMemoryTag, isGitRepo, cloneMemoryRepo, pullMemory } = const { addGitMemoryTag, isGitRepo, cloneMemoryRepo, pullMemory } =
await import("./memoryGit"); await import("./memoryGit");
await addGitMemoryTag(agentId); await addGitMemoryTag(
agentId,
options?.agentTags ? { tags: options.agentTags } : undefined,
);
if (!isGitRepo(agentId)) { if (!isGitRepo(agentId)) {
await cloneMemoryRepo(agentId); await cloneMemoryRepo(agentId);
} else if (options?.pullOnExistingRepo) { } else if (options?.pullOnExistingRepo) {

View File

@@ -453,10 +453,13 @@ export async function getMemoryGitStatus(
* Add the git-memory-enabled tag to an agent. * Add the git-memory-enabled tag to an agent.
* This triggers the backend to create the git repo. * This triggers the backend to create the git repo.
*/ */
export async function addGitMemoryTag(agentId: string): Promise<void> { export async function addGitMemoryTag(
agentId: string,
prefetchedAgent?: { tags?: string[] | null },
): Promise<void> {
const client = await getClient(); const client = await getClient();
try { try {
const agent = await client.agents.retrieve(agentId); const agent = prefetchedAgent ?? (await client.agents.retrieve(agentId));
const tags = agent.tags || []; const tags = agent.tags || [];
if (!tags.includes(GIT_MEMORY_ENABLED_TAG)) { if (!tags.includes(GIT_MEMORY_ENABLED_TAG)) {
await client.agents.update(agentId, { await client.agents.update(agentId, {

View File

@@ -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) // Set agent context for tools that need it (e.g., Skill tool, Task tool)
setAgentContext(agent.id, skillsDirectory, resolvedSkillSources); setAgentContext(agent.id, skillsDirectory, resolvedSkillSources);

View File

@@ -1074,6 +1074,10 @@ async function main(): Promise<void> {
const [selectedGlobalAgentId, setSelectedGlobalAgentId] = useState< const [selectedGlobalAgentId, setSelectedGlobalAgentId] = useState<
string | null string | null
>(null); >(null);
// Cache agent object from Phase 1 validation to avoid redundant re-fetch in Phase 2
const [validatedAgent, setValidatedAgent] = useState<AgentState | null>(
null,
);
// Track agent and conversation for conversation selector (--resume flag) // Track agent and conversation for conversation selector (--resume flag)
const [resumeAgentId, setResumeAgentId] = useState<string | null>(null); const [resumeAgentId, setResumeAgentId] = useState<string | null>(null);
const [resumeAgentName, setResumeAgentName] = useState<string | null>(null); const [resumeAgentName, setResumeAgentName] = useState<string | null>(null);
@@ -1384,11 +1388,13 @@ async function main(): Promise<void> {
} }
// Step 1: Check local project LRU (session helpers centralize legacy fallback) // 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()); const localAgentId = settingsManager.getLocalLastAgentId(process.cwd());
let localAgentExists = false; let localAgentExists = false;
let cachedAgent: AgentState | null = null;
if (localAgentId) { if (localAgentId) {
try { try {
await client.agents.retrieve(localAgentId); cachedAgent = await client.agents.retrieve(localAgentId);
localAgentExists = true; localAgentExists = true;
} catch { } catch {
setFailedAgentMessage( setFailedAgentMessage(
@@ -1402,7 +1408,7 @@ async function main(): Promise<void> {
let globalAgentExists = false; let globalAgentExists = false;
if (globalAgentId && globalAgentId !== localAgentId) { if (globalAgentId && globalAgentId !== localAgentId) {
try { try {
await client.agents.retrieve(globalAgentId); cachedAgent = await client.agents.retrieve(globalAgentId);
globalAgentExists = true; globalAgentExists = true;
} catch { } catch {
// Global agent doesn't exist either // Global agent doesn't exist either
@@ -1432,6 +1438,9 @@ async function main(): Promise<void> {
switch (target.action) { switch (target.action) {
case "resume": case "resume":
setSelectedGlobalAgentId(target.agentId); setSelectedGlobalAgentId(target.agentId);
if (cachedAgent && cachedAgent.id === target.agentId) {
setValidatedAgent(cachedAgent);
}
// Don't set selectedConversationId — DEFAULT PATH uses default conv. // Don't set selectedConversationId — DEFAULT PATH uses default conv.
// Conversation restoration is handled by --continue path instead. // Conversation restoration is handled by --continue path instead.
setLoadingState("assembling"); setLoadingState("assembling");
@@ -1695,10 +1704,13 @@ async function main(): Promise<void> {
// Priority 4: Try to resume from project settings LRU (.letta/settings.local.json) // 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 // 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) { if (!agent && resumingAgentId) {
try { try {
agent = await client.agents.retrieve(resumingAgentId); agent =
validatedAgent && validatedAgent.id === resumingAgentId
? validatedAgent
: await client.agents.retrieve(resumingAgentId);
} catch (error) { } catch (error) {
// Agent disappeared between validation and now - show selector // Agent disappeared between validation and now - show selector
console.error( console.error(
@@ -1746,38 +1758,19 @@ async function main(): Promise<void> {
settingsManager.updateLocalProjectSettings({ lastAgent: agent.id }); settingsManager.updateLocalProjectSettings({ lastAgent: agent.id });
settingsManager.updateSettings({ 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) // Set agent context for tools that need it (e.g., Skill tool)
setAgentContext(agent.id, skillsDirectory, resolvedSkillSources); 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"; const isSubagent = process.env.LETTA_CODE_AGENT_ROLE === "subagent";
try { const agentId = agent.id;
const { applyMemfsFlags } = await import("./agent/memoryFilesystem"); const agentTags = agent.tags ?? undefined;
await applyMemfsFlags(agent.id, memfsFlag, noMemfsFlag, { const memfsSyncPromise = import("./agent/memoryFilesystem").then(
agentTags: agent.tags, ({ applyMemfsFlags }) =>
}); applyMemfsFlags(agentId, memfsFlag, noMemfsFlag, {
} catch (error) { agentTags,
console.error(error instanceof Error ? error.message : String(error)); }),
process.exit(1); );
}
// Check if we're resuming an existing agent // Check if we're resuming an existing agent
// We're resuming if: // We're resuming if:
@@ -1851,12 +1844,10 @@ async function main(): Promise<void> {
setResumedExistingConversation(true); setResumedExistingConversation(true);
try { try {
// Load message history and pending approvals from the conversation // Load message history and pending approvals from the conversation
// Re-fetch agent to get fresh message_ids for accurate pending approval detection
setLoadingState("checking"); setLoadingState("checking");
const freshAgent = await client.agents.retrieve(agent.id);
const data = await getResumeData( const data = await getResumeData(
client, client,
freshAgent, agent,
specifiedConversationId, specifiedConversationId,
); );
setResumeData(data); setResumeData(data);
@@ -1890,12 +1881,10 @@ async function main(): Promise<void> {
// If it no longer exists, fall back to creating new // If it no longer exists, fall back to creating new
try { try {
// Load message history and pending approvals from the conversation // Load message history and pending approvals from the conversation
// Re-fetch agent to get fresh message_ids for accurate pending approval detection
setLoadingState("checking"); setLoadingState("checking");
const freshAgent = await client.agents.retrieve(agent.id);
const data = await getResumeData( const data = await getResumeData(
client, client,
freshAgent, agent,
lastSession.conversationId, lastSession.conversationId,
); );
// Only set state after validation succeeds // Only set state after validation succeeds
@@ -1933,10 +1922,9 @@ async function main(): Promise<void> {
// User selected a specific conversation from the --resume selector // User selected a specific conversation from the --resume selector
try { try {
setLoadingState("checking"); setLoadingState("checking");
const freshAgent = await client.agents.retrieve(agent.id);
const data = await getResumeData( const data = await getResumeData(
client, client,
freshAgent, agent,
selectedConversationId, selectedConversationId,
); );
conversationIdToUse = selectedConversationId; conversationIdToUse = selectedConversationId;
@@ -1963,14 +1951,29 @@ async function main(): Promise<void> {
// Default (including --new-agent): use the agent's "default" conversation // Default (including --new-agent): use the agent's "default" conversation
conversationIdToUse = "default"; conversationIdToUse = "default";
// Load message history from the default conversation // Load message history and memfs sync in parallel — they're independent
setLoadingState("checking"); setLoadingState("checking");
const freshAgent = await client.agents.retrieve(agent.id); const [data] = await Promise.all([
const data = await getResumeData(client, freshAgent, "default"); getResumeData(client, agent, "default"),
memfsSyncPromise.catch((error) => {
console.error(
error instanceof Error ? error.message : String(error),
);
process.exit(1);
}),
]);
setResumeData(data); setResumeData(data);
setResumedExistingConversation(true); 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 // Save the session (agent + conversation) to settings
// Skip for subagents - they shouldn't pollute the LRU settings // Skip for subagents - they shouldn't pollute the LRU settings
if (!isSubagent) { if (!isSubagent) {
@@ -2011,6 +2014,7 @@ async function main(): Promise<void> {
fromAfFile, fromAfFile,
loadingState, loadingState,
selectedGlobalAgentId, selectedGlobalAgentId,
validatedAgent,
shouldContinue, shouldContinue,
resumeAgentId, resumeAgentId,
selectedConversationId, selectedConversationId,