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:
@@ -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) {
|
||||
|
||||
@@ -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<void> {
|
||||
export async function addGitMemoryTag(
|
||||
agentId: string,
|
||||
prefetchedAgent?: { tags?: string[] | null },
|
||||
): Promise<void> {
|
||||
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, {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
90
src/index.ts
90
src/index.ts
@@ -1074,6 +1074,10 @@ async function main(): Promise<void> {
|
||||
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<AgentState | null>(
|
||||
null,
|
||||
);
|
||||
// Track agent and conversation for conversation selector (--resume flag)
|
||||
const [resumeAgentId, setResumeAgentId] = 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)
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
|
||||
// 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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
fromAfFile,
|
||||
loadingState,
|
||||
selectedGlobalAgentId,
|
||||
validatedAgent,
|
||||
shouldContinue,
|
||||
resumeAgentId,
|
||||
selectedConversationId,
|
||||
|
||||
Reference in New Issue
Block a user