
























@@ -11,7 +11,7 @@ import {
1111CodexNativeSubagentMonitor,
1212registerCodexNativeSubagentMonitor,
1313} from "./native-subagent-monitor.js";
14-import type { CodexServerNotification } from "./protocol.js";
14+import type { CodexServerNotification, JsonValue } from "./protocol.js";
15151616function createClient() {
1717const handlers = new Set<(notification: CodexServerNotification) => Promise<void> | void>();
@@ -158,6 +158,27 @@ function nativeCompletionNotification(params: {
158158};
159159}
160160161+function childTurnCompletedNotification(params: {
162+status: "completed" | "failed" | "interrupted";
163+error?: string;
164+turnId?: string;
165+items?: JsonValue[];
166+}): CodexServerNotification {
167+const turnId = params.turnId ?? "child-turn";
168+return {
169+method: "turn/completed",
170+params: {
171+threadId: "child-thread",
172+turn: {
173+id: turnId,
174+status: params.status,
175+ ...(params.items ? { items: params.items } : {}),
176+ ...(params.error ? { error: { message: params.error } } : {}),
177+},
178+},
179+};
180+}
181+161182describe("CodexNativeSubagentMonitor", () => {
162183it("keeps native subagent task mirroring alive on the shared client", async () => {
163184const client = createClient();
@@ -314,12 +335,10 @@ describe("CodexNativeSubagentMonitor", () => {
314335);
315336});
316337317-it("delivers child agent-message completion when a native subagent becomes idle", async () => {
338+it("delivers a completed child turn with its final agent message", async () => {
318339const client = createClient();
319340const runtime = createRuntime();
320-const monitor = new CodexNativeSubagentMonitor(client, runtime, {
321-codexHome: "/tmp/codex-home",
322-});
341+const monitor = new CodexNativeSubagentMonitor(client, runtime);
323342monitor.registerParent({
324343parentThreadId: "parent-thread",
325344requesterSessionKey: "agent:main:discord:channel:C123",
@@ -329,15 +348,21 @@ describe("CodexNativeSubagentMonitor", () => {
329348330349await notifyChildStarted(client);
331350await client.notify({
332-method: "item/completed",
351+method: "item/agentMessage/delta",
333352params: {
334353threadId: "child-thread",
335-item: {
336-type: "agentMessage",
337-id: "msg-child-final",
338-phase: "final_answer",
339-text: "child final result",
340-},
354+turnId: "child-turn",
355+itemId: "msg-child-final",
356+delta: "child ",
357+},
358+});
359+await client.notify({
360+method: "item/agentMessage/delta",
361+params: {
362+threadId: "child-thread",
363+turnId: "child-turn",
364+itemId: "msg-child-final",
365+delta: "final result",
341366},
342367});
343368@@ -351,6 +376,9 @@ describe("CodexNativeSubagentMonitor", () => {
351376},
352377});
353378379+expect(runtime.deliverAgentHarnessTaskCompletion).not.toHaveBeenCalled();
380+await client.notify(childTurnCompletedNotification({ status: "completed" }));
381+354382expect(runtime.finalizeTaskRunByRunId).toHaveBeenCalledWith(
355383expect.objectContaining({
356384runId: "codex-thread:child-thread",
@@ -362,20 +390,18 @@ describe("CodexNativeSubagentMonitor", () => {
362390expect.objectContaining({
363391childSessionId: "child-thread",
364392status: "succeeded",
365-statusLabel: "agent_message",
393+statusLabel: "turn_completed",
366394result: "child final result",
367395}),
368396);
369397370398client.close();
371399});
372400373-it("does not deliver commentary-only child messages as native subagent completion", async () => {
401+it("does not complete commentary-only child messages before a terminal turn", async () => {
374402const client = createClient();
375403const runtime = createRuntime();
376-const monitor = new CodexNativeSubagentMonitor(client, runtime, {
377-codexHome: "/tmp/codex-home",
378-});
404+const monitor = new CodexNativeSubagentMonitor(client, runtime);
379405monitor.registerParent({
380406parentThreadId: "parent-thread",
381407requesterSessionKey: "agent:main:discord:channel:C123",
@@ -384,10 +410,20 @@ describe("CodexNativeSubagentMonitor", () => {
384410});
385411386412await notifyChildStarted(client);
413+await client.notify({
414+method: "item/agentMessage/delta",
415+params: {
416+threadId: "child-thread",
417+turnId: "child-turn",
418+itemId: "msg-child-commentary",
419+delta: "checking now",
420+},
421+});
387422await client.notify({
388423method: "item/completed",
389424params: {
390425threadId: "child-thread",
426+turnId: "child-turn",
391427item: {
392428type: "agentMessage",
393429id: "msg-child-commentary",
@@ -407,6 +443,162 @@ describe("CodexNativeSubagentMonitor", () => {
407443expect(runtime.finalizeTaskRunByRunId).not.toHaveBeenCalled();
408444expect(runtime.deliverAgentHarnessTaskCompletion).not.toHaveBeenCalled();
409445446+await client.notify(childTurnCompletedNotification({ status: "completed" }));
447+448+expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledWith(
449+expect.objectContaining({
450+childSessionId: "child-thread",
451+result: "Codex native subagent completed without a final assistant message.",
452+}),
453+);
454+455+client.close();
456+});
457+458+it("delivers a completed child turn with its snapshot-only final message", async () => {
459+const client = createClient();
460+const runtime = createRuntime();
461+const monitor = new CodexNativeSubagentMonitor(client, runtime);
462+monitor.registerParent({
463+parentThreadId: "parent-thread",
464+requesterSessionKey: "agent:main:discord:channel:C123",
465+taskRuntimeScope: createTaskScope(),
466+agentId: "main",
467+});
468+469+await notifyChildStarted(client);
470+await client.notify(
471+childTurnCompletedNotification({
472+status: "completed",
473+items: [
474+{
475+id: "msg-child-snapshot",
476+type: "agentMessage",
477+phase: "final_answer",
478+text: "snapshot final result",
479+},
480+],
481+}),
482+);
483+484+expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledWith(
485+expect.objectContaining({
486+childSessionId: "child-thread",
487+result: "snapshot final result",
488+}),
489+);
490+491+client.close();
492+});
493+494+it("reconciles transcript text for a completed child turn without a final message", async () => {
495+const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-codex-subagent-"));
496+const codexHome = path.join(tempDir, "codex-home");
497+const transcriptDir = path.join(codexHome, "sessions", "2026", "06", "09");
498+await fs.mkdir(transcriptDir, { recursive: true });
499+await fs.writeFile(
500+path.join(transcriptDir, "rollout-2026-06-09T10-11-12-child-thread.jsonl"),
501+[
502+JSON.stringify({
503+type: "session_meta",
504+payload: {
505+source: {
506+subagent: {
507+thread_spawn: {
508+parent_thread_id: "parent-thread",
509+depth: 1,
510+},
511+},
512+},
513+},
514+}),
515+JSON.stringify({
516+timestamp: "2026-06-09T10:12:00.000Z",
517+type: "event_msg",
518+payload: {
519+type: "task_complete",
520+last_agent_message: "child turn transcript result",
521+completed_at: 1781009520,
522+},
523+}),
524+"",
525+].join("\n"),
526+);
527+const client = createClient();
528+const runtime = createRuntime();
529+const monitor = new CodexNativeSubagentMonitor(client, runtime, {
530+ codexHome,
531+transcriptPollDelaysMs: [60_000],
532+});
533+monitor.registerParent({
534+parentThreadId: "parent-thread",
535+requesterSessionKey: "agent:main:discord:channel:C123",
536+taskRuntimeScope: createTaskScope(),
537+agentId: "main",
538+});
539+540+await notifyChildStarted(client);
541+await client.notify(childTurnCompletedNotification({ status: "completed" }));
542+543+expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledWith(
544+expect.objectContaining({
545+childSessionId: "child-thread",
546+statusLabel: "task_complete",
547+result: "child turn transcript result",
548+}),
549+);
550+551+client.close();
552+});
553+554+it("does not reuse an interrupted child turn's message after resuming", async () => {
555+const client = createClient();
556+const runtime = createRuntime();
557+const monitor = new CodexNativeSubagentMonitor(client, runtime);
558+monitor.registerParent({
559+parentThreadId: "parent-thread",
560+requesterSessionKey: "agent:main:discord:channel:C123",
561+taskRuntimeScope: createTaskScope(),
562+agentId: "main",
563+});
564+565+await notifyChildStarted(client);
566+await client.notify({
567+method: "item/completed",
568+params: {
569+threadId: "child-thread",
570+turnId: "child-turn",
571+item: {
572+type: "agentMessage",
573+id: "msg-child-partial",
574+text: "partial child result",
575+},
576+},
577+});
578+await client.notify({
579+method: "thread/status/changed",
580+params: {
581+threadId: "child-thread",
582+status: { type: "idle" },
583+},
584+});
585+await client.notify(childTurnCompletedNotification({ status: "interrupted" }));
586+587+expect(runtime.deliverAgentHarnessTaskCompletion).not.toHaveBeenCalled();
588+589+await client.notify(
590+childTurnCompletedNotification({ status: "completed", turnId: "resumed-child-turn" }),
591+);
592+593+expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledTimes(1);
594+expect(runtime.deliverAgentHarnessTaskCompletion).toHaveBeenCalledWith(
595+expect.objectContaining({
596+childSessionId: "child-thread",
597+status: "succeeded",
598+result: "Codex native subagent completed without a final assistant message.",
599+}),
600+);
601+410602client.close();
411603});
412604此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。