From 905473d98518abdfe16b5c3a4905a378d4aac111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 19 Apr 2026 19:08:41 -0400 Subject: [PATCH] Unify message read state on agent_message_reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit listAgentInboxUnread's direct-channel branch filtered on agent_messages.read_at, a column no code path in the repo ever writes. Every DM was therefore reported unread on every wake — the standup kept re-surfacing the same message, agents either looped on duplicate replies or devolved into posting status comments instead of real replies. Route every channel through agent_message_reads. The legacy read_at column stays in the schema as a wire-shape placeholder but is explicitly documented as unused; every surface that returns readAt already derives it from the receipts table. Also delete the wake-reason instruction that taught agents to post a status comment when nothing needs attention — that line was the direct source of heartbeat-noise comments cluttering the activity feed. --- src/main/database/messages.ts | 18 ++++++++---------- src/main/prompt-builder/wake-reasons/en.ts | 2 +- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/database/messages.ts b/src/main/database/messages.ts index 785209a6..a3df9bb0 100644 --- a/src/main/database/messages.ts +++ b/src/main/database/messages.ts @@ -264,10 +264,13 @@ export function buildMessageMethods( * visibility rules as listAgentMessages (direct/company/incident always, * department when agent belongs, project when assigned/lead). * - * "Unread" means: - * - Direct messages to this agent: `agent_messages.read_at IS NULL`. - * - Channel messages: no matching row in `agent_message_reads` for - * (message_id, agent_id). + * "Unread" is defined uniformly across every channel: no row in + * `agent_message_reads` for (message_id, agent_id). The legacy + * `agent_messages.read_at` column is never written anywhere in the + * codebase and must not be used as a read signal — it only appears in + * SELECT lists for wire-shape compatibility and is overwritten from the + * read-receipts table by every caller that surfaces `readAt` to agents + * or the renderer. * * Messages where `from_agent_id = agentId` are excluded (agents don't * inject their own sent messages back into their transcript). @@ -331,12 +334,7 @@ export function buildMessageMethods( ).map((row) => row.messageId), ); - return rows.filter((message) => { - if (message.channel === "direct" && message.toAgentId === agentId) { - return !message.readAt; - } - return !readIds.has(message.id); - }); + return rows.filter((message) => !readIds.has(message.id)); }, }; diff --git a/src/main/prompt-builder/wake-reasons/en.ts b/src/main/prompt-builder/wake-reasons/en.ts index 1e0103fb..9eaaee7d 100644 --- a/src/main/prompt-builder/wake-reasons/en.ts +++ b/src/main/prompt-builder/wake-reasons/en.ts @@ -28,7 +28,7 @@ export const WAKE_REASONS: Record = { "Call GET /api/agents/me/standup to get your full briefing", "Review all your assigned tasks and their statuses", "Take action on the most important item: reviews > unblocking > new work", - "If nothing needs attention, post a brief status update on your current task", + "If nothing actionable is in your briefing, return control without posting a status comment — activity noise is worse than silence.", ], }, manual: {