From b3da6dbf8aaa89a051646094075ee98f635390fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 29 Apr 2026 20:25:18 -0400 Subject: [PATCH] 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 --- src/main/agent-runtime/inbox-projection.ts | 7 +++++-- src/main/api-server/routes-agents-standup.ts | 1 + src/main/prompt-builder/messages/operating-en.ts | 4 ++-- src/main/prompt-builder/wake-reasons/en.ts | 8 ++++---- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/main/agent-runtime/inbox-projection.ts b/src/main/agent-runtime/inbox-projection.ts index 48fbd543..6b1f9e4a 100644 --- a/src/main/agent-runtime/inbox-projection.ts +++ b/src/main/agent-runtime/inbox-projection.ts @@ -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); diff --git a/src/main/api-server/routes-agents-standup.ts b/src/main/api-server/routes-agents-standup.ts index f209cb0e..43f8155c 100644 --- a/src/main/api-server/routes-agents-standup.ts +++ b/src/main/api-server/routes-agents-standup.ts @@ -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, diff --git a/src/main/prompt-builder/messages/operating-en.ts b/src/main/prompt-builder/messages/operating-en.ts index 11e31cd2..c901c9d6 100644 --- a/src/main/prompt-builder/messages/operating-en.ts +++ b/src/main/prompt-builder/messages/operating-en.ts @@ -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 diff --git a/src/main/prompt-builder/wake-reasons/en.ts b/src/main/prompt-builder/wake-reasons/en.ts index 467f0452..5ef6d74e 100644 --- a/src/main/prompt-builder/wake-reasons/en.ts +++ b/src/main/prompt-builder/wake-reasons/en.ts @@ -123,13 +123,13 @@ export const WAKE_REASONS: Record = { ], }, 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", ],