diff --git a/src/agent/modify.ts b/src/agent/modify.ts index 514f012..59bf9ea 100644 --- a/src/agent/modify.ts +++ b/src/agent/modify.ts @@ -249,16 +249,14 @@ export async function updateConversationLLMConfig( export interface RecompileAgentSystemPromptOptions { dryRun?: boolean; - updateTimestamp?: boolean; } -interface AgentSystemPromptRecompileClient { - agents: { +interface ConversationSystemPromptRecompileClient { + conversations: { recompile: ( - agentId: string, + conversationId: string, params: { dry_run?: boolean; - update_timestamp?: boolean; }, ) => Promise; }; @@ -268,22 +266,21 @@ interface AgentSystemPromptRecompileClient { * Recompile an agent's system prompt after memory writes so server-side prompt * state picks up the latest memory content. * - * @param agentId - The agent ID to recompile - * @param options - Optional dry-run/timestamp controls + * @param conversationId - The conversation whose prompt should be recompiled + * @param options - Optional dry-run control * @param clientOverride - Optional injected client for tests * @returns The compiled system prompt returned by the API */ export async function recompileAgentSystemPrompt( - agentId: string, + conversationId: string, options: RecompileAgentSystemPromptOptions = {}, - clientOverride?: AgentSystemPromptRecompileClient, + clientOverride?: ConversationSystemPromptRecompileClient, ): Promise { const client = (clientOverride ?? - (await getClient())) as AgentSystemPromptRecompileClient; + (await getClient())) as ConversationSystemPromptRecompileClient; - return client.agents.recompile(agentId, { + return client.conversations.recompile(conversationId, { dry_run: options.dryRun, - update_timestamp: options.updateTimestamp, }); } diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 725c8d7..7bf01cc 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -1701,10 +1701,12 @@ export default function App({ const initProgressByAgentRef = useRef( new Map(), ); - const systemPromptRecompileByAgentRef = useRef( + const systemPromptRecompileByConversationRef = useRef( new Map>(), ); - const queuedSystemPromptRecompileByAgentRef = useRef(new Set()); + const queuedSystemPromptRecompileByConversationRef = useRef( + new Set(), + ); const updateInitProgress = ( forAgentId: string, update: Partial<{ shallowCompleted: boolean; deepFired: boolean }>, @@ -9289,15 +9291,17 @@ export default function App({ const msg = await handleMemorySubagentCompletion( { agentId, + conversationId: conversationIdRef.current, subagentType: "init", initDepth: "deep", success, error, }, { - recompileByAgent: systemPromptRecompileByAgentRef.current, - recompileQueuedByAgent: - queuedSystemPromptRecompileByAgentRef.current, + recompileByConversation: + systemPromptRecompileByConversationRef.current, + recompileQueuedByConversation: + queuedSystemPromptRecompileByConversationRef.current, updateInitProgress, logRecompileFailure: (message) => debugWarn("memory", message), @@ -9476,15 +9480,17 @@ export default function App({ const msg = await handleMemorySubagentCompletion( { agentId, + conversationId: conversationIdRef.current, subagentType: "init", initDepth: "shallow", success, error, }, { - recompileByAgent: systemPromptRecompileByAgentRef.current, - recompileQueuedByAgent: - queuedSystemPromptRecompileByAgentRef.current, + recompileByConversation: + systemPromptRecompileByConversationRef.current, + recompileQueuedByConversation: + queuedSystemPromptRecompileByConversationRef.current, updateInitProgress, logRecompileFailure: (message) => debugWarn("memory", message), @@ -9605,14 +9611,16 @@ ${SYSTEM_REMINDER_CLOSE} const msg = await handleMemorySubagentCompletion( { agentId, + conversationId: conversationIdRef.current, subagentType: "reflection", success, error, }, { - recompileByAgent: systemPromptRecompileByAgentRef.current, - recompileQueuedByAgent: - queuedSystemPromptRecompileByAgentRef.current, + recompileByConversation: + systemPromptRecompileByConversationRef.current, + recompileQueuedByConversation: + queuedSystemPromptRecompileByConversationRef.current, updateInitProgress, logRecompileFailure: (message) => debugWarn("memory", message), @@ -9660,15 +9668,17 @@ ${SYSTEM_REMINDER_CLOSE} const msg = await handleMemorySubagentCompletion( { agentId, + conversationId: conversationIdRef.current, subagentType: "init", initDepth: "deep", success, error, }, { - recompileByAgent: systemPromptRecompileByAgentRef.current, - recompileQueuedByAgent: - queuedSystemPromptRecompileByAgentRef.current, + recompileByConversation: + systemPromptRecompileByConversationRef.current, + recompileQueuedByConversation: + queuedSystemPromptRecompileByConversationRef.current, updateInitProgress, logRecompileFailure: (message) => debugWarn("memory", message), diff --git a/src/cli/helpers/memorySubagentCompletion.ts b/src/cli/helpers/memorySubagentCompletion.ts index 0ef282d..0aa42ef 100644 --- a/src/cli/helpers/memorySubagentCompletion.ts +++ b/src/cli/helpers/memorySubagentCompletion.ts @@ -12,13 +12,14 @@ export interface MemoryInitProgressUpdate { } type RecompileAgentSystemPromptFn = ( - agentId: string, + conversationId: string, options?: RecompileAgentSystemPromptOptions, ) => Promise; export type MemorySubagentCompletionArgs = | { agentId: string; + conversationId: string; subagentType: "init"; initDepth: MemoryInitDepth; success: boolean; @@ -26,6 +27,7 @@ export type MemorySubagentCompletionArgs = } | { agentId: string; + conversationId: string; subagentType: "reflection"; initDepth?: never; success: boolean; @@ -33,8 +35,8 @@ export type MemorySubagentCompletionArgs = }; export interface MemorySubagentCompletionDeps { - recompileByAgent: Map>; - recompileQueuedByAgent: Set; + recompileByConversation: Map>; + recompileQueuedByConversation: Set; updateInitProgress: ( agentId: string, update: Partial, @@ -51,7 +53,8 @@ export async function handleMemorySubagentCompletion( args: MemorySubagentCompletionArgs, deps: MemorySubagentCompletionDeps, ): Promise { - const { agentId, subagentType, initDepth, success, error } = args; + const { agentId, conversationId, subagentType, initDepth, success, error } = + args; const recompileAgentSystemPromptFn = deps.recompileAgentSystemPromptImpl ?? recompileAgentSystemPrompt; let recompileError: string | null = null; @@ -67,25 +70,23 @@ export async function handleMemorySubagentCompletion( } try { - let inFlight = deps.recompileByAgent.get(agentId); + let inFlight = deps.recompileByConversation.get(conversationId); if (!inFlight) { inFlight = (async () => { do { - deps.recompileQueuedByAgent.delete(agentId); - await recompileAgentSystemPromptFn(agentId, { - updateTimestamp: true, - }); - } while (deps.recompileQueuedByAgent.has(agentId)); + deps.recompileQueuedByConversation.delete(conversationId); + await recompileAgentSystemPromptFn(conversationId, {}); + } while (deps.recompileQueuedByConversation.has(conversationId)); })().finally(() => { // Cleanup runs only after the shared promise settles, so every // concurrent caller awaits the same full recompile lifecycle. - deps.recompileQueuedByAgent.delete(agentId); - deps.recompileByAgent.delete(agentId); + deps.recompileQueuedByConversation.delete(conversationId); + deps.recompileByConversation.delete(conversationId); }); - deps.recompileByAgent.set(agentId, inFlight); + deps.recompileByConversation.set(conversationId, inFlight); } else { - deps.recompileQueuedByAgent.add(agentId); + deps.recompileQueuedByConversation.add(conversationId); } await inFlight; @@ -95,7 +96,7 @@ export async function handleMemorySubagentCompletion( ? recompileFailure.message : String(recompileFailure); deps.logRecompileFailure?.( - `Failed to recompile system prompt after ${subagentType} subagent for ${agentId}: ${recompileError}`, + `Failed to recompile system prompt after ${subagentType} subagent for ${agentId} in conversation ${conversationId}: ${recompileError}`, ); } } diff --git a/src/tests/agent/recompile-system-prompt.test.ts b/src/tests/agent/recompile-system-prompt.test.ts index c7945eb..ff624a1 100644 --- a/src/tests/agent/recompile-system-prompt.test.ts +++ b/src/tests/agent/recompile-system-prompt.test.ts @@ -4,11 +4,11 @@ import { recompileAgentSystemPrompt } from "../../agent/modify"; describe("recompileAgentSystemPrompt", () => { test("calls the Letta agent recompile endpoint with mapped params", async () => { const agentsRecompileMock = mock( - (_agentId: string, _params?: Record) => + (_conversationId: string, _params?: Record) => Promise.resolve("compiled-system-prompt"), ); const client = { - agents: { + conversations: { recompile: agentsRecompileMock, }, }; @@ -16,7 +16,6 @@ describe("recompileAgentSystemPrompt", () => { const compiledPrompt = await recompileAgentSystemPrompt( "agent-123", { - updateTimestamp: true, dryRun: true, }, client, @@ -25,7 +24,6 @@ describe("recompileAgentSystemPrompt", () => { expect(compiledPrompt).toBe("compiled-system-prompt"); expect(agentsRecompileMock).toHaveBeenCalledWith("agent-123", { dry_run: true, - update_timestamp: true, }); }); }); diff --git a/src/tests/cli/memory-subagent-recompile-wiring.test.ts b/src/tests/cli/memory-subagent-recompile-wiring.test.ts index 07dbbf7..09c3fc8 100644 --- a/src/tests/cli/memory-subagent-recompile-wiring.test.ts +++ b/src/tests/cli/memory-subagent-recompile-wiring.test.ts @@ -3,7 +3,7 @@ import type { RecompileAgentSystemPromptOptions } from "../../agent/modify"; import { handleMemorySubagentCompletion } from "../../cli/helpers/memorySubagentCompletion"; const recompileAgentSystemPromptMock = mock( - (_agentId: string, _opts?: RecompileAgentSystemPromptOptions) => + (_conversationId: string, _opts?: RecompileAgentSystemPromptOptions) => Promise.resolve("compiled-system-prompt"), ); @@ -33,13 +33,14 @@ describe("memory subagent recompile handling", () => { const message = await handleMemorySubagentCompletion( { agentId: "agent-init-1", + conversationId: "conv-init-1", subagentType: "init", initDepth: "shallow", success: true, }, { - recompileByAgent: new Map(), - recompileQueuedByAgent: new Set(), + recompileByConversation: new Map(), + recompileQueuedByConversation: new Set(), recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, updateInitProgress: (agentId, update) => { progressUpdates.push({ @@ -60,10 +61,8 @@ describe("memory subagent recompile handling", () => { }, ]); expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith( - "agent-init-1", - { - updateTimestamp: true, - }, + "conv-init-1", + {}, ); }); @@ -74,11 +73,11 @@ describe("memory subagent recompile handling", () => { .mockImplementationOnce(() => firstDeferred.promise) .mockImplementationOnce(() => secondDeferred.promise); - const recompileByAgent = new Map>(); - const recompileQueuedByAgent = new Set(); + const recompileByConversation = new Map>(); + const recompileQueuedByConversation = new Set(); const deps = { - recompileByAgent, - recompileQueuedByAgent, + recompileByConversation, + recompileQueuedByConversation, recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, updateInitProgress: () => {}, }; @@ -86,6 +85,7 @@ describe("memory subagent recompile handling", () => { const first = handleMemorySubagentCompletion( { agentId: "agent-shared", + conversationId: "conv-shared", subagentType: "reflection", success: true, }, @@ -94,6 +94,7 @@ describe("memory subagent recompile handling", () => { const second = handleMemorySubagentCompletion( { agentId: "agent-shared", + conversationId: "conv-shared", subagentType: "reflection", success: true, }, @@ -102,6 +103,7 @@ describe("memory subagent recompile handling", () => { const third = handleMemorySubagentCompletion( { agentId: "agent-shared", + conversationId: "conv-shared", subagentType: "reflection", success: true, }, @@ -109,14 +111,14 @@ describe("memory subagent recompile handling", () => { ); expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(1); - expect(recompileByAgent.has("agent-shared")).toBe(true); - expect(recompileQueuedByAgent.has("agent-shared")).toBe(true); + expect(recompileByConversation.has("conv-shared")).toBe(true); + expect(recompileQueuedByConversation.has("conv-shared")).toBe(true); firstDeferred.resolve("compiled-system-prompt"); await Promise.resolve(); expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(2); - expect(recompileByAgent.has("agent-shared")).toBe(true); + expect(recompileByConversation.has("conv-shared")).toBe(true); secondDeferred.resolve("compiled-system-prompt"); @@ -134,7 +136,47 @@ describe("memory subagent recompile handling", () => { expect(thirdMessage).toBe( "Reflected on /palace, the halls remember more now.", ); - expect(recompileByAgent.size).toBe(0); - expect(recompileQueuedByAgent.size).toBe(0); + expect(recompileByConversation.size).toBe(0); + expect(recompileQueuedByConversation.size).toBe(0); + }); + + test("does not coalesce recompiles across different conversations for same agent", async () => { + const deps = { + recompileByConversation: new Map>(), + recompileQueuedByConversation: new Set(), + recompileAgentSystemPromptImpl: recompileAgentSystemPromptMock, + updateInitProgress: () => {}, + }; + + const [firstMessage, secondMessage] = await Promise.all([ + handleMemorySubagentCompletion( + { + agentId: "agent-shared", + conversationId: "conv-a", + subagentType: "reflection", + success: true, + }, + deps, + ), + handleMemorySubagentCompletion( + { + agentId: "agent-shared", + conversationId: "conv-b", + subagentType: "reflection", + success: true, + }, + deps, + ), + ]); + + expect(firstMessage).toBe( + "Reflected on /palace, the halls remember more now.", + ); + expect(secondMessage).toBe( + "Reflected on /palace, the halls remember more now.", + ); + expect(recompileAgentSystemPromptMock).toHaveBeenCalledTimes(2); + expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith("conv-a", {}); + expect(recompileAgentSystemPromptMock).toHaveBeenCalledWith("conv-b", {}); }); }); diff --git a/src/tests/cli/task-notification-flush.test.ts b/src/tests/cli/task-notification-flush.test.ts index 4208756..f529b1b 100644 --- a/src/tests/cli/task-notification-flush.test.ts +++ b/src/tests/cli/task-notification-flush.test.ts @@ -143,7 +143,7 @@ describe("background onComplete → flush wiring in App.tsx", () => { const onCompleteIdx = source.indexOf("onComplete:", initBlock); expect(onCompleteIdx).toBeGreaterThan(-1); - const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 900); + const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 1600); expect(onCompleteWindow).toContain("await handleMemorySubagentCompletion("); expect(onCompleteWindow).toContain("appendTaskNotificationEvents("); }); @@ -157,7 +157,7 @@ describe("background onComplete → flush wiring in App.tsx", () => { const onCompleteIdx = source.indexOf("onComplete:", reflectionBlock); expect(onCompleteIdx).toBeGreaterThan(-1); - const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 700); + const onCompleteWindow = source.slice(onCompleteIdx, onCompleteIdx + 1400); expect(onCompleteWindow).toContain("await handleMemorySubagentCompletion("); expect(onCompleteWindow).toContain("appendTaskNotificationEvents("); });