mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 13:13:25 +00:00
fix(codex): keep control cards from claiming child routes
Only spawn announcements establish child ownership. Wait and close results update existing children, while failed resume attempts retain their own visible result without replacing a previous child outcome.
This commit is contained in:
@@ -1892,6 +1892,7 @@ function readCodexCollabAgentControl(item: unknown): CodexCollabAgentControl | n
|
||||
? { kind: "submission", childThreadIds: receivers }
|
||||
: null;
|
||||
}
|
||||
if (tool !== "wait" && tool !== "closeAgent") return null;
|
||||
const outcomeStates = toObjectRecord(record.agentsStates);
|
||||
const updates = Object.entries(outcomeStates ?? {}).flatMap(
|
||||
([threadId, value]): CodexCollabAgentUpdate[] => {
|
||||
@@ -1968,7 +1969,7 @@ function updateHistoricalSubAgentActivity(
|
||||
};
|
||||
}
|
||||
|
||||
function readCodexHistoricalSubAgentThreadIds(item: unknown): string[] {
|
||||
function readCodexSpawnedSubAgentThreadIds(item: unknown): string[] {
|
||||
const activity = readCodexSubAgentActivity(item);
|
||||
if (activity) {
|
||||
return activity.kind === "started" ? [activity.agentThreadId] : [];
|
||||
@@ -2237,7 +2238,7 @@ async function projectCodexThreadHistory(
|
||||
? { providerTurnId: row.turnId }
|
||||
: {}),
|
||||
});
|
||||
for (const childThreadId of readCodexHistoricalSubAgentThreadIds(item)) {
|
||||
for (const childThreadId of readCodexSpawnedSubAgentThreadIds(item)) {
|
||||
subAgentsByThreadId.set(childThreadId, {
|
||||
timelineIndex: timeline.length - 1,
|
||||
pendingFollowUp: false,
|
||||
@@ -4137,6 +4138,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
this.registerSubAgentToolCall({
|
||||
timelineItem: next.route.toolCall,
|
||||
rawItem: { agentThreadId: next.route.childThreadId },
|
||||
childThreadIds: [next.route.childThreadId],
|
||||
parentCallId: next.parentCallId,
|
||||
parentSubagentId: next.parentSubagentId,
|
||||
});
|
||||
@@ -5784,6 +5786,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
private registerSubAgentToolCall(params: {
|
||||
timelineItem: ToolCallTimelineItem;
|
||||
rawItem: { [key: string]: unknown };
|
||||
childThreadIds: readonly string[];
|
||||
parentCallId: string | null;
|
||||
parentSubagentId: string | null;
|
||||
}): string[] {
|
||||
@@ -5825,16 +5828,9 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
this.subAgentCallsByCallId.set(timelineItem.callId, state);
|
||||
|
||||
const receiverThreadIds = Array.isArray(rawItem.receiverThreadIds)
|
||||
? rawItem.receiverThreadIds.filter((value): value is string => typeof value === "string")
|
||||
: [];
|
||||
const agentThreadId =
|
||||
typeof rawItem.agentThreadId === "string" && rawItem.agentThreadId.length > 0
|
||||
? rawItem.agentThreadId
|
||||
: null;
|
||||
const childThreadIds = Array.from(
|
||||
new Set(agentThreadId ? [...receiverThreadIds, agentThreadId] : receiverThreadIds),
|
||||
).filter((threadId) => threadId !== this.currentThreadId);
|
||||
const childThreadIds = Array.from(new Set(params.childThreadIds)).filter(
|
||||
(threadId) => threadId !== this.currentThreadId,
|
||||
);
|
||||
for (const receiverThreadId of childThreadIds) {
|
||||
const owningCallId = this.subAgentCallIdByChildThreadId.get(receiverThreadId);
|
||||
if (owningCallId && owningCallId !== timelineItem.callId) {
|
||||
@@ -6770,6 +6766,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
? this.registerSubAgentToolCall({
|
||||
timelineItem,
|
||||
rawItem: parsed.item,
|
||||
childThreadIds: readCodexSpawnedSubAgentThreadIds(parsed.item),
|
||||
parentCallId: childSubAgentCallId,
|
||||
parentSubagentId: resolveCodexParentSubagentId(childSubAgentCallId, parsed.threadId),
|
||||
})
|
||||
@@ -6937,6 +6934,7 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
const registeredChildThreadIds = this.registerSubAgentToolCall({
|
||||
timelineItem,
|
||||
rawItem: parsed.item,
|
||||
childThreadIds: readCodexSpawnedSubAgentThreadIds(parsed.item),
|
||||
parentCallId: childSubAgentCallId,
|
||||
parentSubagentId: resolveCodexParentSubagentId(childSubAgentCallId, parsed.threadId),
|
||||
});
|
||||
|
||||
@@ -174,6 +174,35 @@ describe("Codex collaboration history", () => {
|
||||
).toEqual([{ id: "closeAgent", status: "canceled" }]);
|
||||
});
|
||||
|
||||
test.each(["notFound", "errored"])(
|
||||
"preserves completion and shows a failed resume reporting %s",
|
||||
async (status) => {
|
||||
const history = await replay([
|
||||
collab("spawnAgent", { first: "pendingInit" }, "spawn-first"),
|
||||
collab("wait", { first: "completed" }),
|
||||
collab("closeAgent", { first: "completed" }),
|
||||
{ ...collab("resumeAgent", { first: status }), status: "failed" },
|
||||
]);
|
||||
expect(
|
||||
history.flatMap((event) =>
|
||||
event.type === "provider_subagent" && event.event.type === "upsert"
|
||||
? [event.event.status]
|
||||
: [],
|
||||
),
|
||||
).toEqual(["completed"]);
|
||||
expect(
|
||||
history.flatMap((event) =>
|
||||
event.type === "timeline" && event.item.type === "tool_call"
|
||||
? [{ id: event.item.callId, status: event.item.status }]
|
||||
: [],
|
||||
),
|
||||
).toEqual([
|
||||
{ id: "spawn-first", status: "completed" },
|
||||
{ id: "resumeAgent", status: "failed" },
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
test("stops counting a child closed while running", async () => {
|
||||
const history = await replay([
|
||||
collab("spawnAgent", { first: "pendingInit" }, "spawn-first"),
|
||||
@@ -330,13 +359,15 @@ describe("Codex collaboration history", () => {
|
||||
});
|
||||
await vi.waitFor(() => expect(cards.get("mixed-wait")).toBe("running"));
|
||||
notify("item/completed", collab("wait", { first: "completed" }, "mixed-wait"));
|
||||
notify("item/completed", collab("wait", { second: "completed" }, "wait-second"));
|
||||
appServer.completeTurn();
|
||||
await result;
|
||||
expect([...statuses]).toEqual([["first", "completed"]]);
|
||||
expect([...cards]).toEqual([
|
||||
["spawn-first", "completed"],
|
||||
["mixed-wait", "completed"],
|
||||
["wait-second", "completed"],
|
||||
]);
|
||||
expect(statuses.get("first")).toBe("completed");
|
||||
} finally {
|
||||
await session.close();
|
||||
appServer.assertNoErrors();
|
||||
|
||||
Reference in New Issue
Block a user