Make @mentions visible to the agents they name
A DM addressed to one agent that @-mentions another was silently dropping the second agent out of the conversation: * `dispatchAgentMessage` added the mentioned agent to the wake set, so CEO's heartbeat fired — but * `listAgentInboxUnread` only surfaced messages where the current agent was `to_agent_id` or sat in a matching channel/project/ department. Mentions were invisible, so CEO's next turn saw nothing new and returned silently. From the operator's chair this looked exactly like "CEO ignored me; 张工程师 answered instead". * `findMentionedAgentIds` had a latent bug on top: its unique-alias filter used `ids.length === 1`, but a single agent whose name matched its role registered itself twice, tripped the filter, and every `@CEO` in a scaffold with role === name was a no-op. Once inbox visibility was fixed the bug became load-bearing. Root-cause fixes: * New migration v48 adds `agent_message_mentions(message_id, agent_id, company_id, created_at)` so mention fan-out is a first-class join. * `sendAgentMessage` now takes the resolved mention set and writes the row + mentions in a single transaction — a crash between the two writes can no longer leave a message visible to the recipient but invisible to the mentioned agent. * `dispatchAgentMessage` computes the mention set exactly once via `resolveMentionedAgentIds` and hands the same list to both the DB write and `resolveMessageWakeTargets`, so persistence and wake stay in lockstep. * `listAgentInboxUnread` and `listAgentMessages` gain an `id IN (select message_id from agent_message_mentions …)` clause so the mentioned agent's inbox actually contains the message that woke it. * `findMentionedAgentIds` now stores per-alias agent IDs in a `Set` and filters on `ids.size === 1`, dropping the self-duplication bug while preserving the "refuse ambiguous aliases" semantics. * Company teardown drops `agent_message_mentions` alongside the existing `agent_message_reads` cleanup so deletes stay clean. Tests pin the new behaviour: a DM with a `@CEO` mention reaches CEO's inbox and both agents get woken, while a DM whose recipient is `__board__` never attempts to wake the human sentinel.
This commit is contained in:
1 parent
e74dafcd36
commit
7833ded4a1
8 files changed
+231
-36
No files matched your search
@@ -166,6 +166,71 @@ describe("message dispatch", () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("routes a DM addressed to one agent into the @mentioned agent's inbox (group-chat @mention visibility)", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-message-mention-");
|
||||
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: "status",
|
||||
body: "Hi @CEO, please share a progress update, @CTO is on infra.",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("Expected dispatch to succeed.");
|
||||
expect(new Set(result.data.wakeTargets)).toEqual(new Set([ceoId, ctoId]));
|
||||
|
||||
const ceoInbox = db.listAgentInboxUnread(companyId, ceoId);
|
||||
expect(ceoInbox.map((entry) => entry.id)).toContain(result.data.id);
|
||||
const ctoInbox = db.listAgentInboxUnread(companyId, ctoId);
|
||||
expect(ctoInbox.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);
|
||||
db.init();
|
||||
|
||||
const { companyId, ceoId } = createScaffold(db);
|
||||
const wakeAgentIfPossible = vi.fn().mockReturnValue(null);
|
||||
|
||||
const result = dispatchAgentMessage({
|
||||
db,
|
||||
wakeAgentIfPossible,
|
||||
}, {
|
||||
companyId,
|
||||
fromAgentId: ceoId,
|
||||
channel: "direct",
|
||||
toAgentId: HUMAN_SENDER_ID,
|
||||
subject: "report",
|
||||
body: "Update ready for review.",
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw new Error("Expected dispatch to succeed.");
|
||||
expect(result.data.wakeTargets).not.toContain(HUMAN_SENDER_ID);
|
||||
expect(wakeAgentIfPossible).not.toHaveBeenCalledWith(
|
||||
HUMAN_SENDER_ID,
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
|
||||
db.close();
|
||||
});
|
||||
|
||||
it("resolves only user_reply wait states for company-wide human messages, but scoped replies resolve thread wait states", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-message-wait-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
|
||||
Reference in new issue
Block a user