From e62172a98d145a583756450a6a4a26b6dcbb4267 Mon Sep 17 00:00:00 2001 From: Bartok9 <259807879+Bartok9@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:41:00 -0400 Subject: [PATCH] Hand a group reply on after the owner allows it to be shown A reply held for private-share permission was written into the transcript and then stopped. The same reply allowed immediately was handed to the Bot it named. Allowing it now relays the same way, including the chain stored on the waiting row. --- CHANGELOG.md | 4 ++ server/src/channels/group.ts | 7 +++ .../group-conversations.integration.test.ts | 52 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1062725c8..7725ef7fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged. ## Unreleased +### A group reply the owner allows still reaches the Bot it names + +A reply held until its owner allowed it to be shown in a group was written into the transcript and then stopped. The same reply allowed immediately was handed to the Bot it named. Allowing it now hands it on the same way. + **Before upgrading.** Four things change for an existing deployment: - Automatic Learning is on unless an administrator saved it off. It does nothing until a Learning container is assigned; see below. diff --git a/server/src/channels/group.ts b/server/src/channels/group.ts index 4ea6187a5..5529cafdc 100644 --- a/server/src/channels/group.ts +++ b/server/src/channels/group.ts @@ -1019,8 +1019,15 @@ export function createGroupConversations(deps: { messageId: row.id, agentIds: [agentId], text: held.text, + ...chainFrom(row.details), }; await deps.activity?.(shown, agentId, held.text, `group:${row.id}`); + // The same reply allowed immediately would have been handed to the Bots it names. + // Holding it for the owner's permission must not drop that handoff. + const bots = await orderedRoster(row.ownerUserId, row.channelId); + const bot = bots?.find((candidate) => candidate.id === agentId); + if (bots && bot) + await relay(shown, bot, row.id, held.text, bots, runIdFor(row.id)); return { replyText: held.text }; } try { diff --git a/server/tests/group-conversations.integration.test.ts b/server/tests/group-conversations.integration.test.ts index 7eba9fb16..02c875396 100644 --- a/server/tests/group-conversations.integration.test.ts +++ b/server/tests/group-conversations.integration.test.ts @@ -580,6 +580,58 @@ describe("group conversations in PostgreSQL", () => { text: "From your notes: the launch slipped.", }); }); + test("a reply held for the owner's permission still hands off to the Bot it names", async () => { + const owner = await person(); + const teammate = await person(); + const ada = await bot(owner, "Ada"); + const grace = await bot(owner, "Grace"); + let decision: "pending" | "approved" = "pending"; + const privateShare = createPrivateShareCheck({ + approvals: { + open: async () => + ({ id: `${prefix}-held-relay`, status: decision }) as never, + list: async () => [], + rules: async () => [], + }, + }); + const { conversations, audit } = service({ + replies: { + [ada]: () => "Over to you, @Grace.", + [grace]: () => "On it.", + }, + privateShare, + }); + const channel = await conversations.create(owner.id, { + agentIds: [ada, grace], + }); + const [row] = await database + .select({ email: users.email }) + .from(users) + .where(eq(users.id, teammate.id)); + await conversations.addMember(owner.id, channel.id, { email: row?.email }); + await conversations.send(owner.id, channel.id, { + id: `${prefix}-held-relay-send`, + text: "ask grace", + agentId: ada, + }); + await drain(conversations); + expect( + audit.filter((event) => event.eventType === "agent.handoff_offered"), + ).toHaveLength(0); + + decision = "approved"; + await conversations.continueWaiting(`${prefix}-held-relay`, async () => { + throw new Error("a held reply is shown, not re-run"); + }); + await drain(conversations); + expect( + audit.filter((event) => event.eventType === "agent.handoff_offered"), + ).toHaveLength(1); + const after = await conversations.list(owner.id, channel.id); + expect(after.messages.some((message) => message.text === "On it.")).toBe( + true, + ); + }); test("while a reply waits on the owner's permission, other members do not see it stream in", async () => { const owner = await person(); const teammate = await person();