Make @mentions in a DM redirect the turn (group-chat semantics)

The operator's mental model: a chat is a room, @X means "X takes the
turn", unmentioned participants keep working. Two separate gaps were
letting 张工程师 answer when the user wrote "@CEO 更新一下进度" inside
their Board↔张工程师 chat:

* `resolveMessageWakeTargets` always woke the DM recipient, regardless
  of who the body @-mentioned. CEO woke too, but 张工程师 — the
  addressed party — replied first and CEO's busy-queued wake arrived
  after the operator had already taken the earlier reply as "the
  answer". Explicit mentions now shadow the implicit DM recipient:
  with mentions, only the named agents wake; the recipient still sees
  the message in their inbox, they just aren't obliged to answer.
  No mentions → fall back to the historical "DM wakes the recipient"
  contract so every existing flow is untouched.

* `channel-utils` keyed each chat by `sort(fromAgent, toAgent)`. CEO's
  reply with `toAgentId=__board__, parentMessageId=<original>` landed
  in a fresh Board↔CEO pane — the exact "new session" the operator
  complained about. Walk `parentMessageId` back to the root of the
  reply chain and use the root's participant pair: every reply in a
  thread now collapses into the chat the sender originally opened,
  cycle-guarded against malformed parent pointers.

Pinned by a new unit test: DM to CTO with "@CEO 更新一下进度。" wakes
only CEO while keeping the message visible to both CTO's and CEO's
inbox.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-20 08:59:14 -04:00
1 parent 7833ded4a1
commit af72f1b4d9
3 files changed
+99 -11

No files matched your search

+13 -2
View File
@@ -237,9 +237,21 @@ export function resolveMessageWakeTargets(
mentionedAgentIds?: readonly string[],
): string[] {
const wakeTargets = new Set<string>();
const mentioned = mentionedAgentIds ?? resolveMentionedAgentIds(db, input);
const hasExplicitMentions = mentioned.length > 0;
if (input.channel === "direct" && input.toAgentId && input.toAgentId !== input.fromAgentId) {
wakeTargets.add(input.toAgentId);
// Chatroom semantics: when the body carries explicit @mentions, the
// mention list is the recipient list. The DM's `toAgentId` is only
// woken if the sender named them; otherwise the named agent takes
// over the turn and the DM partner "keeps working" — exactly the
// behaviour the operator asks for when they say "@CEO 更新一下进度"
// inside a chat with 张工程师 and expects CEO (not 张工程师) to
// answer. With no mentions, preserve the historical "DM always
// wakes the recipient" contract so every existing flow is intact.
if (!hasExplicitMentions || mentioned.includes(input.toAgentId)) {
wakeTargets.add(input.toAgentId);
}
}
if (input.channel === "department" && input.channelTargetId) {
@@ -270,7 +282,6 @@ export function resolveMessageWakeTargets(
}
}
const mentioned = mentionedAgentIds ?? resolveMentionedAgentIds(db, input);
for (const mentionedId of mentioned) {
wakeTargets.add(mentionedId);
}