mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 08:54:42 +00:00
fix(subagents): retain dismissed timelines and trim paths
This commit is contained in:
@@ -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, {
|
||||
|
||||
@@ -245,8 +245,11 @@ export const useProviderSubagentStore = create<ProviderSubagentState>((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 };
|
||||
}
|
||||
|
||||
@@ -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 } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3137,6 +3137,7 @@ export const ProviderSubagentUpdateMessageSchema = z.object({
|
||||
kind: z.literal("remove"),
|
||||
parentAgentId: z.string(),
|
||||
subagentId: z.string(),
|
||||
retainTimeline: z.boolean().optional(),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1192,6 +1192,7 @@ export class Session {
|
||||
kind: "remove",
|
||||
parentAgentId: update.parentAgentId,
|
||||
subagentId: update.subagentId,
|
||||
...(update.retainTimeline ? { retainTimeline: true } : {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user