From 247494a9aee8dec2deabecba1d940ce32df653ea Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 19 Apr 2026 20:58:24 +0900 Subject: [PATCH] fix(team-mailbox): dedupe pendingInjectedMessageIds across turns Without ack between turns the same unread messageIds would re-enter pendingInjectedMessageIds on every poll, unboundedly growing the list (state.json observed 8-12 copies of the same id under runtime pressure). Wrap the append with a Set so pending state stays idempotent regardless of how many times the transform hook polls before ack lands. --- .../team-mode/team-mailbox/poll.test.ts | 26 ++++++++++++++++++- src/features/team-mode/team-mailbox/poll.ts | 2 +- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/features/team-mode/team-mailbox/poll.test.ts b/src/features/team-mode/team-mailbox/poll.test.ts index 91c8afade..efed5415c 100644 --- a/src/features/team-mode/team-mailbox/poll.test.ts +++ b/src/features/team-mode/team-mailbox/poll.test.ts @@ -7,7 +7,7 @@ import { tmpdir } from "node:os" import path from "node:path" import { TeamModeConfigSchema } from "../../../config/schema/team-mode" -import { createRuntimeState } from "../team-state-store/store" +import { createRuntimeState, loadRuntimeState } from "../team-state-store/store" import type { TeamSpec } from "../types" import { sendMessage } from "./send" @@ -144,4 +144,28 @@ describe("pollAndBuildInjection", () => { expect(inboxEntries).toContain(`${secondMessageId}.json`) expect(inboxEntries).not.toContain("processed") }) + + test("deduplicates pendingInjectedMessageIds when the same unread message surfaces across turns", async () => { + // given + const { teamRunId, config } = await setupRuntime(["m1"]) + const messageId = randomUUID() + await sendMessage({ + version: 1, + messageId, + from: "lead", + to: "m1", + kind: "message", + body: "persistent", + timestamp: 100, + }, teamRunId, config, { isLead: true, activeMembers: ["m1"] }) + + // when + await pollAndBuildInjection("session-1", "m1", teamRunId, config, "turn-A") + await pollAndBuildInjection("session-1", "m1", teamRunId, config, "turn-B") + const runtimeState = await loadRuntimeState(teamRunId, config) + const member = runtimeState.members.find((entry) => entry.name === "m1") + + // then + expect(member?.pendingInjectedMessageIds).toEqual([messageId]) + }) }) diff --git a/src/features/team-mode/team-mailbox/poll.ts b/src/features/team-mode/team-mailbox/poll.ts index e2468dd62..853ba0de5 100644 --- a/src/features/team-mode/team-mailbox/poll.ts +++ b/src/features/team-mode/team-mailbox/poll.ts @@ -78,7 +78,7 @@ export async function pollAndBuildInjection( ? { ...member, lastInjectedTurnMarker: turnMarker, - pendingInjectedMessageIds: [...member.pendingInjectedMessageIds, ...messageIds], + pendingInjectedMessageIds: Array.from(new Set([...member.pendingInjectedMessageIds, ...messageIds])), } : member )),