fix(subagents): retain history across hydration

This commit is contained in:
Mohamed Boudra
2026-07-15 00:10:31 +02:00
parent d39127e2a6
commit 493a42ff75
5 changed files with 45 additions and 10 deletions
@@ -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 () => {
@@ -985,6 +985,9 @@ export class AgentManager {
const hasBusyDescendant = (agentId: string, visited = new Set<string>()): 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<AgentStreamEvent, { type: "provider_subagent" }>,
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 });
}
}
@@ -160,8 +160,8 @@ export class ProviderSubagentStore {
};
}
deleteParent(parentAgentId: string): ProviderSubagentStoreEvent[] {
const events: ProviderSubagentStoreEvent[] = [];
deleteParent(parentAgentId: string): Extract<ProviderSubagentStoreEvent, { type: "remove" }>[] {
const events: Extract<ProviderSubagentStoreEvent, { type: "remove" }>[] = [];
for (const subagent of this.list(parentAgentId)) {
const key = storeKey(parentAgentId, subagent.id);
this.descriptors.delete(key);
@@ -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",
},
});
});
@@ -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: [],
},