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))
|
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 () => {
|
test("live delivery pins the recipient's resolved subagent_type and model on promptAsync", async () => {
|
||||||
// given
|
// given
|
||||||
const fixture = await createTeamFixture()
|
const fixture = await createTeamFixture()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
reserveMessageForDelivery,
|
reserveMessageForDelivery,
|
||||||
} from "../team-mailbox/reservation"
|
} from "../team-mailbox/reservation"
|
||||||
import { BroadcastNotPermittedError, sendMessage } from "../team-mailbox/send"
|
import { BroadcastNotPermittedError, sendMessage } from "../team-mailbox/send"
|
||||||
|
import { promptAsyncAfterSessionIdle } from "../../../hooks/shared/prompt-async-gate"
|
||||||
|
|
||||||
import type { Message } from "../types"
|
import type { Message } from "../types"
|
||||||
import { MessageSchema } from "../types"
|
import { MessageSchema } from "../types"
|
||||||
@@ -33,6 +34,7 @@ export type LiveDeliveryClient = {
|
|||||||
}
|
}
|
||||||
query?: { directory: string }
|
query?: { directory: string }
|
||||||
}): Promise<unknown>
|
}): Promise<unknown>
|
||||||
|
status?: () => Promise<unknown>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,11 +182,31 @@ async function deliverLive(
|
|||||||
applyMemberSessionRouting(recipientSessionId, recipientMember)
|
applyMemberSessionRouting(recipientSessionId, recipientMember)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await client.session.promptAsync({
|
const promptResult = await promptAsyncAfterSessionIdle({
|
||||||
path: { id: recipientSessionId },
|
client,
|
||||||
body: buildMemberPromptBody(recipientMember, envelope),
|
sessionID: recipientSessionId,
|
||||||
query: { directory: recipientMember.worktreePath ?? directory },
|
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)
|
await commitDeliveryReservation(reservation)
|
||||||
log("[team-mailbox] live delivery committed", {
|
log("[team-mailbox] live delivery committed", {
|
||||||
teamRunId,
|
teamRunId,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
} from "../../features/team-mode/member-session-routing"
|
} from "../../features/team-mode/member-session-routing"
|
||||||
import { resolveSessionEventID } from "../../shared/event-session-id"
|
import { resolveSessionEventID } from "../../shared/event-session-id"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { shouldPromptAfterSessionIdle } from "../shared/session-idle-settle"
|
import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate"
|
||||||
|
|
||||||
type PromptAsyncInput = {
|
type PromptAsyncInput = {
|
||||||
path: { id: string }
|
path: { id: string }
|
||||||
@@ -100,23 +100,29 @@ export function createTeamIdleWakeHint(ctx: TeamIdleWakeHintContext, config: Tea
|
|||||||
}
|
}
|
||||||
|
|
||||||
applyMemberSessionRouting(sessionID, memberEntry)
|
applyMemberSessionRouting(sessionID, memberEntry)
|
||||||
if (!(await shouldPromptAfterSessionIdle(ctx.client, sessionID, options?.idleSettleMs))) {
|
const promptResult = await promptAsyncAfterSessionIdle({
|
||||||
log("team idle wake hint skipped because session is active", {
|
client: ctx.client,
|
||||||
event: "team-mode-idle-wake-hint-active-session",
|
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,
|
teamRunId: runtimeState.teamRunId,
|
||||||
memberName: memberEntry.name,
|
memberName: memberEntry.name,
|
||||||
sessionID,
|
sessionID,
|
||||||
unreadCount: unreadMessages.length,
|
unreadCount: unreadMessages.length,
|
||||||
|
status: promptResult.status,
|
||||||
})
|
})
|
||||||
return
|
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", {
|
log("team idle wake hint sent", {
|
||||||
event: "team-mode-idle-wake-hint",
|
event: "team-mode-idle-wake-hint",
|
||||||
teamRunId: runtimeState.teamRunId,
|
teamRunId: runtimeState.teamRunId,
|
||||||
|
|||||||
Reference in New Issue
Block a user