diff --git a/packages/app/src/subagents/provider-store.test.ts b/packages/app/src/subagents/provider-store.test.ts index 03c6ade169..c607cd17af 100644 --- a/packages/app/src/subagents/provider-store.test.ts +++ b/packages/app/src/subagents/provider-store.test.ts @@ -129,6 +129,48 @@ describe("provider subagent client store", () => { ).toBe(false); }); + test("keeps an open timeline when a child is removed only from the subagent track", () => { + const store = useProviderSubagentStore.getState(); + store.applyUpdate(SERVER_ID, { + kind: "upsert", + subagent: { + id: SUBAGENT_ID, + parentAgentId: PARENT_ID, + provider: "codex", + title: "Finished child", + description: null, + status: "completed", + createdAt: "2026-07-12T10:00:00.000Z", + updatedAt: "2026-07-12T10:00:02.000Z", + toolCallId: "call-1", + }, + }); + store.applyUpdate(SERVER_ID, { + kind: "timeline", + parentAgentId: PARENT_ID, + subagentId: SUBAGENT_ID, + provider: "codex", + epoch: "epoch-1", + seq: 1, + timestamp: "2026-07-12T10:00:01.000Z", + item: { type: "assistant_message", text: "Finished output." }, + }); + + store.applyUpdate(SERVER_ID, { + kind: "remove", + parentAgentId: PARENT_ID, + subagentId: SUBAGENT_ID, + retainTimeline: true, + }); + + const state = useProviderSubagentStore.getState(); + const key = providerSubagentKey(SERVER_ID, PARENT_ID, SUBAGENT_ID); + expect(state.descriptors.has(key)).toBe(false); + expect(state.timelines.get(key)?.tail).toEqual([ + expect.objectContaining({ kind: "assistant_message", text: "Finished output." }), + ]); + }); + test("applies terminal list status to a timeline received before its descriptor", () => { const store = useProviderSubagentStore.getState(); store.applyUpdate(SERVER_ID, { diff --git a/packages/app/src/subagents/provider-store.ts b/packages/app/src/subagents/provider-store.ts index f0d76ee704..c4fa337a51 100644 --- a/packages/app/src/subagents/provider-store.ts +++ b/packages/app/src/subagents/provider-store.ts @@ -245,8 +245,11 @@ export const useProviderSubagentStore = create((set) => ( if (payload.kind === "remove") { const key = providerSubagentKey(serverId, payload.parentAgentId, payload.subagentId); const descriptors = new Map(state.descriptors); - const timelines = new Map(state.timelines); descriptors.delete(key); + if (payload.retainTimeline) { + return { descriptors, timelines: state.timelines }; + } + const timelines = new Map(state.timelines); timelines.delete(key); return { descriptors, timelines }; } diff --git a/packages/protocol/src/messages.provider-subagents.test.ts b/packages/protocol/src/messages.provider-subagents.test.ts index babd4c0e84..8a18f338e7 100644 --- a/packages/protocol/src/messages.provider-subagents.test.ts +++ b/packages/protocol/src/messages.provider-subagents.test.ts @@ -101,4 +101,18 @@ describe("provider subagent protocol", () => { }, }); }); + + test("accepts a remove update that preserves an open provider timeline", () => { + expect( + SessionOutboundMessageSchema.parse({ + type: "agent.provider_subagents.update", + payload: { + kind: "remove", + parentAgentId: "parent-1", + subagentId: "child-1", + retainTimeline: true, + }, + }), + ).toMatchObject({ payload: { kind: "remove", retainTimeline: true } }); + }); }); diff --git a/packages/protocol/src/messages.ts b/packages/protocol/src/messages.ts index 11e7026f69..00b77641bb 100644 --- a/packages/protocol/src/messages.ts +++ b/packages/protocol/src/messages.ts @@ -3137,6 +3137,7 @@ export const ProviderSubagentUpdateMessageSchema = z.object({ kind: z.literal("remove"), parentAgentId: z.string(), subagentId: z.string(), + retainTimeline: z.boolean().optional(), }), ]), }); diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index ba3297582e..c379e3dae7 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -2907,11 +2907,21 @@ 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(); expect((await storage.get(parent.id))?.dismissedProviderSubagentIds).toEqual([ "native-finished", "native-failed", ]); + await expect(manager.archiveFinishedSubagents(parent.id)).resolves.toEqual({ + archivedAgentIds: [], + dismissedProviderSubagentIds: [], + }); + await manager.hydrateTimelineFromProvider(parent.id, { force: true }); expect(manager.listProviderSubagents(parent.id).map((subagent) => subagent.id)).toEqual([ diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 28ba0ad1f0..58f9852870 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -960,8 +960,10 @@ export class AgentManager { } listProviderSubagents(parentAgentId: string): ProviderSubagentDescriptor[] { - this.requirePublicAgent(parentAgentId); - return this.providerSubagents.list(parentAgentId); + const parent = this.requirePublicAgent(parentAgentId); + return this.providerSubagents + .list(parentAgentId) + .filter((subagent) => !parent.dismissedProviderSubagentIds.has(subagent.id)); } async archiveFinishedSubagents(parentAgentId: string): Promise<{ @@ -1013,14 +1015,19 @@ export class AgentManager { const dismissedProviderSubagentIds = this.providerSubagents .list(parentAgentId) - .filter((subagent) => subagent.status !== "running") + .filter( + (subagent) => + subagent.status !== "running" && !parent.dismissedProviderSubagentIds.has(subagent.id), + ) .map((subagent) => subagent.id); for (const subagentId of dismissedProviderSubagentIds) { parent.dismissedProviderSubagentIds.add(subagentId); - const event = this.providerSubagents.apply(parentAgentId, parent.provider, { + const event: ProviderSubagentStoreEvent = { type: "remove", - id: subagentId, - }); + parentAgentId, + subagentId, + retainTimeline: true, + }; this.dispatch({ type: "provider_subagent", event }); } if (dismissedProviderSubagentIds.length > 0) { diff --git a/packages/server/src/server/agent/provider-subagents/store.ts b/packages/server/src/server/agent/provider-subagents/store.ts index 0d05e64269..7e99032d87 100644 --- a/packages/server/src/server/agent/provider-subagents/store.ts +++ b/packages/server/src/server/agent/provider-subagents/store.ts @@ -52,7 +52,12 @@ export type ProviderSubagentStoreEvent = row: AgentTimelineRow; epoch: string; } - | { type: "remove"; parentAgentId: string; subagentId: string }; + | { + type: "remove"; + parentAgentId: string; + subagentId: string; + retainTimeline?: boolean; + }; function storeKey(parentAgentId: string, subagentId: string): string { return `${parentAgentId}\0${subagentId}`; 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 4959ddf49e..90c58ce8c5 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 @@ -285,6 +285,24 @@ describe("codex tool-call mapper", () => { }); }); + it("uses only the final segment of a Windows subAgentActivity path", () => { + const item = mapCodexToolCallFromThreadItem({ + type: "subAgentActivity", + id: "activity-windows-path", + kind: "started", + agentThreadId: "child-thread-windows-path", + agentPath: "C:\\Users\\dev\\agents\\investigator", + }); + + expect(item).toMatchObject({ + detail: { + type: "sub_agent", + subAgentType: "investigator", + description: "C:\\Users\\dev\\agents\\investigator", + }, + }); + }); + it("does not fail a collabAgentToolCall from child error state alone", () => { const item = mapCodexToolCallFromThreadItem({ type: "collabAgentToolCall", 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 f6fd593c30..ae26b9e0c9 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 @@ -998,8 +998,10 @@ function mapSubAgentActivityItem( nativeName = ""; } else if (nativeName.startsWith("/root/")) { nativeName = nativeName.slice("/root/".length); - } else if (nativeName.includes("/")) { - nativeName = nativeName.slice(nativeName.lastIndexOf("/") + 1); + } else if (/[\\/]/.test(nativeName)) { + nativeName = nativeName.slice( + Math.max(nativeName.lastIndexOf("/"), nativeName.lastIndexOf("\\")) + 1, + ); } nativeName ||= "Sub-agent"; return { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index bd2ffbaa5a..b7d48f9156 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -1192,6 +1192,7 @@ export class Session { kind: "remove", parentAgentId: update.parentAgentId, subagentId: update.subagentId, + ...(update.retainTimeline ? { retainTimeline: true } : {}), }, }); }