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:
1 parent
7833ded4a1
commit
af72f1b4d9
3 files changed
+99
-11
No files matched your search
@@ -199,6 +199,45 @@ describe("message dispatch", () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("redirects a DM's wake to the @mentioned agent instead of the DM recipient (chatroom semantics)", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-message-redirect-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
|
||||
const { companyId, ceoId, ctoId } = createScaffold(db);
|
||||
const wakeAgentIfPossible = vi.fn().mockReturnValue(null);
|
||||
|
||||
const result = dispatchAgentMessage({
|
||||
db,
|
||||
wakeAgentIfPossible,
|
||||
}, {
|
||||
companyId,
|
||||
fromAgentId: HUMAN_SENDER_ID,
|
||||
fromHumanUser: true,
|
||||
channel: "direct",
|
||||
toAgentId: ctoId,
|
||||
subject: "progress",
|
||||
body: "@CEO 更新一下进度。",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("Expected dispatch to succeed.");
|
||||
// CEO is the only wake target — the DM recipient (CTO) is NOT
|
||||
// woken because an explicit @mention overrides the implicit DM
|
||||
// recipient. This is what lets the operator say "@CEO" inside a
|
||||
// chat with CTO and have CEO (not CTO) take the turn.
|
||||
expect(result.data.wakeTargets).toEqual([ceoId]);
|
||||
|
||||
// The message is still visible to CTO's inbox — they were the
|
||||
// addressed recipient — but CTO is not required to answer.
|
||||
const ctoInbox = db.listAgentInboxUnread(companyId, ctoId);
|
||||
expect(ctoInbox.map((entry) => entry.id)).toContain(result.data.id);
|
||||
const ceoInbox = db.listAgentInboxUnread(companyId, ceoId);
|
||||
expect(ceoInbox.map((entry) => entry.id)).toContain(result.data.id);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("keeps __board__ out of wake targets even when the recipient is the human reader", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-message-board-wake-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
|
||||
Reference in new issue
Block a user