From 6eaff70b556a20b2f043b767f682c8c2bd9eea3d Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Tue, 24 Mar 2026 19:35:22 -0500 Subject: [PATCH] fix: ignore moved child rows in subagent announces --- .../subagent-announce.format.e2e.test.ts | 75 +++++++++++++++++++ src/agents/subagent-announce.ts | 45 ++++++++++- src/agents/subagent-registry-runtime.ts | 1 + 3 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/agents/subagent-announce.format.e2e.test.ts b/src/agents/subagent-announce.format.e2e.test.ts index 2497c0ce35d..3750a15d453 100644 --- a/src/agents/subagent-announce.format.e2e.test.ts +++ b/src/agents/subagent-announce.format.e2e.test.ts @@ -94,6 +94,9 @@ const { subagentRegistryMock } = vi.hoisted(() => ({ countActiveDescendantRuns: vi.fn((_sessionKey: string) => 0), countPendingDescendantRuns: vi.fn((_sessionKey: string) => 0), countPendingDescendantRunsExcludingRun: vi.fn((_sessionKey: string, _runId: string) => 0), + getLatestSubagentRunByChildSessionKey: vi.fn( + (_childSessionKey: string): MockSubagentRun | undefined => undefined, + ), listSubagentRunsForRequester: vi.fn( (_sessionKey: string, _scope?: { requesterRunId?: string }): MockSubagentRun[] => [], ), @@ -290,6 +293,9 @@ describe("subagent announce formatting", () => { .mockImplementation((sessionKey: string, _runId: string) => subagentRegistryMock.countPendingDescendantRuns(sessionKey), ); + subagentRegistryMock.getLatestSubagentRunByChildSessionKey + .mockClear() + .mockReturnValue(undefined); subagentRegistryMock.listSubagentRunsForRequester.mockClear().mockReturnValue([]); subagentRegistryMock.replaceSubagentRunAfterSteer.mockClear().mockReturnValue(true); subagentRegistryMock.resolveRequesterForChildSession.mockClear().mockReturnValue(null); @@ -2321,6 +2327,75 @@ describe("subagent announce formatting", () => { expect(msg.match(/1\. child-a/g)?.length ?? 0).toBe(1); }); + it("does not announce a direct child that moved to a newer parent", async () => { + subagentRegistryMock.countPendingDescendantRuns.mockReturnValue(0); + subagentRegistryMock.listSubagentRunsForRequester.mockImplementation( + (sessionKey: string, scope?: { requesterRunId?: string }) => { + if (sessionKey !== "agent:main:subagent:old-parent") { + return []; + } + if (scope?.requesterRunId !== "run-old-parent-settled") { + return []; + } + return [ + { + runId: "run-child-old-parent", + childSessionKey: "agent:main:subagent:shared-child", + requesterSessionKey: "agent:main:subagent:old-parent", + requesterDisplayKey: "old-parent", + task: "shared child task", + label: "shared-child", + cleanup: "keep", + createdAt: 10, + endedAt: 20, + cleanupCompletedAt: 21, + frozenResultText: "stale old parent result", + outcome: { status: "ok" }, + }, + ]; + }, + ); + subagentRegistryMock.getLatestSubagentRunByChildSessionKey.mockImplementation( + (childSessionKey: string) => { + if (childSessionKey !== "agent:main:subagent:shared-child") { + return undefined; + } + return { + runId: "run-child-new-parent", + childSessionKey, + requesterSessionKey: "agent:main:subagent:new-parent", + requesterDisplayKey: "new-parent", + task: "shared child task", + label: "shared-child", + cleanup: "keep", + createdAt: 11, + endedAt: 22, + cleanupCompletedAt: 23, + frozenResultText: "current new parent result", + outcome: { status: "ok" }, + }; + }, + ); + + const didAnnounce = await runSubagentAnnounceFlow({ + childSessionKey: "agent:main:subagent:old-parent", + childRunId: "run-old-parent-settled", + requesterSessionKey: "agent:main:main", + requesterDisplayKey: "main", + ...defaultOutcomeAnnounce, + expectsCompletionMessage: true, + roundOneReply: "old parent fallback reply", + }); + + expect(didAnnounce).toBe(true); + expect(agentSpy).toHaveBeenCalledTimes(1); + const call = agentSpy.mock.calls[0]?.[0] as { params?: { message?: string } }; + const msg = call?.params?.message ?? ""; + expect(msg).not.toContain("Child completion results:"); + expect(msg).not.toContain("stale old parent result"); + expect(msg).toContain("old parent fallback reply"); + }); + it("wakes an ended orchestrator run with settled child results before any upward announce", async () => { sessionStore = { "agent:main:subagent:parent": { diff --git a/src/agents/subagent-announce.ts b/src/agents/subagent-announce.ts index a9b857b5f34..098c16ee7f4 100644 --- a/src/agents/subagent-announce.ts +++ b/src/agents/subagent-announce.ts @@ -570,6 +570,43 @@ function dedupeLatestChildCompletionRows( return [...latestByChildSessionKey.values()]; } +function filterCurrentDirectChildCompletionRows( + children: Array<{ + runId: string; + childSessionKey: string; + requesterSessionKey: string; + task: string; + label?: string; + createdAt: number; + endedAt?: number; + frozenResultText?: string | null; + outcome?: SubagentRunOutcome; + }>, + params: { + requesterSessionKey: string; + getLatestSubagentRunByChildSessionKey?: (childSessionKey: string) => + | { + runId: string; + requesterSessionKey: string; + } + | null + | undefined; + }, +) { + if (typeof params.getLatestSubagentRunByChildSessionKey !== "function") { + return children; + } + return children.filter((child) => { + const latest = params.getLatestSubagentRunByChildSessionKey?.(child.childSessionKey); + if (!latest) { + return true; + } + return ( + latest.runId === child.runId && latest.requesterSessionKey === params.requesterSessionKey + ); + }); +} + function formatDurationShort(valueMs?: number) { if (!valueMs || !Number.isFinite(valueMs) || valueMs <= 0) { return "n/a"; @@ -1418,7 +1455,13 @@ export async function runSubagentAnnounceFlow(params: { ); if (Array.isArray(directChildren) && directChildren.length > 0) { childCompletionFindings = buildChildCompletionFindings( - dedupeLatestChildCompletionRows(directChildren), + dedupeLatestChildCompletionRows( + filterCurrentDirectChildCompletionRows(directChildren, { + requesterSessionKey: params.childSessionKey, + getLatestSubagentRunByChildSessionKey: + subagentRegistryRuntime.getLatestSubagentRunByChildSessionKey, + }), + ), ); } } diff --git a/src/agents/subagent-registry-runtime.ts b/src/agents/subagent-registry-runtime.ts index 567c0321543..99c0a5a3b84 100644 --- a/src/agents/subagent-registry-runtime.ts +++ b/src/agents/subagent-registry-runtime.ts @@ -2,6 +2,7 @@ export { countActiveDescendantRuns, countPendingDescendantRuns, countPendingDescendantRunsExcludingRun, + getLatestSubagentRunByChildSessionKey, isSubagentSessionRunActive, listSubagentRunsForRequester, replaceSubagentRunAfterSteer,