fix(team-mode): gate member wake prompts
This commit is contained in:
@@ -282,6 +282,34 @@ describe("createTeamSendMessageTool", () => {
|
||||
expect(calls[0]?.directory).toBe(resolveBaseDir(fixture.config))
|
||||
})
|
||||
|
||||
test("#given recipient OpenCode session is busy #when team_send_message attempts live delivery #then it leaves the message unread without starting another reply", async () => {
|
||||
// given
|
||||
const fixture = await createTeamFixture()
|
||||
let promptCalls = 0
|
||||
const client = {
|
||||
session: {
|
||||
status: async () => ({ data: { [fixture.memberTwoSessionId]: { type: "busy" } } }),
|
||||
promptAsync: async () => {
|
||||
promptCalls += 1
|
||||
},
|
||||
},
|
||||
}
|
||||
const liveTool = createTeamSendMessageTool(fixture.config, client)
|
||||
|
||||
// when
|
||||
await liveTool.execute({
|
||||
teamRunId: fixture.teamRunId,
|
||||
to: "m2",
|
||||
body: "ping while busy",
|
||||
}, fixture.toolContext(fixture.memberOneSessionId))
|
||||
|
||||
// then
|
||||
expect(promptCalls).toBe(0)
|
||||
const unread = await listUnreadMessages(fixture.teamRunId, "m2", fixture.config)
|
||||
expect(unread).toHaveLength(1)
|
||||
expect(unread[0]?.body).toBe("ping while busy")
|
||||
})
|
||||
|
||||
test("live delivery pins the recipient's resolved subagent_type and model on promptAsync", async () => {
|
||||
// given
|
||||
const fixture = await createTeamFixture()
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
reserveMessageForDelivery,
|
||||
} from "../team-mailbox/reservation"
|
||||
import { BroadcastNotPermittedError, sendMessage } from "../team-mailbox/send"
|
||||
import { promptAsyncAfterSessionIdle } from "../../../hooks/shared/prompt-async-gate"
|
||||
|
||||
import type { Message } from "../types"
|
||||
import { MessageSchema } from "../types"
|
||||
@@ -33,6 +34,7 @@ export type LiveDeliveryClient = {
|
||||
}
|
||||
query?: { directory: string }
|
||||
}): Promise<unknown>
|
||||
status?: () => Promise<unknown>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,11 +182,31 @@ async function deliverLive(
|
||||
applyMemberSessionRouting(recipientSessionId, recipientMember)
|
||||
|
||||
try {
|
||||
await client.session.promptAsync({
|
||||
path: { id: recipientSessionId },
|
||||
body: buildMemberPromptBody(recipientMember, envelope),
|
||||
query: { directory: recipientMember.worktreePath ?? directory },
|
||||
const promptResult = await promptAsyncAfterSessionIdle({
|
||||
client,
|
||||
sessionID: recipientSessionId,
|
||||
source: "team-live-delivery",
|
||||
input: {
|
||||
path: { id: recipientSessionId },
|
||||
body: buildMemberPromptBody(recipientMember, envelope),
|
||||
query: { directory: recipientMember.worktreePath ?? directory },
|
||||
},
|
||||
})
|
||||
if (promptResult.status !== "dispatched") {
|
||||
log("[team-mailbox] live delivery skipped by promptAsync gate, falling back to inbox injection", {
|
||||
status: promptResult.status,
|
||||
teamRunId,
|
||||
recipient: recipientName,
|
||||
recipientSessionId,
|
||||
messageId: message.messageId,
|
||||
})
|
||||
await releaseReservationSafely(reservation, {
|
||||
teamRunId,
|
||||
recipient: recipientName,
|
||||
messageId: message.messageId,
|
||||
})
|
||||
continue
|
||||
}
|
||||
await commitDeliveryReservation(reservation)
|
||||
log("[team-mailbox] live delivery committed", {
|
||||
teamRunId,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "../../features/team-mode/member-session-routing"
|
||||
import { resolveSessionEventID } from "../../shared/event-session-id"
|
||||
import { log } from "../../shared/logger"
|
||||
import { shouldPromptAfterSessionIdle } from "../shared/session-idle-settle"
|
||||
import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate"
|
||||
|
||||
type PromptAsyncInput = {
|
||||
path: { id: string }
|
||||
@@ -100,23 +100,29 @@ export function createTeamIdleWakeHint(ctx: TeamIdleWakeHintContext, config: Tea
|
||||
}
|
||||
|
||||
applyMemberSessionRouting(sessionID, memberEntry)
|
||||
if (!(await shouldPromptAfterSessionIdle(ctx.client, sessionID, options?.idleSettleMs))) {
|
||||
log("team idle wake hint skipped because session is active", {
|
||||
event: "team-mode-idle-wake-hint-active-session",
|
||||
const promptResult = await promptAsyncAfterSessionIdle({
|
||||
client: ctx.client,
|
||||
sessionID,
|
||||
source: "team-idle-wake-hint",
|
||||
settleMs: options?.idleSettleMs,
|
||||
input: {
|
||||
path: { id: sessionID },
|
||||
body: buildMemberPromptBody(memberEntry, buildWakeHint(unreadMessages.length)),
|
||||
query: { directory: ctx.directory },
|
||||
},
|
||||
})
|
||||
if (promptResult.status !== "dispatched") {
|
||||
log("team idle wake hint skipped by promptAsync gate", {
|
||||
event: "team-mode-idle-wake-hint-gated",
|
||||
teamRunId: runtimeState.teamRunId,
|
||||
memberName: memberEntry.name,
|
||||
sessionID,
|
||||
unreadCount: unreadMessages.length,
|
||||
status: promptResult.status,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
await ctx.client.session.promptAsync({
|
||||
path: { id: sessionID },
|
||||
body: buildMemberPromptBody(memberEntry, buildWakeHint(unreadMessages.length)),
|
||||
query: { directory: ctx.directory },
|
||||
})
|
||||
|
||||
log("team idle wake hint sent", {
|
||||
event: "team-mode-idle-wake-hint",
|
||||
teamRunId: runtimeState.teamRunId,
|
||||
|
||||
Reference in New Issue
Block a user