fix(messages): company channel fans out to every active agent
"Company" channel fell through to `listTopLevelAgentIds`, so a broadcast on the most ambient-sounding channel in the product only reached agents with `reports_to IS NULL`. The label said company; the semantics said board-only. The rest of the org never knew a message existed. Add `listCompanyBroadcastAgentIds` — returns every non-terminated, non-paused, non-pending agent for the company — and wire it through the `company` case in `resolveMessageWakeTargets`. Board-only fan-outs are still reachable: the `incident` channel keeps the broader pingable set (paused-by-budget included) and direct DMs to top-level agents work as before. The prompt doc catches up to the runtime: company is explicitly framed as all-hands, with board-only guidance redirected to DMs or incidents. The message-dispatch unit test asserts the new fan-out.
This commit is contained in:
1 parent
0a40c55b3c
commit
d47e637a45
5 files changed
+28
-7
No files matched your search
@@ -253,6 +253,7 @@ export class AppDatabaseBase {
|
||||
countActiveRunsForAgent(agentId: string, companyId: string) { return this.agents.countActiveRunsForAgent(agentId, companyId); }
|
||||
listDepartmentAgentIds(companyId: string, department: string) { return this.agents.listDepartmentAgentIds(companyId, department); }
|
||||
listTopLevelAgentIds(companyId: string) { return this.agents.listTopLevelAgentIds(companyId); }
|
||||
listCompanyBroadcastAgentIds(companyId: string) { return this.agents.listCompanyBroadcastAgentIds(companyId); }
|
||||
listIncidentPingableAgentIds(companyId: string) { return this.agents.listIncidentPingableAgentIds(companyId); }
|
||||
findAgentName(agentId: string) { return this.agents.findAgentName(agentId); }
|
||||
listAgentNamesByIds(companyId: string, agentIds: readonly string[]) { return this.agents.listAgentNamesByIds(companyId, agentIds); }
|
||||
|
||||
@@ -286,8 +286,8 @@ export function buildAgentHierarchyMethods(
|
||||
|
||||
/**
|
||||
* Returns ids of non-terminated agents that have no manager (top of the
|
||||
* reporting tree). Used by `resolveMessageWakeTargets` for "company"
|
||||
* channel broadcast.
|
||||
* reporting tree). Used for board-only fan-outs where only top-level
|
||||
* leadership should be notified.
|
||||
*/
|
||||
listTopLevelAgentIds(companyId: string): string[] {
|
||||
return readAll<{ id: string }>(
|
||||
@@ -296,6 +296,21 @@ export function buildAgentHierarchyMethods(
|
||||
).map((row) => row.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns ids of every agent that is eligible to receive a routine
|
||||
* company-wide broadcast. Paused, pending-approval, and terminated
|
||||
* agents are excluded — they should not be woken by normal chatter;
|
||||
* only incidents reach the broader pingable set. Used by
|
||||
* `resolveMessageWakeTargets` for the "company" channel so the
|
||||
* semantic matches Slack (all-hands) rather than board-only.
|
||||
*/
|
||||
listCompanyBroadcastAgentIds(companyId: string): string[] {
|
||||
return readAll<{ id: string }>(
|
||||
"select id from agents where company_id = ? and status not in ('terminated', 'paused', 'pending_approval')",
|
||||
companyId,
|
||||
).map((row) => row.id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns ids of agents that can receive incident pings: everybody not
|
||||
* terminated or still pending approval.
|
||||
|
||||
@@ -231,7 +231,11 @@ export function resolveMessageWakeTargets(
|
||||
}
|
||||
|
||||
if (input.channel === "company") {
|
||||
for (const agentId of db.listTopLevelAgentIds(input.companyId)) {
|
||||
// Company channel is Slack-semantics: every active agent in the
|
||||
// workspace is a listener. Board-only fan-outs use the dedicated
|
||||
// `incident` channel; the previous top-level-only fan-out made the
|
||||
// "company" label a misnomer that silently dropped ICs.
|
||||
for (const agentId of db.listCompanyBroadcastAgentIds(input.companyId)) {
|
||||
if (agentId !== input.fromAgentId) wakeTargets.add(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ channelTargetId rules:
|
||||
- channel "incident": set channelTargetId to null
|
||||
Direct messages WAKE the recipient immediately. Use this when you need another agent to act.
|
||||
Department messages wake all members of that department.
|
||||
Company-wide messages wake the CEO. Incident messages are visible company-wide.
|
||||
Company-wide messages wake every active agent in the company — treat this channel like an all-hands broadcast, not a CEO inbox. For board-only announcements, DM the top-level agents directly or open an incident.
|
||||
Incident messages are visible company-wide and pull the broader pingable set (including paused-by-budget agents).
|
||||
Example — send urgent message to a peer:
|
||||
curl -s -X POST "$AGENT_COMPANY_API_URL/api/companies/{{companyId}}/messages" \
|
||||
-H "Authorization: Bearer $AGENT_COMPANY_API_KEY" \
|
||||
|
||||
@@ -125,7 +125,7 @@ function createScaffold(db: AppDatabase) {
|
||||
}
|
||||
|
||||
describe("message dispatch", () => {
|
||||
it("wakes top-level active agents for company messages and active agents for incident messages", async () => {
|
||||
it("wakes every active agent for company channel broadcasts (Slack-style all-hands)", async () => {
|
||||
const profileDir = await createTempDir("agentcompany-message-dispatch-");
|
||||
const db = new AppDatabase(profileDir);
|
||||
db.init();
|
||||
@@ -141,12 +141,12 @@ describe("message dispatch", () => {
|
||||
fromAgentId: HUMAN_SENDER_ID,
|
||||
fromHumanUser: true,
|
||||
channel: "company",
|
||||
body: "Board broadcast",
|
||||
body: "All-hands broadcast",
|
||||
});
|
||||
|
||||
expect(companyResult.ok).toBe(true);
|
||||
if (!companyResult.ok) throw new Error("Expected company message dispatch to succeed.");
|
||||
expect(companyResult.data.wakeTargets).toEqual([ceoId]);
|
||||
expect(new Set(companyResult.data.wakeTargets)).toEqual(new Set([ceoId, ctoId, cmoId]));
|
||||
|
||||
const incidentResult = dispatchAgentMessage({
|
||||
db,
|
||||
|
||||
Reference in new issue
Block a user