fix(messaging): preserve conversation channel in agent replies

Agents always replied with channel="direct" regardless of the original
message's channel, creating new DM conversations instead of replying
in the existing group chat (department/company/project/incident).

Root cause: three layers of the system guided agents toward direct-only
replies:
1. Prompt instructions (wake-reasons, IC/manager operating instructions)
   hardcoded channel="direct" and toAgentId=fromAgentId for all replies
2. Standup API omitted toAgentId from unread messages, removing context
   about who the original message was addressed to
3. Inbox projection contained no reply-routing metadata, so the agent's
   LLM had no signal about which channel to respond in

Fixes:
- routes-agents-standup.ts: include toAgentId in unread message payload
- wake-reasons/en.ts: differentiate direct vs group channel reply format
- operating-en.ts: same channel-preserving guidance for IC and manager
- inbox-projection.ts: embed reply-to routing context in message header
  so the LLM sees channel + target alongside the message body
This commit is contained in:
ZacharyZhang-NY committed 2026-04-29 20:25:18 -04:00
1 parent 1e36259c5d
commit b3da6dbf8a
4 files changed
+12 -8

No files matched your search

+5 -2
View File
@@ -83,9 +83,12 @@ export function projectInboxMessageToPi(
): AgentMessage {
const senderLabel = resolveSenderLabel(record, directory);
const channelLabel = formatChannelLabel(record);
const replyContext = record.channel === "direct"
? `reply-to: channel=direct, toAgentId=${record.fromAgentId}`
: `reply-to: channel=${record.channel}, channelTargetId=${record.channelTargetId ?? ""}`;
const header = channelLabel
? `[Message from ${senderLabel} · ${channelLabel}]`
: `[Message from ${senderLabel}]`;
? `[Message from ${senderLabel} · ${channelLabel} | ${replyContext}]`
: `[Message from ${senderLabel} | ${replyContext}]`;
const subject = record.subject ? truncate(cleanText(record.subject), MAX_SUBJECT_LENGTH) : "";
const body = truncate(cleanText(record.body), MAX_MESSAGE_BODY_LENGTH);
@@ -89,6 +89,7 @@ export function registerAgentStandupRoute(server: AgentApiServer): void {
id: m.id,
from: senderNames.get(m.fromAgentId) ?? m.fromAgentId,
fromAgentId: m.fromAgentId,
toAgentId: m.toAgentId,
channel: m.channel,
channelTargetId: m.channelTargetId,
subject: m.subject,
@@ -39,7 +39,7 @@ You are a middle manager running a team within an automated company. You take re
## Priority 1: Standup & Team Assessment (EVERY wake — SOP-001)
1. GET /api/agents/me/standup — get your full briefing
2. **CHECK MESSAGES FIRST**: GET /api/companies/${companyId}/messages?unreadOnly=true&limit=20 — read and respond to ALL unread messages before doing anything else
2. **CHECK MESSAGES FIRST**: GET /api/companies/${companyId}/messages?unreadOnly=true&limit=20 — read and respond to ALL unread messages before doing anything else. Reply in the same channel as the original: for channel="direct" use toAgentId=fromAgentId; for group channels ("department", "company", "project", "incident") use the same channel and channelTargetId, do NOT set toAgentId. Always set parentMessageId to the original message's id
3. Check tasks with status "in_review" from your reports — these MUST be reviewed FIRST
4. Check for "blocked" tasks from your reports — second priority
5. Read new comments on your tasks since last run
@@ -119,7 +119,7 @@ You are an individual contributor — a skilled specialist in an automated compa
## Priority 1: Check for Feedback & New Assignments (EVERY wake — SOP-001)
1. GET /api/agents/me/standup — get your full briefing in ONE call. The response includes \`unreadMessages\`, \`recentDocuments\`, \`recentWorkProducts\`, and every other state you need to plan this turn.
2. **MESSAGES FIRST**: walk \`unreadMessages\` from the standup. For each row, post a real reply (POST /api/companies/${companyId}/messages with channel="direct", toAgentId set to that row's \`fromAgentId\`, parentMessageId set to that row's \`id\`) AND mark it read (POST /api/companies/${companyId}/messages/{messageId}/read). Status comments do not count as replies.
2. **MESSAGES FIRST**: walk \`unreadMessages\` from the standup. For each row, post a real reply that preserves the original conversation context: if channel="direct", use channel="direct" with toAgentId set to that row's \`fromAgentId\`; if channel is "department", "company", "project", or "incident", use the SAME channel and channelTargetId from that row (do NOT set toAgentId for group channels). Always set parentMessageId to that row's \`id\`. Mark it read (POST /api/companies/${companyId}/messages/{messageId}/read). Status comments do not count as replies.
3. Read ALL new comments on your assigned tasks (GET /api/issues/{issueId}/comments)
4. If your manager has set a task back to "in_progress": read their feedback and address it FIRST
5. If there are new comments with questions: respond promptly via POST /api/issues/{issueId}/comments
+4 -4
View File
@@ -123,13 +123,13 @@ export const WAKE_REASONS: Record<WakeReason, WakeReasonEntry> = {
],
},
message: {
title: "You received a direct message from another agent.",
title: "You received a message from another agent or the Board.",
steps: (v) => [
`GET /api/companies/${v.companyId}/messages?unreadOnly=true to read your new messages`,
"Read each message and understand what is being asked",
`Respond to each message using the exact sender and message IDs returned by the API:
- For direct messages, POST /api/companies/${v.companyId}/messages with channel="direct", toAgentId set to the original message's fromAgentId, and parentMessageId set to that message's id
- If the sender is Board, the toAgentId is "__board__"`,
`Respond to each message by preserving the original conversation context:
- For direct messages (channel="direct"): POST /api/companies/${v.companyId}/messages with channel="direct", toAgentId set to the original message's fromAgentId, and parentMessageId set to that message's id. If the sender is Board, set toAgentId to "__board__"
- For group channels (channel="department", "company", "project", or "incident"): POST /api/companies/${v.companyId}/messages with the SAME channel and channelTargetId as the original message, and parentMessageId set to that message's id. Do NOT set toAgentId for group channel replies`,
`Mark messages as read: POST /api/companies/${v.companyId}/messages/{messageId}/read`,
"If the message requires you to take action on a task, do so",
],