From 6e50c229561ea9d8b9bab367f7e54c4a977e5910 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Jul 2026 23:43:35 +0200 Subject: [PATCH] fix(subagents): separate hidden rows from retained history --- .../app/src/subagents/provider-store.test.ts | 9 ++++- packages/app/src/subagents/provider-store.ts | 27 +++++++++---- packages/app/src/subagents/select.test.ts | 39 ++++++++++++++++++- packages/app/src/subagents/select.ts | 2 +- .../src/server/agent/agent-manager.test.ts | 29 ++++++++++++++ .../server/src/server/agent/agent-manager.ts | 5 ++- 6 files changed, 98 insertions(+), 13 deletions(-) diff --git a/packages/app/src/subagents/provider-store.test.ts b/packages/app/src/subagents/provider-store.test.ts index c607cd17af..dd745d19fe 100644 --- a/packages/app/src/subagents/provider-store.test.ts +++ b/packages/app/src/subagents/provider-store.test.ts @@ -6,7 +6,11 @@ const PARENT_ID = "parent-1"; const SUBAGENT_ID = "child-1"; afterEach(() => { - useProviderSubagentStore.setState({ descriptors: new Map(), timelines: new Map() }); + useProviderSubagentStore.setState({ + descriptors: new Map(), + timelines: new Map(), + hiddenFromTrack: new Set(), + }); }); describe("provider subagent client store", () => { @@ -165,7 +169,8 @@ describe("provider subagent client store", () => { const state = useProviderSubagentStore.getState(); const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID); - expect(state.descriptors.has(key)).toBe(false); + expect(state.descriptors.get(key)?.title).toBe("Finished child"); + expect(state.hiddenFromTrack.has(key)).toBe(true); expect(state.timelines.get(key)?.tail).toEqual([ expect.objectContaining({ kind: "assistant_message", text: "Finished output." }), ]); diff --git a/packages/app/src/subagents/provider-store.ts b/packages/app/src/subagents/provider-store.ts index c4fa337a51..3dc3f5c5cb 100644 --- a/packages/app/src/subagents/provider-store.ts +++ b/packages/app/src/subagents/provider-store.ts @@ -32,6 +32,7 @@ export interface ProviderSubagentTimelineState { interface ProviderSubagentState { descriptors: Map; timelines: Map; + hiddenFromTrack: Set; replaceList( serverId: string, parentAgentId: string, @@ -193,14 +194,20 @@ function buildTimelineResponseRows( export const useProviderSubagentStore = create((set) => ({ descriptors: new Map(), timelines: new Map(), + hiddenFromTrack: new Set(), replaceList(serverId, parentAgentId, subagents) { set((state) => { const prefix = parentPrefix(serverId, parentAgentId); const descriptors = new Map( - [...state.descriptors].filter(([key]) => !key.startsWith(prefix)), + [...state.descriptors].filter( + ([key]) => !key.startsWith(prefix) || state.hiddenFromTrack.has(key), + ), ); + const hiddenFromTrack = new Set(state.hiddenFromTrack); for (const subagent of subagents) { - descriptors.set(providerSubagentKey(serverId, parentAgentId, subagent.id), subagent); + const key = providerSubagentKey(serverId, parentAgentId, subagent.id); + descriptors.set(key, subagent); + hiddenFromTrack.delete(key); } const retainedKeys = new Set(descriptors.keys()); const timelines = new Map( @@ -217,7 +224,7 @@ export const useProviderSubagentStore = create((set) => ( ); } } - return { descriptors, timelines }; + return { descriptors, timelines, hiddenFromTrack }; }); }, applyUpdate(serverId, payload) { @@ -229,8 +236,10 @@ export const useProviderSubagentStore = create((set) => ( payload.subagent.id, ); const descriptors = new Map(state.descriptors); + const hiddenFromTrack = new Set(state.hiddenFromTrack); const previous = descriptors.get(key); descriptors.set(key, payload.subagent); + hiddenFromTrack.delete(key); let timelines = state.timelines; const current = state.timelines.get(key); if (current && previous?.status !== payload.subagent.status) { @@ -240,18 +249,22 @@ export const useProviderSubagentStore = create((set) => ( buildTimelineState(current.rows, current.epoch, payload.subagent, current.hasOlder), ); } - return { descriptors, timelines }; + return { descriptors, timelines, hiddenFromTrack }; } if (payload.kind === "remove") { const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId); const descriptors = new Map(state.descriptors); - descriptors.delete(key); if (payload.retainTimeline) { - return { descriptors, timelines: state.timelines }; + const hiddenFromTrack = new Set(state.hiddenFromTrack); + hiddenFromTrack.add(key); + return { descriptors, timelines: state.timelines, hiddenFromTrack }; } + descriptors.delete(key); const timelines = new Map(state.timelines); + const hiddenFromTrack = new Set(state.hiddenFromTrack); + hiddenFromTrack.delete(key); timelines.delete(key); - return { descriptors, timelines }; + return { descriptors, timelines, hiddenFromTrack }; } const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId); const existing = state.timelines.get(key); diff --git a/packages/app/src/subagents/select.test.ts b/packages/app/src/subagents/select.test.ts index 4a8599467a..fd965698f1 100644 --- a/packages/app/src/subagents/select.test.ts +++ b/packages/app/src/subagents/select.test.ts @@ -59,7 +59,11 @@ function setAgents(agents: Agent[]): void { afterEach(() => { useSessionStore.getState().clearSession(SERVER_ID); - useProviderSubagentStore.setState({ descriptors: new Map(), timelines: new Map() }); + useProviderSubagentStore.setState({ + descriptors: new Map(), + timelines: new Map(), + hiddenFromTrack: new Set(), + }); }); describe("selectSubagentsForParent", () => { @@ -90,6 +94,39 @@ describe("selectSubagentsForParent", () => { ).toEqual(["provider-child"]); }); + it("hides dismissed provider children from the track while retaining their descriptor", () => { + const store = useProviderSubagentStore.getState(); + store.applyUpdate(SERVER_ID, { + kind: "upsert", + subagent: { + id: "provider-child", + parentAgentId: "parent-a", + provider: "codex", + title: "Provider child", + description: null, + status: "completed", + createdAt: "2026-03-08T10:01:00.000Z", + updatedAt: "2026-03-08T10:02:00.000Z", + toolCallId: "call-1", + }, + }); + store.applyUpdate(SERVER_ID, { + kind: "remove", + parentAgentId: "parent-a", + subagentId: "provider-child", + retainTimeline: true, + }); + + expect( + selectProviderSubagentsForParent( + useProviderSubagentStore.getState(), + { serverId: SERVER_ID, parentAgentId: "parent-a" }, + true, + ), + ).toEqual([]); + expect(useProviderSubagentStore.getState().descriptors.size).toBe(1); + }); + it("returns only non-archived children for the requested parent", () => { setAgents([ makeAgent({ id: "parent-a" }), diff --git a/packages/app/src/subagents/select.ts b/packages/app/src/subagents/select.ts index a235cc445a..d96c1628af 100644 --- a/packages/app/src/subagents/select.ts +++ b/packages/app/src/subagents/select.ts @@ -91,7 +91,7 @@ export function selectProviderSubagentsForParent( const rows: ProviderSubagentRow[] = []; const prefix = `${params.serverId}\0${params.parentAgentId}\0`; for (const [key, subagent] of state.descriptors) { - if (!key.startsWith(prefix)) continue; + if (!key.startsWith(prefix) || state.hiddenFromTrack.has(key)) continue; rows.push({ kind: "provider", id: subagent.id, diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index e27d476815..0b802fe6c0 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -2857,6 +2857,15 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro this.sessions.push(session); return session; } + + override async resumeSession( + _handle: AgentPersistenceHandle, + config?: Partial, + ): Promise { + const session = new ParentSession({ provider: "codex", cwd: config?.cwd ?? workdir }); + this.sessions.push(session); + return session; + } } const client = new SubagentsClient(); const manager = new AgentManager({ @@ -2951,6 +2960,26 @@ test("archiveFinishedSubagents archives managed children and keeps dismissed pro status: "completed", }); expect(() => manager.fetchProviderSubagentTimeline(parent.id, "native-finished")).not.toThrow(); + + const reloadedManager = new AgentManager({ + clients: { codex: new SubagentsClient() }, + registry: storage, + logger, + }); + await reloadedManager.resumeAgentFromPersistence( + { provider: "codex", sessionId: "persisted-parent" }, + { cwd: workdir }, + parent.id, + { dismissedProviderSubagentIds: ["native-finished", "native-failed"] }, + ); + await reloadedManager.hydrateTimelineFromProvider(parent.id); + expect(reloadedManager.listProviderSubagents(parent.id).map((subagent) => subagent.id)).toEqual([ + "native-running", + ]); + expect(reloadedManager.getProviderSubagent(parent.id, "native-finished")).toMatchObject({ + id: "native-finished", + status: "completed", + }); }); 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 bc6d77202f..3d076e1ddb 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -3165,8 +3165,9 @@ export class AgentManager { try { for await (const event of agent.session.streamHistory()) { if (event.type === "provider_subagent") { - 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 }); } continue;