fix(announce): use deterministic idempotency keys to prevent duplicate subagent announces (#17150)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 54bba3cea1
Co-authored-by: widingmarcus-cyber <245375637+widingmarcus-cyber@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Marcus Widing
2026-02-15 16:34:34 +01:00
committed by GitHub
parent 7ea14a1c87
commit ade11ec892
5 changed files with 132 additions and 3 deletions

View File

@@ -149,6 +149,28 @@ describe("subagent announce formatting", () => {
expect(msg).toContain("completed successfully");
});
it("uses child-run announce identity for direct idempotency", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-direct-idem",
requesterSessionKey: "agent:main:main",
requesterDisplayKey: "main",
task: "do thing",
timeoutMs: 1000,
cleanup: "keep",
waitForCompletion: false,
startedAt: 10,
endedAt: 20,
outcome: { status: "ok" },
});
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
expect(call?.params?.idempotencyKey).toBe(
"announce:v1:agent:main:subagent:worker:run-direct-idem",
);
});
it("keeps full findings and includes compact stats", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
sessionStore = {
@@ -266,6 +288,60 @@ describe("subagent announce formatting", () => {
expect(call?.params?.accountId).toBe("kev");
});
it("keeps queued idempotency unique for same-ms distinct child runs", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
sessionStore = {
"agent:main:main": {
sessionId: "session-followup",
lastChannel: "whatsapp",
lastTo: "+1555",
queueMode: "followup",
queueDebounceMs: 0,
},
};
const nowSpy = vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
try {
await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-1",
requesterSessionKey: "main",
requesterDisplayKey: "main",
task: "first task",
timeoutMs: 1000,
cleanup: "keep",
waitForCompletion: false,
startedAt: 10,
endedAt: 20,
outcome: { status: "ok" },
});
await runSubagentAnnounceFlow({
childSessionKey: "agent:main:subagent:worker",
childRunId: "run-2",
requesterSessionKey: "main",
requesterDisplayKey: "main",
task: "second task",
timeoutMs: 1000,
cleanup: "keep",
waitForCompletion: false,
startedAt: 10,
endedAt: 20,
outcome: { status: "ok" },
});
} finally {
nowSpy.mockRestore();
}
await expect.poll(() => agentSpy.mock.calls.length).toBe(2);
const idempotencyKeys = agentSpy.mock.calls
.map((call) => (call[0] as { params?: Record<string, unknown> })?.params?.idempotencyKey)
.filter((value): value is string => typeof value === "string");
expect(idempotencyKeys).toContain("announce:v1:agent:main:subagent:worker:run-1");
expect(idempotencyKeys).toContain("announce:v1:agent:main:subagent:worker:run-2");
expect(new Set(idempotencyKeys).size).toBe(2);
});
it("queues announce delivery back into requester subagent session", async () => {
const { runSubagentAnnounceFlow } = await import("./subagent-announce.js");
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(true);