mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 05:02:58 +00:00
fix(codex): fold live control starts into existing subagents
A wait starts without child states. Recognize it from its receivers so live handling consumes both lifecycle notifications, as replay does, instead of leaving an extra running control card behind. Normalize child status directly with the mapper's existing child-status rule.
This commit is contained in:
@@ -58,6 +58,7 @@ import {
|
||||
mapCodexToolCallEnvelope,
|
||||
mapCodexToolCallFromThreadItem,
|
||||
normalizeCommandExecutionCommand,
|
||||
normalizeCollabAgentChildStatus,
|
||||
splitCodexMcpToolResultImages,
|
||||
} from "./codex/tool-call-mapper.js";
|
||||
import {
|
||||
@@ -1867,7 +1868,12 @@ interface CodexCollabAgentUpdate {
|
||||
reopen: boolean;
|
||||
}
|
||||
|
||||
function readCodexCollabAgentUpdates(item: unknown): CodexCollabAgentUpdate[] {
|
||||
interface CodexCollabAgentControl {
|
||||
childThreadIds: string[];
|
||||
updates: CodexCollabAgentUpdate[];
|
||||
}
|
||||
|
||||
function readCodexCollabAgentControl(item: unknown): CodexCollabAgentControl | null {
|
||||
const record = toObjectRecord(item);
|
||||
if (
|
||||
!record ||
|
||||
@@ -1875,34 +1881,33 @@ function readCodexCollabAgentUpdates(item: unknown): CodexCollabAgentUpdate[] {
|
||||
"collabAgentToolCall" ||
|
||||
record.tool === "spawnAgent"
|
||||
) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
return Object.entries(toObjectRecord(record.agentsStates) ?? {}).flatMap(([threadId, value]) => {
|
||||
const state = toObjectRecord(value);
|
||||
if (typeof state?.status !== "string") return [];
|
||||
const mapped = mapCodexToolCallFromThreadItem({
|
||||
type: "collabAgentToolCall",
|
||||
id: record.id,
|
||||
tool: record.tool,
|
||||
status: "completed",
|
||||
agentsStates: { [threadId]: state },
|
||||
});
|
||||
return mapped
|
||||
? [
|
||||
{
|
||||
threadId,
|
||||
status:
|
||||
record.tool === "closeAgent" &&
|
||||
record.status === "completed" &&
|
||||
mapped.status === "running"
|
||||
? "canceled"
|
||||
: mapped.status,
|
||||
shutdown: state.status === "shutdown",
|
||||
reopen: record.tool === "sendInput" && record.status === "completed",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
});
|
||||
const updates = Object.entries(toObjectRecord(record.agentsStates) ?? {}).flatMap(
|
||||
([threadId, value]): CodexCollabAgentUpdate[] => {
|
||||
const state = toObjectRecord(value);
|
||||
if (typeof state?.status !== "string") return [];
|
||||
const status = normalizeCollabAgentChildStatus(state.status);
|
||||
return [
|
||||
{
|
||||
threadId,
|
||||
status:
|
||||
record.tool === "closeAgent" && record.status === "completed" && status === "running"
|
||||
? "canceled"
|
||||
: status,
|
||||
shutdown: state.status === "shutdown",
|
||||
reopen: record.tool === "sendInput" && record.status === "completed",
|
||||
},
|
||||
];
|
||||
},
|
||||
);
|
||||
const receivers = Array.isArray(record.receiverThreadIds)
|
||||
? record.receiverThreadIds.filter((id): id is string => typeof id === "string")
|
||||
: [];
|
||||
return {
|
||||
childThreadIds: [...new Set([...receivers, ...updates.map((update) => update.threadId)])],
|
||||
updates,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveCodexCollabAgentUpdate(
|
||||
@@ -2156,8 +2161,9 @@ function updateHistoricalCollabAgentCalls(
|
||||
timelineIndexByThreadId: Map<string, number>,
|
||||
item: unknown,
|
||||
): boolean {
|
||||
let updated = false;
|
||||
for (const update of readCodexCollabAgentUpdates(item)) {
|
||||
const control = readCodexCollabAgentControl(item);
|
||||
if (!control) return false;
|
||||
for (const update of control.updates) {
|
||||
const existingIndex = timelineIndexByThreadId.get(update.threadId);
|
||||
const existing = existingIndex === undefined ? undefined : timeline[existingIndex];
|
||||
if (existingIndex === undefined || existing?.item.type !== "tool_call") continue;
|
||||
@@ -2167,9 +2173,11 @@ function updateHistoricalCollabAgentCalls(
|
||||
? { ...existing.item, status, error: { message: "Sub-agent failed" } }
|
||||
: { ...existing.item, status, error: null };
|
||||
timeline[existingIndex] = { ...existing, item: toolCall };
|
||||
updated = true;
|
||||
}
|
||||
return updated;
|
||||
return (
|
||||
control.childThreadIds.length > 0 &&
|
||||
control.childThreadIds.every((id) => timelineIndexByThreadId.has(id))
|
||||
);
|
||||
}
|
||||
|
||||
async function loadCodexThreadHistoryTimeline(params: {
|
||||
@@ -5843,19 +5851,23 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
}
|
||||
|
||||
private handleRegisteredSubAgentActivity(rawItem: { [key: string]: unknown }): boolean {
|
||||
let updatedExistingSubagent = false;
|
||||
for (const update of readCodexCollabAgentUpdates(rawItem)) {
|
||||
const callId = this.subAgentCallIdByChildThreadId.get(update.threadId);
|
||||
if (!callId) continue;
|
||||
const state = this.subAgentCallsByCallId.get(callId);
|
||||
if (!state) continue;
|
||||
const status = resolveCodexCollabAgentUpdate(state.toolCall.status, update);
|
||||
if (status !== state.toolCall.status) {
|
||||
this.emitSubAgentActivityUpdate(callId, status, { reopen: update.reopen });
|
||||
const control = readCodexCollabAgentControl(rawItem);
|
||||
if (control) {
|
||||
for (const update of control.updates) {
|
||||
const callId = this.subAgentCallIdByChildThreadId.get(update.threadId);
|
||||
if (!callId) continue;
|
||||
const state = this.subAgentCallsByCallId.get(callId);
|
||||
if (!state) continue;
|
||||
const status = resolveCodexCollabAgentUpdate(state.toolCall.status, update);
|
||||
if (status !== state.toolCall.status) {
|
||||
this.emitSubAgentActivityUpdate(callId, status, { reopen: update.reopen });
|
||||
}
|
||||
}
|
||||
updatedExistingSubagent = true;
|
||||
return (
|
||||
control.childThreadIds.length > 0 &&
|
||||
control.childThreadIds.every((id) => this.subAgentCallIdByChildThreadId.has(id))
|
||||
);
|
||||
}
|
||||
if (updatedExistingSubagent) return true;
|
||||
const activity = readCodexSubAgentActivity(rawItem);
|
||||
if (!activity) {
|
||||
return false;
|
||||
|
||||
@@ -116,12 +116,27 @@ describe("Codex collaboration history", () => {
|
||||
async () => appServer.child,
|
||||
);
|
||||
const statuses = new Map<string, string>();
|
||||
const timelineCallIds = new Set<string>();
|
||||
session.subscribe((event) => {
|
||||
if (event.type === "provider_subagent" && event.event.type === "upsert") {
|
||||
statuses.set(event.event.id, event.event.status);
|
||||
}
|
||||
if (event.type === "timeline" && event.item.type === "tool_call") {
|
||||
timelineCallIds.add(event.item.callId);
|
||||
}
|
||||
});
|
||||
function complete(tool: string, states: Record<string, string>, id = tool) {
|
||||
if (tool !== "spawnAgent") {
|
||||
appServer.child.stdout.write(
|
||||
`${JSON.stringify({
|
||||
method: "item/started",
|
||||
params: {
|
||||
threadId: "thread-1",
|
||||
item: { ...collab(tool, states, id), status: "inProgress", agentsStates: {} },
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
appServer.child.stdout.write(
|
||||
`${JSON.stringify({
|
||||
method: "item/completed",
|
||||
@@ -147,6 +162,7 @@ describe("Codex collaboration history", () => {
|
||||
expect(statuses.get("first")).toBe("completed");
|
||||
complete("sendInput", { first: "running" });
|
||||
await vi.waitFor(() => expect(statuses.get("first")).toBe("running"));
|
||||
expect([...timelineCallIds]).toEqual(["spawn-first", "spawn-second"]);
|
||||
} finally {
|
||||
await session.close();
|
||||
appServer.assertNoErrors();
|
||||
|
||||
@@ -545,7 +545,7 @@ function readStatus(value: unknown): string | undefined {
|
||||
return typeof value.status === "string" ? value.status : undefined;
|
||||
}
|
||||
|
||||
function normalizeCollabAgentChildStatus(status: string): ToolCallTimelineItem["status"] {
|
||||
export function normalizeCollabAgentChildStatus(status: string): ToolCallTimelineItem["status"] {
|
||||
const normalized = status.trim().toLowerCase();
|
||||
switch (normalized) {
|
||||
case "error":
|
||||
|
||||
Reference in New Issue
Block a user