diff --git a/src/main/services/agent-read-messages.ts b/src/main/services/agent-read-messages.ts index fc929d65..95f5af92 100644 --- a/src/main/services/agent-read-messages.ts +++ b/src/main/services/agent-read-messages.ts @@ -3,6 +3,12 @@ import { optionalString } from "./agent-tool-args"; import type { AgentToolContext, AgentToolDeps } from "./agent-tools"; import type { JsonRecord } from "./provider-http"; +interface Sender { + sender_type: string; + sender_name: string; + sender_role: string | null; +} + interface MessageFilter { before: { createdAt: string; rowid: number } | null; query: string | null; @@ -59,11 +65,12 @@ function messagePage( label: string, ): string { const rows = deps.db.prepare(` - select m.id, m.rowid as row_id, m.body_md, m.created_at, m.thread_id, - coalesce(a.name, e.display_name, m.sender_type) as sender_name + select m.id, m.rowid as row_id, m.body_md, m.created_at, m.thread_id, m.sender_type, + coalesce(a.name, e.display_name, m.sender_type) as sender_name, + coalesce(a.role, e.role) as sender_role from chat_messages m left join agents a on a.id = m.sender_id and m.sender_type = 'agent' - left join employees e on e.id = m.sender_id and m.sender_type = 'employee' + left join employees e on e.id = m.sender_id and m.sender_type in ('employee', 'board') where m.${scope.column} = ? and m.org_id = ? and m.deleted_at is null and (? is null or m.created_at < ? or (m.created_at = ? and m.rowid < ?)) and (? is null or instr(lower(m.body_md), lower(?)) > 0) @@ -73,24 +80,25 @@ function messagePage( scope.id, orgId, filter.before?.createdAt ?? null, filter.before?.createdAt ?? null, filter.before?.createdAt ?? null, filter.before?.rowid ?? null, filter.query, filter.query, filter.limit + 1, - ) as Array<{ id: string; row_id: number; body_md: string; created_at: string; thread_id: string; sender_name: string }>; + ) as unknown as Array<{ id: string; row_id: number; body_md: string; created_at: string; thread_id: string } & Sender>; if (rows.length === 0) return `No messages${filter.query ? ` matching "${filter.query}"` : ""}${filter.before ? " before that point" : ""} in ${label}.`; const page = rows.slice(0, filter.limit).reverse(); - const lines = page.map((m) => `[${m.created_at}] ${m.sender_name} (thread=${m.thread_id}): ${m.body_md}${attachmentNote(deps.db, m.id)}`); + const lines = page.map((m) => `[${m.created_at}] ${senderLabel(m)} (thread=${m.thread_id}): ${m.body_md}${attachmentNote(deps.db, m.id)}`); if (rows.length > filter.limit) lines.unshift(`(Older messages exist: pass before="${page[0].id}" to read them.)`); return lines.join("\n"); } function unansweredMentions(deps: AgentToolDeps, ctx: AgentToolContext, limit: number): string { const rows = deps.db.prepare(` - select m.id, m.body_md, + select m.id, m.body_md, m.sender_type, coalesce(a.name, e.display_name, m.sender_type) as sender_name, + coalesce(a.role, e.role) as sender_role, m.thread_id, s.name as channel_name, s.space_type from chat_mentions cm join chat_messages m on m.id = cm.message_id join chat_spaces s on s.id = m.space_id left join agents a on a.id = m.sender_id and m.sender_type = 'agent' - left join employees e on e.id = m.sender_id and m.sender_type = 'employee' + left join employees e on e.id = m.sender_id and m.sender_type in ('employee', 'board') where cm.target_type = 'agent' and cm.target_id = ? and m.org_id = ? and m.deleted_at is null and not exists ( select 1 from chat_messages reply @@ -99,11 +107,11 @@ function unansweredMentions(deps: AgentToolDeps, ctx: AgentToolContext, limit: n ) order by m.created_at desc limit ? - `).all(ctx.agent.id, ctx.orgId, limit) as Array<{ id: string; body_md: string; sender_name: string; thread_id: string; channel_name: string; space_type: string }>; + `).all(ctx.agent.id, ctx.orgId, limit) as unknown as Array<{ id: string; body_md: string; thread_id: string; channel_name: string; space_type: string } & Sender>; if (rows.length === 0) return "No unanswered mentions."; return rows.reverse().map((m) => { const where = m.space_type === "direct" ? "in your direct conversation" : `in #${m.channel_name}`; - return `[${m.sender_name} ${where}] (thread=${m.thread_id}): ${m.body_md}${attachmentNote(deps.db, m.id)}`; + return `[${senderLabel(m)} ${where}] (thread=${m.thread_id}): ${m.body_md}${attachmentNote(deps.db, m.id)}`; }).join("\n"); } @@ -124,3 +132,7 @@ function beforeCursor(deps: AgentToolDeps, orgId: string, text: string | null): } return { createdAt: normalized, rowid: -1 }; } + +function senderLabel(m: Sender): string { + return m.sender_role ? `${m.sender_name} (${m.sender_type}, ${m.sender_role})` : `${m.sender_name} (${m.sender_type})`; +} diff --git a/src/main/services/chat-wake-context.ts b/src/main/services/chat-wake-context.ts index fd865b77..36acbc36 100644 --- a/src/main/services/chat-wake-context.ts +++ b/src/main/services/chat-wake-context.ts @@ -10,7 +10,7 @@ const MESSAGE_SELECT = ` coalesce(a.role, e.role) as sender_role from chat_messages m left join agents a on a.id = m.sender_id and m.sender_type = 'agent' - left join employees e on e.id = m.sender_id and m.sender_type = 'employee'`; + left join employees e on e.id = m.sender_id and m.sender_type in ('employee', 'board')`; interface ThreadMessageRow { id: string; diff --git a/tests/integration/chat-context.test.ts b/tests/integration/chat-context.test.ts index 9e86c4a3..d353d80e 100644 --- a/tests/integration/chat-context.test.ts +++ b/tests/integration/chat-context.test.ts @@ -90,7 +90,9 @@ async function founderLine() { `).run(fileId, orgId, name, content.length, path, founderId, new Date().toISOString()); return fileId; }; - return { connection, orgId, ceoId, wakePrompts, wakeInputs, say, runWake, runManually, edit, remove, onWakeTurn, addFile }; + const sayAs = (senderType: "employee" | "board", senderId: string, bodyMd: string, threadId: string) => + chat.sendMessage({ orgId, spaceId, threadId, senderType, senderId, bodyMd, attachmentFileIds: [] }); + return { connection, orgId, ceoId, founderId, wakePrompts, wakeInputs, say, sayAs, runWake, runManually, edit, remove, onWakeTurn, addFile }; } describe("chat wake context", () => { @@ -287,4 +289,22 @@ describe("chat wake context", () => { expect(dispatchAgentTool(deps, ctx, { id: "c2", name: "read_attachment", arguments: { file_id: fileId } })).toContain("Ship the booking API by Friday."); h.connection.close(); }); + + test("each human sender keeps their own kind and role; nobody is labeled founder", async () => { + const h = await founderLine(); + h.connection.db.prepare("insert into employees(id, org_id, display_name, role, created_at) values ('emp_mia', ?, 'Mia', 'Contractor', ?)") + .run(h.orgId, new Date().toISOString()); + h.connection.db.prepare("insert into chat_memberships(space_id, principal_type, principal_id, role, mute_state) select space_id, 'employee', 'emp_mia', 'member', 'none' from chat_memberships where principal_id = ?") + .run(h.founderId); + const threadId = h.say("Kick off the audit.", null); + h.sayAs("employee", "emp_mia", "Can I skip the audit?", threadId); + h.sayAs("board", h.founderId, "No skipping.", threadId); + await h.runWake(); + const prompt = h.wakePrompts[0]; + expect(prompt).toMatch(/Yang \(employee, Board Chair\): Kick off the audit\./); + expect(prompt).toMatch(/Mia \(employee, Contractor\): Can I skip the audit\?/); + expect(prompt).toMatch(/Yang \(board, Board Chair\): No skipping\./); + expect(prompt.toLowerCase()).not.toContain("(founder)"); + h.connection.close(); + }); }); diff --git a/tests/unit/agent-direct-messages.test.ts b/tests/unit/agent-direct-messages.test.ts index 90ba5fc0..42aff882 100644 --- a/tests/unit/agent-direct-messages.test.ts +++ b/tests/unit/agent-direct-messages.test.ts @@ -153,6 +153,6 @@ describe("agent direct messages", () => { dispatchAgentTool(deps, h.ctx, call("send_message", { to: "Dana", body: "Morning - priorities for today are in the doc." })); const readBack = dispatchAgentTool(deps, h.ctx, call("read_messages", { with: "Dana" })); expect(readBack).toContain("priorities for today"); - expect(readBack).toMatch(/\] CEO \(thread=/); + expect(readBack).toMatch(/\] CEO \(agent, [^)]+\) \(thread=/); }); });