Skip to content

Commit d085143

Browse files
fix #86872: Subagent run reports success but fails to write output file (#92642)
* fix(codex): wait for native subagent completion Codex native subagent lifecycle status is only a progress signal; the task row should not report success until the transcript or native completion result is available. * fix(codex): preserve later native subagent failures * test(codex): freeze authoritative subagent results * fix(codex): preserve remote V1 completion fallback --------- Co-authored-by: Vincent Koc <[email protected]>
1 parent 3826cda commit d085143

5 files changed

Lines changed: 348 additions & 46 deletions

File tree

extensions/codex/src/app-server/native-subagent-monitor.test.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,191 @@ describe("CodexNativeSubagentMonitor", () => {
203203
task: "inspect the repo",
204204
}),
205205
);
206+
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith(
207+
expect.objectContaining({
208+
runId: "codex-thread:child-thread",
209+
progressSummary: "Codex native subagent is idle.",
210+
}),
211+
);
212+
expect(runtime.finalizeTaskRunByRunId).not.toHaveBeenCalled();
213+
});
214+
215+
it.each([
216+
{ label: "remote V1", codexHome: undefined, finalizes: true },
217+
{ label: "local transcript-backed V1", codexHome: "/tmp/codex-home", finalizes: false },
218+
])(
219+
"uses collab completion as a terminal fallback only for $label",
220+
async ({ codexHome, finalizes }) => {
221+
const client = createClient();
222+
const runtime = createRuntime();
223+
const monitor = new CodexNativeSubagentMonitor(client, runtime, { codexHome });
224+
monitor.registerParent({
225+
parentThreadId: "parent-thread",
226+
requesterSessionKey: "agent:main:main",
227+
taskRuntimeScope: createTaskScope("agent:main:main"),
228+
agentId: "main",
229+
});
230+
231+
await notifyChildStarted(client, "parent-thread", "child-thread", "");
232+
await client.notify({
233+
method: "item/completed",
234+
params: {
235+
threadId: "parent-thread",
236+
item: {
237+
type: "collabAgentToolCall",
238+
tool: "wait",
239+
senderThreadId: "parent-thread",
240+
agentsStates: {
241+
"child-thread": {
242+
status: "completed",
243+
message: "child final result",
244+
},
245+
},
246+
},
247+
},
248+
});
249+
250+
if (finalizes) {
251+
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith(
252+
expect.objectContaining({
253+
runId: "codex-thread:child-thread",
254+
status: "succeeded",
255+
terminalSummary: "child final result",
256+
}),
257+
);
258+
} else {
259+
expect(runtime.recordTaskRunProgressByRunId).toHaveBeenCalledWith(
260+
expect.objectContaining({
261+
runId: "codex-thread:child-thread",
262+
progressSummary: "child final result",
263+
}),
264+
);
265+
expect(runtime.finalizeTaskRunByRunId).not.toHaveBeenCalled();
266+
}
267+
monitor.dispose();
268+
},
269+
);
270+
271+
it("does not complete mirrored task rows from idle status before native completion", async () => {
272+
const client = createClient();
273+
const runtime = createRuntime();
274+
const monitor = new CodexNativeSubagentMonitor(client, runtime);
275+
monitor.registerParent({
276+
parentThreadId: "parent-thread",
277+
requesterSessionKey: "agent:main:discord:channel:C123",
278+
taskRuntimeScope: createTaskScope(),
279+
agentId: "main",
280+
});
281+
282+
await notifyChildStarted(client);
283+
await client.notify({
284+
method: "thread/status/changed",
285+
params: {
286+
threadId: "child-thread",
287+
status: { type: "idle" },
288+
},
289+
});
290+
291+
expect(runtime.finalizeTaskRunByRunId).not.toHaveBeenCalled();
292+
expect(runtime.deliverAgentHarnessTaskCompletion).not.toHaveBeenCalled();
293+
294+
await client.notify(
295+
nativeCompletionNotification({
296+
agentPath: "child-thread",
297+
statusLabel: "completed",
298+
result: "child final result",
299+
}),
300+
);
301+
302+
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith(
303+
expect.objectContaining({
304+
runId: "codex-thread:child-thread",
305+
status: "succeeded",
306+
terminalSummary: "child final result",
307+
}),
308+
);
309+
expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledWith(
310+
expect.objectContaining({
311+
childSessionId: "child-thread",
312+
result: "child final result",
313+
}),
314+
);
315+
});
316+
317+
it("keeps late idle lifecycle updates from overwriting native completion results", async () => {
318+
const client = createClient();
319+
const runtime = createRuntime();
320+
const monitor = new CodexNativeSubagentMonitor(client, runtime);
321+
monitor.registerParent({
322+
parentThreadId: "parent-thread",
323+
requesterSessionKey: "agent:main:discord:channel:C123",
324+
taskRuntimeScope: createTaskScope(),
325+
agentId: "main",
326+
});
327+
328+
await notifyChildStarted(client);
329+
await client.notify(
330+
nativeCompletionNotification({
331+
agentPath: "child-thread",
332+
statusLabel: "completed",
333+
result: "child final result",
334+
}),
335+
);
336+
runtime.recordTaskRunProgressByRunId.mockClear();
337+
338+
await client.notify({
339+
method: "thread/status/changed",
340+
params: {
341+
threadId: "child-thread",
342+
status: { type: "idle" },
343+
},
344+
});
345+
346+
expect(runtime.recordTaskRunProgressByRunId).not.toHaveBeenCalled();
347+
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledTimes(1);
206348
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith(
207349
expect.objectContaining({
208350
runId: "codex-thread:child-thread",
209351
status: "succeeded",
352+
terminalSummary: "child final result",
353+
}),
354+
);
355+
});
356+
357+
it("keeps later lifecycle errors from rewriting native completion results", async () => {
358+
const client = createClient();
359+
const runtime = createRuntime();
360+
const monitor = new CodexNativeSubagentMonitor(client, runtime);
361+
monitor.registerParent({
362+
parentThreadId: "parent-thread",
363+
requesterSessionKey: "agent:main:discord:channel:C123",
364+
taskRuntimeScope: createTaskScope(),
365+
agentId: "main",
366+
});
367+
368+
await notifyChildStarted(client);
369+
await client.notify(
370+
nativeCompletionNotification({
371+
agentPath: "child-thread",
372+
statusLabel: "completed",
373+
result: "child final result",
374+
}),
375+
);
376+
377+
await client.notify({
378+
method: "thread/status/changed",
379+
params: {
380+
threadId: "child-thread",
381+
status: { type: "systemError" },
382+
},
383+
});
384+
385+
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledTimes(1);
386+
expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith(
387+
expect.objectContaining({
388+
runId: "codex-thread:child-thread",
389+
status: "succeeded",
390+
terminalSummary: "child final result",
210391
}),
211392
);
212393
});

extensions/codex/src/app-server/native-subagent-monitor.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,9 @@ export class CodexNativeSubagentMonitor {
493493
if (!taskRuntime) {
494494
return;
495495
}
496+
this.getMirrorForChild(completion.childThreadId)?.markAuthoritativeCompletion(
497+
completion.childThreadId,
498+
);
496499
taskRuntime.finalizeTaskRunByRunId({
497500
runId: codexNativeSubagentRunId(completion.childThreadId),
498501
status: completion.status,
@@ -510,6 +513,12 @@ export class CodexNativeSubagentMonitor {
510513
return state?.taskRuntime;
511514
}
512515

516+
private getMirrorForChild(childThreadId: string): CodexNativeSubagentTaskMirror | undefined {
517+
const childState = this.childStates.get(childThreadId.trim());
518+
const state = childState ? this.parentStates.get(childState.parentThreadId) : undefined;
519+
return state?.mirror;
520+
}
521+
513522
private registerChildThread(
514523
parentThreadId: string,
515524
childThreadId: string,
@@ -526,6 +535,10 @@ export class CodexNativeSubagentMonitor {
526535
normalizedChildThreadId,
527536
);
528537
const agentPath = normalizeOptionalString(options.agentPath);
538+
const state = this.parentStates.get(normalizedParentThreadId);
539+
if (state?.mirror && (this.codexHome || agentPath)) {
540+
state.mirror.markAuthoritativeCompletionExpected(normalizedChildThreadId);
541+
}
529542
if (agentPath) {
530543
this.childThreadIdsByAgentPath.set(
531544
buildParentAgentPathKey(normalizedParentThreadId, agentPath),

0 commit comments

Comments
 (0)