fix(api): route group follow-ups to the waiting member bot

Group threads/followUp always steered to members[0], so a follow-up
while a later member had waiting_input went to the wrong bot or started
a fresh run on the first member instead of continuing the ask.

Resolve the target bot like group sends: prefer waiting ask/takeover on
any member, then the newest steerable active run, then @mentions, then
the default first member. Extract followUpGroupThreadMessage and add
regression tests.

Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-09-22 06:03:59 +00:00 committed by Ingo Wagner
parent 44d1bc7277
commit f5265924ba

View File

@ -180,6 +180,7 @@ import {
shouldForwardPeerThreadEvent, shouldForwardPeerThreadEvent,
} from "./thread-message-pages.js"; } from "./thread-message-pages.js";
import { import {
followUpGroupThreadMessage,
reactToThreadMessage, reactToThreadMessage,
resolveThreadTarget, resolveThreadTarget,
sendThreadMessage, sendThreadMessage,
@ -1543,79 +1544,8 @@ export function createRouter(deps: RouterDeps) {
} }
return { ok: true as const }; return { ok: true as const };
} }
const committed = await deps.prisma.$transaction(async (tx) => { const committed = await followUpGroupThreadMessage(deps, context.actor, target, {
await lockOwnedGroup(tx, context.actor, target.groupId); text: input.text,
const group = await tx.chatGroup.findFirst({
where: {
id: target.groupId,
archivedAt: null,
thread: { id: target.threadId },
},
include: { members: { orderBy: { createdAt: "asc" } } },
});
const botId = group?.members[0]?.botId;
if (!botId) throw new IsolationError();
const blocks = [{ kind: "text" as const, text: input.text }];
const message = await createThreadMessageInTransaction(tx, {
threadId: target.threadId,
role: "user",
blocks,
});
const active = await tx.run.findFirst({
where: {
threadId: target.threadId,
botId,
status: { in: [...ACTIVE_RUN_STATUSES] },
},
select: { id: true },
});
let run: { id: string } | null = null;
if (!active) {
const task = await tx.task.create({
data: {
spaceId: context.actor.spaceId,
botId,
threadId: target.threadId,
userId: context.actor.userId,
prompt: input.text,
status: "queued",
},
});
run = await tx.run.create({
data: {
spaceId: context.actor.spaceId,
botId,
threadId: target.threadId,
taskId: task.id,
userId: context.actor.userId,
status: "queued",
trigger: "follow_up",
sourceMessageId: message.id,
},
select: { id: true },
});
await tx.message.update({ where: { id: message.id }, data: { runId: run.id } });
} else {
await tx.steeringMessage.create({
data: {
messageId: message.id,
botId,
userId: context.actor.userId,
runId: active.id,
},
});
await tx.message.update({ where: { id: message.id }, data: { runId: active.id } });
}
const event = await appendEventInTransaction(tx, {
spaceId: context.actor.spaceId,
threadId: target.threadId,
botId,
type: "thread.message.created",
runId: run?.id ?? active?.id,
payload: { messageId: message.id, role: "user", blocks },
});
await touchGroupUpdatedAt(tx, target.groupId);
return { runId: run?.id, eventSeq: event.seq };
}); });
await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => { await deps.events.notify(target.threadId, committed.eventSeq).catch((error) => {
getLogger().error("group follow-up realtime notification", error); getLogger().error("group follow-up realtime notification", error);