Subagents: restore announce chain + fix nested retry/drop regressions (#22223)

* Subagents: restore announce flow and fix nested delivery retries

* fix: prep subagent announce + docs alignment (#22223) (thanks @tyler6204)
This commit is contained in:
Tyler Yust
2026-02-20 15:39:09 -08:00
committed by GitHub
parent 086af56867
commit fe57bea088
21 changed files with 579 additions and 985 deletions

View File

@@ -7,8 +7,13 @@ type RequesterResolution = {
requesterOrigin?: Record<string, unknown>;
} | null;
type DescendantRun = {
runId: string;
requesterSessionKey: string;
childSessionKey: string;
};
const agentSpy = vi.fn(async (_req: AgentCallRequest) => ({ runId: "run-main", status: "ok" }));
const sendSpy = vi.fn(async (_req: AgentCallRequest) => ({ runId: "send-main", status: "ok" }));
const sessionsDeleteSpy = vi.fn((_req: AgentCallRequest) => undefined);
const readLatestAssistantReplyMock = vi.fn(
async (_sessionKey?: string): Promise<string | undefined> => "raw subagent reply",
@@ -22,11 +27,9 @@ const embeddedRunMock = {
const subagentRegistryMock = {
isSubagentSessionRunActive: vi.fn(() => true),
countActiveDescendantRuns: vi.fn((_sessionKey: string) => 0),
listDescendantRunsForRequester: vi.fn((_sessionKey: string): DescendantRun[] => []),
resolveRequesterForChildSession: vi.fn((_sessionKey: string): RequesterResolution => null),
};
const chatHistoryMock = vi.fn(async (_sessionKey?: string) => ({
messages: [] as Array<unknown>,
}));
let sessionStore: Record<string, Record<string, unknown>> = {};
let configOverride: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
session: {
@@ -67,15 +70,9 @@ vi.mock("../gateway/call.js", () => ({
if (typed.method === "agent") {
return await agentSpy(typed);
}
if (typed.method === "send") {
return await sendSpy(typed);
}
if (typed.method === "agent.wait") {
return { status: "error", startedAt: 10, endedAt: 20, error: "boom" };
}
if (typed.method === "chat.history") {
return await chatHistoryMock(typed.params?.sessionKey);
}
if (typed.method === "sessions.patch") {
return {};
}
@@ -115,7 +112,6 @@ vi.mock("../config/config.js", async (importOriginal) => {
describe("subagent announce formatting", () => {
beforeEach(() => {
agentSpy.mockClear();
sendSpy.mockClear();
sessionsDeleteSpy.mockClear();
embeddedRunMock.isEmbeddedPiRunActive.mockReset().mockReturnValue(false);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReset().mockReturnValue(false);
@@ -123,9 +119,9 @@ describe("subagent announce formatting", () => {
embeddedRunMock.waitForEmbeddedPiRunEnd.mockReset().mockResolvedValue(true);
subagentRegistryMock.isSubagentSessionRunActive.mockReset().mockReturnValue(true);
subagentRegistryMock.countActiveDescendantRuns.mockReset().mockReturnValue(0);
subagentRegistryMock.listDescendantRunsForRequester.mockReset().mockReturnValue([]);
subagentRegistryMock.resolveRequesterForChildSession.mockReset().mockReturnValue(null);
readLatestAssistantReplyMock.mockReset().mockResolvedValue("raw subagent reply");
chatHistoryMock.mockReset().mockResolvedValue({ messages: [] });
sessionStore = {};
configOverride = {
session: {
@@ -209,72 +205,6 @@ describe("subagent announce formatting", () => {
);
});
it.each([
{ role: "toolResult", toolOutput: "tool output line 1", childRunId: "run-tool-fallback-1" },
{ role: "tool", toolOutput: "tool output line 2", childRunId: "run-tool-fallback-2" },
] as const)(
"falls back to latest $role output when assistant reply is empty",
async (testCase) => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
chatHistoryMock.mockResolvedValueOnce({
messages: [
{
role: "assistant",
content: [{ type: "text", text: "" }],
},
{
role: testCase.role,
content: [{ type: "text", text: testCase.toolOutput }],
},
],
});
readLatestAssistantReplyMock.mockResolvedValue("");
await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: testCase.childRunId,
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
...defaultOutcomeAnnounce,
waitForCompletion: false,
});
const call = agentSpy.mock.calls[0]?.[0] as { params?: { message?: string } };
const msg = call?.params?.message as string;
expect(msg).toContain(testCase.toolOutput);
},
);
it("uses latest assistant text when it appears after a tool output", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
chatHistoryMock.mockResolvedValueOnce({
messages: [
{
role: "tool",
content: [{ type: "text", text: "tool output line" }],
},
{
role: "assistant",
content: [{ type: "text", text: "assistant final line" }],
},
],
});
readLatestAssistantReplyMock.mockResolvedValue("");
await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-latest-assistant",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
...defaultOutcomeAnnounce,
waitForCompletion: false,
});
const call = agentSpy.mock.calls[0]?.[0] as { params?: { message?: string } };
const msg = call?.params?.message as string;
expect(msg).toContain("assistant final line");
});
it("keeps full findings and includes compact stats", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
sessionStore = {
@@ -312,121 +242,6 @@ describe("subagent announce formatting", () => {
expect(msg).toContain("step-139");
});
it("sends deterministic completion message directly for manual spawn completion", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
sessionStore = {
"agent:main:subagent:test": {
sessionId: "child-session-direct",
inputTokens: 12,
outputTokens: 34,
totalTokens: 46,
},
"agent:main:main": {
sessionId: "requester-session",
},
};
chatHistoryMock.mockResolvedValueOnce({
messages: [{ role: "assistant", content: [{ type: "text", text: "final answer: 2" }] }],
});
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-completion",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
...defaultOutcomeAnnounce,
expectsCompletionMessage: true,
});
expect(didAnnounce).toBe(true);
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(agentSpy).not.toHaveBeenCalled();
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
const rawMessage = call?.params?.message;
const msg = typeof rawMessage === "string" ? rawMessage : "";
expect(call?.params?.channel).toBe("discord");
expect(call?.params?.to).toBe("channel:12345");
expect(call?.params?.sessionKey).toBe("agent:main:main");
expect(msg).toContain("✅ Subagent main finished");
expect(msg).toContain("final answer: 2");
expect(msg).not.toContain("Convert the result above into your normal assistant voice");
});
it("ignores stale session thread hints for manual completion direct-send", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
sessionStore = {
"agent:main:subagent:test": {
sessionId: "child-session-direct-thread",
},
"agent:main:main": {
sessionId: "requester-session-thread",
lastChannel: "discord",
lastTo: "channel:stale",
lastThreadId: 42,
},
};
chatHistoryMock.mockResolvedValueOnce({
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
});
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-stale-thread",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
requesterOrigin: { channel: "discord", to: "channel:12345", accountId: "acct-1" },
...defaultOutcomeAnnounce,
expectsCompletionMessage: true,
});
expect(didAnnounce).toBe(true);
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(agentSpy).not.toHaveBeenCalled();
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.channel).toBe("discord");
expect(call?.params?.to).toBe("channel:12345");
expect(call?.params?.threadId).toBeUndefined();
});
it("passes requesterOrigin.threadId for manual completion direct-send", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
sessionStore = {
"agent:main:subagent:test": {
sessionId: "child-session-direct-thread-pass",
},
"agent:main:main": {
sessionId: "requester-session-thread-pass",
},
};
chatHistoryMock.mockResolvedValueOnce({
messages: [{ role: "assistant", content: [{ type: "text", text: "done" }] }],
});
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-thread-pass",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
requesterOrigin: {
channel: "discord",
to: "channel:12345",
accountId: "acct-1",
threadId: 99,
},
...defaultOutcomeAnnounce,
expectsCompletionMessage: true,
});
expect(didAnnounce).toBe(true);
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(agentSpy).not.toHaveBeenCalled();
const call = sendSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.channel).toBe("discord");
expect(call?.params?.to).toBe("channel:12345");
expect(call?.params?.threadId).toBe("99");
});
it("steers announcements into an active run when queue mode is steer", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
@@ -541,139 +356,6 @@ describe("subagent announce formatting", () => {
expect(new Set(idempotencyKeys).size).toBe(2);
});
it("prefers direct delivery first for completion-mode and then queues on direct failure", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
sessionStore = {
"agent:main:main": {
sessionId: "session-collect",
lastChannel: "whatsapp",
lastTo: "+1555",
queueMode: "collect",
queueDebounceMs: 0,
},
};
sendSpy.mockRejectedValueOnce(new Error("direct delivery unavailable"));
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-completion-direct-fallback",
requesterSessionKey: "main",
requesterDisplayKey: "main",
expectsCompletionMessage: true,
...defaultOutcomeAnnounce,
});
expect(didAnnounce).toBe(true);
await expect.poll(() => sendSpy.mock.calls.length).toBe(1);
await expect.poll(() => agentSpy.mock.calls.length).toBe(1);
expect(sendSpy.mock.calls[0]?.[0]).toMatchObject({
method: "send",
params: { sessionKey: "agent:main:main" },
});
expect(agentSpy.mock.calls[0]?.[0]).toMatchObject({
method: "agent",
params: { sessionKey: "agent:main:main" },
});
expect(agentSpy.mock.calls[0]?.[0]).toMatchObject({
method: "agent",
params: { channel: "whatsapp", to: "+1555", deliver: true },
});
});
it("returns failure for completion-mode when direct delivery fails and queue fallback is unavailable", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
sessionStore = {
"agent:main:main": {
sessionId: "session-direct-only",
lastChannel: "whatsapp",
lastTo: "+1555",
},
};
sendSpy.mockRejectedValueOnce(new Error("direct delivery unavailable"));
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-completion-direct-fail",
requesterSessionKey: "main",
requesterDisplayKey: "main",
expectsCompletionMessage: true,
...defaultOutcomeAnnounce,
});
expect(didAnnounce).toBe(false);
expect(sendSpy).toHaveBeenCalledTimes(1);
expect(agentSpy).toHaveBeenCalledTimes(0);
});
it("uses assistant output for completion-mode when latest assistant text exists", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
chatHistoryMock.mockResolvedValueOnce({
messages: [
{
role: "toolResult",
content: [{ type: "text", text: "old tool output" }],
},
{
role: "assistant",
content: [{ type: "text", text: "assistant completion text" }],
},
],
});
readLatestAssistantReplyMock.mockResolvedValue("assistant ignored fallback");
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-completion-assistant-output",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
expectsCompletionMessage: true,
...defaultOutcomeAnnounce,
});
expect(didAnnounce).toBe(true);
await expect.poll(() => sendSpy.mock.calls.length).toBe(1);
const call = sendSpy.mock.calls[0]?.[0] as { params?: { message?: string } };
const msg = call?.params?.message as string;
expect(msg).toContain("assistant completion text");
expect(msg).not.toContain("old tool output");
});
it("falls back to latest tool output for completion-mode when assistant output is empty", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
chatHistoryMock.mockResolvedValueOnce({
messages: [
{
role: "assistant",
content: [{ type: "text", text: "" }],
},
{
role: "toolResult",
content: [{ type: "text", text: "tool output only" }],
},
],
});
readLatestAssistantReplyMock.mockResolvedValue("");
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-completion-tool-output",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
expectsCompletionMessage: true,
...defaultOutcomeAnnounce,
});
expect(didAnnounce).toBe(true);
await expect.poll(() => sendSpy.mock.calls.length).toBe(1);
const call = sendSpy.mock.calls[0]?.[0] as { params?: { message?: string } };
const msg = call?.params?.message as string;
expect(msg).toContain("tool output only");
});
it("queues announce delivery back into requester subagent session", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
@@ -706,24 +388,7 @@ describe("subagent announce formatting", () => {
expect(call?.params?.to).toBeUndefined();
});
it.each([
{
testName: "includes threadId when origin has an active topic/thread",
childRunId: "run-thread",
expectedThreadId: "42",
requesterOrigin: undefined,
},
{
testName: "prefers requesterOrigin.threadId over session entry threadId",
childRunId: "run-thread-override",
expectedThreadId: "99",
requesterOrigin: {
channel: "telegram",
to: "telegram:123",
threadId: 99,
},
},
] as const)("$testName", async (testCase) => {
it("includes threadId when origin has an active topic/thread", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
@@ -740,10 +405,9 @@ describe("subagent announce formatting", () => {
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: testCase.childRunId,
childRunId: "run-thread",
requesterSessionKey: "main",
requesterDisplayKey: "main",
...(testCase.requesterOrigin ? { requesterOrigin: testCase.requesterOrigin } : {}),
...defaultOutcomeAnnounce,
});
@@ -751,7 +415,42 @@ describe("subagent announce formatting", () => {
const params = await getSingleAgentCallParams();
expect(params.channel).toBe("telegram");
expect(params.to).toBe("telegram:123");
expect(params.threadId).toBe(testCase.expectedThreadId);
expect(params.threadId).toBe("42");
});
it("prefers requesterOrigin.threadId over session entry threadId", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
sessionStore = {
"agent:main:main": {
sessionId: "session-thread-override",
lastChannel: "telegram",
lastTo: "telegram:123",
lastThreadId: 42,
queueMode: "collect",
queueDebounceMs: 0,
},
};
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-thread-override",
requesterSessionKey: "main",
requesterDisplayKey: "main",
requesterOrigin: {
channel: "telegram",
to: "telegram:123",
threadId: 99,
},
...defaultOutcomeAnnounce,
});
expect(didAnnounce).toBe(true);
await expect.poll(() => agentSpy.mock.calls.length).toBe(1);
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.threadId).toBe("99");
});
it("splits collect-mode queues when accountId differs", async () => {
@@ -795,31 +494,16 @@ describe("subagent announce formatting", () => {
expect(accountIds).toEqual(expect.arrayContaining(["acct-a", "acct-b"]));
});
it.each([
{
testName: "uses requester origin for direct announce when not queued",
childRunId: "run-direct",
requesterOrigin: { channel: "whatsapp", accountId: "acct-123" },
expectedChannel: "whatsapp",
expectedAccountId: "acct-123",
},
{
testName: "normalizes requesterOrigin for direct announce delivery",
childRunId: "run-direct-origin",
requesterOrigin: { channel: " whatsapp ", accountId: " acct-987 " },
expectedChannel: "whatsapp",
expectedAccountId: "acct-987",
},
] as const)("$testName", async (testCase) => {
it("uses requester origin for direct announce when not queued", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: testCase.childRunId,
childRunId: "run-direct",
requesterSessionKey: "agent:main:main",
requesterOrigin: testCase.requesterOrigin,
requesterOrigin: { channel: "whatsapp", accountId: "acct-123" },
requesterDisplayKey: "main",
...defaultOutcomeAnnounce,
});
@@ -829,8 +513,8 @@ describe("subagent announce formatting", () => {
params?: Record<string, unknown>;
expectFinal?: boolean;
};
expect(call?.params?.channel).toBe(testCase.expectedChannel);
expect(call?.params?.accountId).toBe(testCase.expectedAccountId);
expect(call?.params?.channel).toBe("whatsapp");
expect(call?.params?.accountId).toBe("acct-123");
expect(call?.expectFinal).toBe(true);
});
@@ -933,6 +617,93 @@ describe("subagent announce formatting", () => {
expect(agentSpy).not.toHaveBeenCalled();
});
it("waits for follow-up reply when descendant runs exist and child reply is still waiting", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
const waitingReply = "Spawned the nested subagent. Waiting for its auto-announced result.";
const finalReply = "Nested subagent finished and I synthesized the final result.";
subagentRegistryMock.listDescendantRunsForRequester.mockImplementation((sessionKey: string) =>
sessionKey === "agent:main:subagent:parent"
? [
{
runId: "run-leaf",
requesterSessionKey: sessionKey,
childSessionKey: "agent:main:subagent:parent:subagent:leaf",
},
]
: [],
);
readLatestAssistantReplyMock
.mockResolvedValueOnce(waitingReply)
.mockResolvedValueOnce(waitingReply)
.mockResolvedValueOnce(finalReply);
vi.useFakeTimers();
try {
const announcePromise = runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:parent",
childRunId: "run-parent",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
...defaultOutcomeAnnounce,
});
await vi.advanceTimersByTimeAsync(500);
const didAnnounce = await announcePromise;
expect(didAnnounce).toBe(true);
} finally {
vi.useRealTimers();
}
const call = agentSpy.mock.calls[0]?.[0] as { params?: { message?: string } };
const msg = call?.params?.message as string;
expect(msg).toContain(finalReply);
expect(msg).not.toContain("Waiting for its auto-announced result.");
});
it("defers announce when descendant follow-up reply has not arrived yet", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
const waitingReply = "Spawned the nested subagent. Waiting for its auto-announced result.";
subagentRegistryMock.listDescendantRunsForRequester.mockImplementation((sessionKey: string) =>
sessionKey === "agent:main:subagent:parent"
? [
{
runId: "run-leaf",
requesterSessionKey: sessionKey,
childSessionKey: "agent:main:subagent:parent:subagent:leaf",
},
]
: [],
);
readLatestAssistantReplyMock.mockResolvedValue(waitingReply);
vi.useFakeTimers();
try {
const announcePromise = runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:parent",
childRunId: "run-parent-still-waiting",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
task: "nested test",
timeoutMs: 700,
cleanup: "keep",
waitForCompletion: false,
startedAt: 10,
endedAt: 20,
outcome: { status: "ok" },
});
await vi.advanceTimersByTimeAsync(1200);
const didAnnounce = await announcePromise;
expect(didAnnounce).toBe(false);
} finally {
vi.useRealTimers();
}
expect(agentSpy).not.toHaveBeenCalled();
});
it("bubbles child announce to parent requester when requester subagent already ended", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
subagentRegistryMock.isSubagentSessionRunActive.mockReturnValue(false);
@@ -1013,6 +784,26 @@ describe("subagent announce formatting", () => {
expect(agentSpy).not.toHaveBeenCalled();
});
it("normalizes requesterOrigin for direct announce delivery", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-direct-origin",
requesterSessionKey: "agent:main:main",
requesterOrigin: { channel: " whatsapp ", accountId: " acct-987 " },
requesterDisplayKey: "main",
...defaultOutcomeAnnounce,
});
expect(didAnnounce).toBe(true);
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.channel).toBe("whatsapp");
expect(call?.params?.accountId).toBe("acct-987");
});
it("prefers requesterOrigin channel over stale session lastChannel in queued announce", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
@@ -1045,6 +836,35 @@ describe("subagent announce formatting", () => {
expect(call?.params?.to).toBe("bluebubbles:chat_guid:123");
});
it("falls back to persisted deliverable route when requesterOrigin channel is non-deliverable", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
sessionStore = {
"agent:main:main": {
sessionId: "session-webchat-origin",
lastChannel: "discord",
lastTo: "discord:channel:123",
lastAccountId: "acct-store",
},
};
const didAnnounce = await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:test",
childRunId: "run-webchat-origin",
requesterSessionKey: "main",
requesterOrigin: { channel: "webchat", to: "ignored", accountId: "acct-live" },
requesterDisplayKey: "main",
...defaultOutcomeAnnounce,
});
expect(didAnnounce).toBe(true);
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.channel).toBe("discord");
expect(call?.params?.to).toBe("discord:channel:123");
expect(call?.params?.accountId).toBe("acct-live");
});
it("routes to parent subagent when parent run ended but session still exists (#18037)", async () => {
// Scenario: Newton (depth-1) spawns Birdie (depth-2). Newton's agent turn ends
// after spawning but Newton's SESSION still exists (waiting for Birdie's result).