From 493a42ff754d599c8a736b4b9569a09a3bce97ac Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Jul 2026 23:13:18 +0200 Subject: [PATCH] fix(subagents): retain history across hydration --- .../src/server/agent/agent-manager.test.ts | 24 +++++++++++++++++++ .../server/src/server/agent/agent-manager.ts | 18 ++++++++++---- .../server/agent/provider-subagents/store.ts | 4 ++-- .../providers/codex/tool-call-mapper.test.ts | 6 ++--- .../agent/providers/codex/tool-call-mapper.ts | 3 ++- 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index c379e3dae7..e27d476815 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -2885,6 +2885,11 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro workspaceId: undefined, }, ); + const managedWithRunningProviderDescendant = await manager.createAgent( + { provider: "codex", cwd: workdir }, + undefined, + { labels: { [PARENT_AGENT_ID_LABEL]: parent.id }, workspaceId: undefined }, + ); client.sessions[3]?.pushEvent({ type: "turn_started", provider: "codex", @@ -2893,6 +2898,19 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro await vi.waitFor(() => { expect(manager.getAgent(runningGrandchild.id)?.lifecycle).toBe("running"); }); + client.sessions[4]?.pushEvent({ + type: "provider_subagent", + provider: "codex", + event: { type: "upsert", id: "running-provider-grandchild", status: "running" }, + }); + await vi.waitFor(() => { + expect( + manager.getProviderSubagent( + managedWithRunningProviderDescendant.id, + "running-provider-grandchild", + )?.status, + ).toBe("running"); + }); await manager.hydrateTimelineFromProvider(parent.id); const result = await manager.archiveFinishedSubagents(parent.id); @@ -2903,6 +2921,7 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro }); expectArchivedAgentRecord(await storage.get(managedFinished.id), "closed"); expect((await storage.get(managedWithRunningDescendant.id))?.archivedAt).toBeFalsy(); + expect((await storage.get(managedWithRunningProviderDescendant.id))?.archivedAt).toBeFalsy(); expect(manager.getAgent(runningGrandchild.id)?.lifecycle).toBe("running"); expect(manager.listProviderSubagents(parent.id).map((subagent) => subagent.id)).toEqual([ "native-running", @@ -2927,6 +2946,11 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro expect(manager.listProviderSubagents(parent.id).map((subagent) => subagent.id)).toEqual([ "native-running", ]); + expect(manager.getProviderSubagent(parent.id, "native-finished")).toMatchObject({ + id: "native-finished", + status: "completed", + }); + expect(() => manager.fetchProviderSubagentTimeline(parent.id, "native-finished")).not.toThrow(); }); test("force provider hydration removes children absent from current history", async () => { diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 58f9852870..bc6d77202f 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -985,6 +985,9 @@ export class AgentManager { const hasBusyDescendant = (agentId: string, visited = new Set()): boolean => { if (visited.has(agentId)) return true; visited.add(agentId); + if (this.providerSubagents.list(agentId).some((subagent) => subagent.status === "running")) { + return true; + } for (const child of recordsByParentId.get(agentId) ?? []) { const liveChild = this.agents.get(child.id); if ( @@ -2980,8 +2983,9 @@ export class AgentManager { private applyProviderSubagentEvent( agent: ManagedAgent, event: Extract, + options?: { includeDismissed?: boolean }, ): ProviderSubagentStoreEvent | null { - if (agent.dismissedProviderSubagentIds.has(event.event.id)) { + if (!options?.includeDismissed && agent.dismissedProviderSubagentIds.has(event.event.id)) { return null; } return this.providerSubagents.apply(agent.id, event.provider, event.event); @@ -3120,12 +3124,18 @@ export class AgentManager { for (const event of this.providerSubagents.deleteParent(agent.id)) { if (broadcast) { - this.dispatch({ type: "provider_subagent", event }); + this.dispatch({ + type: "provider_subagent", + event: agent.dismissedProviderSubagentIds.has(event.subagentId) + ? { ...event, retainTimeline: true } + : event, + }); } } for (const event of providerSubagentEvents) { - const update = this.applyProviderSubagentEvent(agent, event); - if (broadcast && update) { + const dismissed = agent.dismissedProviderSubagentIds.has(event.event.id); + const update = this.applyProviderSubagentEvent(agent, event, { includeDismissed: true }); + if (broadcast && update && !dismissed) { this.dispatch({ type: "provider_subagent", event: update }); } } diff --git a/packages/server/src/server/agent/provider-subagents/store.ts b/packages/server/src/server/agent/provider-subagents/store.ts index 7e99032d87..076be537d0 100644 --- a/packages/server/src/server/agent/provider-subagents/store.ts +++ b/packages/server/src/server/agent/provider-subagents/store.ts @@ -160,8 +160,8 @@ export class ProviderSubagentStore { }; } - deleteParent(parentAgentId: string): ProviderSubagentStoreEvent[] { - const events: ProviderSubagentStoreEvent[] = []; + deleteParent(parentAgentId: string): Extract[] { + const events: Extract[] = []; for (const subagent of this.list(parentAgentId)) { const key = storeKey(parentAgentId, subagent.id); this.descriptors.delete(key); diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts index 90c58ce8c5..8a3b142bad 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.test.ts @@ -246,7 +246,7 @@ describe("codex tool-call mapper", () => { detail: { type: "sub_agent", subAgentType: "research/investigator", - description: "/root/research/investigator", + description: "research/investigator", log: "", actions: [], }, @@ -280,7 +280,7 @@ describe("codex tool-call mapper", () => { detail: { type: "sub_agent", subAgentType: "investigator", - description: "/tmp/native/investigator", + description: "investigator", }, }); }); @@ -298,7 +298,7 @@ describe("codex tool-call mapper", () => { detail: { type: "sub_agent", subAgentType: "investigator", - description: "C:\\Users\\dev\\agents\\investigator", + description: "investigator", }, }); }); diff --git a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts index ae26b9e0c9..449cc3e014 100644 --- a/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts +++ b/packages/server/src/server/agent/providers/codex/tool-call-mapper.ts @@ -1003,6 +1003,7 @@ function mapSubAgentActivityItem( Math.max(nativeName.lastIndexOf("/"), nativeName.lastIndexOf("\\")) + 1, ); } + const description = nativeName; nativeName ||= "Sub-agent"; return { type: "tool_call", @@ -1013,7 +1014,7 @@ function mapSubAgentActivityItem( detail: { type: "sub_agent", subAgentType: nativeName, - description: item.agentPath, + description, log: "", actions: [], },