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:
ZacharyZhang-NY committed 2026-04-19 01:21:26 -04:00
1 parent 0a40c55b3c
commit d47e637a45
5 files changed
+28 -7

No files matched your search

+1
View File
@@ -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); }
+17 -2
View File
@@ -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.
+5 -1
View File
@@ -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" \
+3 -3
View File
@@ -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,