fix(codex): stop phantom parent subagents (#2214)

This commit is contained in:
Mohamed Boudra
2026-07-19 09:10:11 +02:00
committed by GitHub
parent c9bcfa7638
commit 3d86c738ff
2 changed files with 111 additions and 2 deletions

View File

@@ -3144,6 +3144,115 @@ describe("Codex app-server provider", () => {
]);
});
test("does not import a parent interaction from child history as another sub-agent", async () => {
const appServer = createFakeCodexAppServer({
"thread/read": (params) => {
const threadId = (params as { threadId?: string }).threadId;
if (threadId === "test-thread") {
return {
thread: {
turns: [
{
items: [
{
type: "subAgentActivity",
id: "child-started",
kind: "started",
agentThreadId: "child-thread",
agentPath: "/root/child",
},
],
},
],
},
};
}
return {
thread: {
turns: [
{
items: [
{
type: "subAgentActivity",
id: "parent-interacted",
kind: "interacted",
agentThreadId: "test-thread",
agentPath: "/root",
},
],
},
],
},
};
},
});
const session = new CodexAppServerAgentSession(
createConfig({ cwd: "/workspace/project" }),
{ sessionId: "test-thread" },
createTestLogger(),
async () => appServer.child,
);
try {
await session.connect();
const providerSubagentIds = new Set<string>();
for await (const event of session.streamHistory()) {
if (event.type === "provider_subagent" && event.event.type === "upsert") {
providerSubagentIds.add(event.event.id);
}
}
expect(providerSubagentIds).toEqual(new Set(["child-thread"]));
appServer.assertNoErrors();
} finally {
await session.close();
}
});
test("does not register a parent interaction on a child thread as another sub-agent", async () => {
const appServer = createFakeCodexAppServer();
const session = new CodexAppServerAgentSession(
createConfig({ cwd: "/workspace/project" }),
null,
createTestLogger(),
async () => appServer.child,
);
const providerSubagentIds = new Set<string>();
session.subscribe((event) => {
if (event.type === "provider_subagent" && event.event.type === "upsert") {
providerSubagentIds.add(event.event.id);
}
});
try {
const resultPromise = session.run("Delegate the investigation.");
await appServer.waitForTurnStart();
const child = waitForProviderSubagent(session, "child-thread");
appServer.startsSubAgent({
callId: "child-started",
threadId: "child-thread",
agentPath: "/root/child",
});
await child;
appServer.beginsSubAgentActivity({
callId: "parent-interacted",
threadId: "thread-1",
parentThreadId: "child-thread",
kind: "interacted",
agentPath: "/root",
});
appServer.completeTurn();
await resultPromise;
expect(providerSubagentIds).toEqual(new Set(["child-thread"]));
appServer.assertNoErrors();
} finally {
await session.close();
}
});
test("uses Codex turn timestamps for timestamp-less persisted history items", async () => {
const session = createSession();
session.client = {

View File

@@ -3503,7 +3503,7 @@ export class CodexAppServerAgentSession implements AgentSession {
rootRoutes: readonly PersistedSubAgentRoute[],
): Promise<void> {
const queue = rootRoutes.map((route) => ({ route, parentCallId: null as string | null }));
const visitedThreadIds = new Set<string>();
const visitedThreadIds = new Set(this.currentThreadId ? [this.currentThreadId] : []);
while (queue.length > 0 && visitedThreadIds.size < 100) {
const next = queue.shift();
if (!next || visitedThreadIds.has(next.route.childThreadId)) {
@@ -4831,7 +4831,7 @@ export class CodexAppServerAgentSession implements AgentSession {
: null;
const childThreadIds = Array.from(
new Set(agentThreadId ? [...receiverThreadIds, agentThreadId] : receiverThreadIds),
);
).filter((threadId) => threadId !== this.currentThreadId);
for (const receiverThreadId of childThreadIds) {
this.subAgentCallIdByChildThreadId.set(receiverThreadId, timelineItem.callId);
state.childThreadIds.add(receiverThreadId);