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)) {
|
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) {
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
90
src/index.ts
90
src/index.ts
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user