fix(team-mode): defer live mailbox acks

This commit is contained in:
YeonGyu-Kim
2026-05-15 22:49:10 +09:00
parent ae7ff3bb7e
commit 196f6512ae
7 changed files with 301 additions and 27 deletions
+15 -9
View File
@@ -17,18 +17,24 @@ export async function ackMessages(
for (const messageId of messageIds) {
const messageFileName = `${messageId}.json`
const sourcePath = path.join(inboxDir, messageFileName)
const sourcePaths = [
path.join(inboxDir, messageFileName),
path.join(inboxDir, `.delivering-${messageFileName}`),
]
const targetPath = path.join(processedDir, messageFileName)
try {
await rename(sourcePath, targetPath)
} catch (error) {
const err = error as NodeJS.ErrnoException
if (err.code === "ENOENT") {
continue
}
for (const sourcePath of sourcePaths) {
try {
await rename(sourcePath, targetPath)
break
} catch (error) {
const err = error as NodeJS.ErrnoException
if (err.code === "ENOENT") {
continue
}
throw error
throw error
}
}
}
}
@@ -530,7 +530,7 @@ describe("createTeamSendMessageTool", () => {
expect(message.from).toBe("m1")
})
test("acks the message after live delivery so the transform hook does not redeliver", async () => {
test("keeps live-delivered messages reserved until the recipient idles", async () => {
// given
const fixture = await createTeamFixture()
const { client } = createRecordingClient()
@@ -546,9 +546,13 @@ describe("createTeamSendMessageTool", () => {
// then
const inboxDir = getInboxDir(resolveBaseDir(fixture.config), fixture.teamRunId, "m2")
const inboxEntries = (await readdir(inboxDir)).filter((entry) => entry.endsWith(".json"))
const processedEntries = (await readdir(path.join(inboxDir, "processed"))).filter((entry) => entry.endsWith(".json"))
expect(inboxEntries).toHaveLength(0)
expect(processedEntries).toHaveLength(1)
expect(inboxEntries).toHaveLength(1)
expect(inboxEntries[0]?.startsWith(".delivering-")).toBe(true)
const { loadRuntimeState: loadState } = await import("../team-state-store/store")
const runtimeState = await loadState(fixture.teamRunId, fixture.config)
const recipient = runtimeState.members.find((member) => member.name === "m2")
expect(recipient?.pendingInjectedMessageIds).toHaveLength(1)
})
test("broadcast fans out live delivery to every member except the sender", async () => {
+25 -8
View File
@@ -1,22 +1,20 @@
import { randomUUID } from "node:crypto"
import { tool, type ToolDefinition } from "@opencode-ai/plugin/tool"
import { type ToolDefinition, tool } from "@opencode-ai/plugin/tool"
import { z } from "zod"
import type { TeamModeConfig } from "../../../config/schema/team-mode"
import { promptAsyncAfterSessionIdle } from "../../../hooks/shared/prompt-async-gate"
import { log } from "../../../shared/logger"
import { applyMemberSessionRouting, buildMemberPromptBody } from "../member-session-routing"
import { lookupTeamSession } from "../team-session-registry"
import { loadRuntimeState } from "../team-state-store/store"
import { buildEnvelope } from "../team-mailbox/poll"
import {
commitDeliveryReservation,
releaseDeliveryReservation,
reserveMessageForDelivery,
} from "../team-mailbox/reservation"
import { BroadcastNotPermittedError, sendMessage } from "../team-mailbox/send"
import { promptAsyncAfterSessionIdle } from "../../../hooks/shared/prompt-async-gate"
import { lookupTeamSession } from "../team-session-registry"
import { loadRuntimeState, transitionRuntimeState } from "../team-state-store/store"
import type { Message } from "../types"
import { MessageSchema } from "../types"
@@ -135,6 +133,25 @@ async function releaseReservationSafely(
}
}
async function markLiveDeliveryPending(
teamRunId: string,
recipientName: string,
messageId: string,
config: TeamModeConfig,
): Promise<void> {
await transitionRuntimeState(teamRunId, (currentRuntimeState) => ({
...currentRuntimeState,
members: currentRuntimeState.members.map((member) => (
member.name === recipientName
? {
...member,
pendingInjectedMessageIds: Array.from(new Set([...member.pendingInjectedMessageIds, messageId])),
}
: member
)),
}), config)
}
async function deliverLive(
client: LiveDeliveryClient,
message: Message,
@@ -207,8 +224,8 @@ async function deliverLive(
})
continue
}
await commitDeliveryReservation(reservation)
log("[team-mailbox] live delivery committed", {
await markLiveDeliveryPending(teamRunId, recipientName, message.messageId, config)
log("[team-mailbox] live delivery reserved until recipient idle", {
teamRunId,
recipient: recipientName,
recipientSessionId,