Skip to content

fix: attribute agent messages by sender — user DMs no longer look like system notices#6

Merged
justinchuby merged 2 commits into
mainfrom
fix/agent-message-attribution
Jun 14, 2026
Merged

fix: attribute agent messages by sender — user DMs no longer look like system notices#6
justinchuby merged 2 commits into
mainfrom
fix/agent-message-attribution

Conversation

@justinchuby

Copy link
Copy Markdown
Member

Problem

Messaging an agent from the Agents page delivered the raw text with no attribution, and persisted the DM as authorType: 'system'. Agents received user messages as if they were system notifications — the Lead, by contrast, already gets a proper [ts] [USER] / source: web-dashboard envelope via buildSteer.

Bonus bug found on the same path: agent→agent DMs via /messages/send were persisted twice — once by the route (correctly, as agent) and again inside sendToAgent (as system).

Fix

  • AgentManager.sendToAgent / interruptAgent accept { sender, skipStore }:
    • user sender → same envelope the Lead receives: [<ts>] [USER]\nsource: web-dashboard\n---\n<message>
    • agent sender → [<ts>] [AGENT <id>]\nsource: direct_message\n---\n<message>
    • system / no sender → unchanged passthrough (orchestrator callers unaffected)
    • the persisted DM now carries the real authorType/authorId instead of always system
  • /agents/:id/send + /interrupt attribute by caller: requests with X-Agent-Id (MCP tools) → agent sender; otherwise the dashboard user
  • /messages/send worker DM path passes skipStore (route already persisted the DM with reply context) — kills the duplicate system copy; channel fan-out keeps storing but with correct agent attribution
  • formatTs deduplicated into utils/time.ts (was copy-pasted in LeadManager and Orchestrator; AgentManager is the third consumer)

Test plan

  • 4 new attribution tests (user envelope + user-attributed DM, agent envelope, system passthrough, skipStore no-double-persist)
  • Updated route-level mocks for the new signature
  • Full server suite: 823/823 passing, tsc --noEmit clean, ESLint clean on changed files

🤖 Generated with Claude Code

…e system notices

Messaging an agent from the Agents page delivered the bare text with no
attribution, and persisted the DM as authorType 'system' — agents treated
user messages as system notifications instead of a user speaking to them
(the Lead already got a proper [USER] envelope via buildSteer).

- AgentManager.sendToAgent/interruptAgent accept { sender, skipStore }:
  user senders get the same envelope the Lead receives
  ([ts] [USER] / source: web-dashboard / --- / message), agent senders get
  [AGENT <id>]; system callers (orchestrator) are unchanged. The stored DM
  now carries the real authorType/authorId.
- /agents/:id/send + /interrupt attribute by caller: X-Agent-Id header
  (MCP tools) → agent sender; otherwise the dashboard user.
- /messages/send worker DM path passes the sender and skipStore — it
  already persisted the DM itself, so sendToAgent used to store a second
  copy attributed to 'system'. Channel fan-out keeps storing (it is the
  recipient's only record) but now with correct agent attribution.
- formatTs deduplicated into utils/time.ts (was copy-pasted in
  LeadManager and Orchestrator; AgentManager is the third consumer).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes DM sender attribution when steering agents so that user/agent-originated messages are no longer persisted and displayed as system notices, and it eliminates a duplicate DM persistence path in /messages/send. It also deduplicates timestamp formatting into a shared utility and adds tests around attribution + skipStore behavior.

Changes:

  • Add sender-aware envelopes + DM persistence attribution to AgentManager.sendToAgent() / interruptAgent() (with skipStore to prevent double persistence).
  • Attribute /agents/:id/send and /agents/:id/interrupt by caller (dashboard user vs MCP agent via X-Agent-Id), and fix /messages/send worker DM path to use skipStore.
  • Deduplicate formatTs() into src/utils/time.ts and update Lead/Orchestrator to use it; add/adjust tests for the new signature and behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/server/src/agents/AgentManager.ts Adds sender-aware envelope + DM persistence attribution and skipStore option.
packages/server/src/api/routes/agents.ts Attributes agent relay messages based on presence of X-Agent-Id.
packages/server/src/api/routes/messages.ts Uses skipStore on worker DM steering and fixes attribution for channel fan-out.
packages/server/src/utils/time.ts New shared timestamp formatter used by steer envelopes.
packages/server/src/orchestrator/Orchestrator.ts Removes local formatTs copy in favor of shared util.
packages/server/src/lead/LeadManager.ts Removes local formatTs copy in favor of shared util.
packages/server/tests/agents/agent-manager.test.ts Adds attribution + skipStore regression tests.
packages/server/tests/api/agent-relay.test.ts Updates mocks/assertions for new sender-aware signature.
packages/server/tests/api/dm-routing.test.ts Updates assertions for { sender, skipStore } options passed to sendToAgent.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/server/src/agents/AgentManager.ts Outdated
Comment thread packages/server/src/api/routes/agents.ts Outdated
Comment thread packages/server/src/api/routes/agents.ts Outdated
Comment thread packages/server/src/utils/time.ts
… formatter

Addresses Copilot review feedback on the agent message attribution change:

- MessageSender is now a discriminated union so { type: 'agent' } requires an
  id at compile time — prevents DMs persisted as agent with authorId: null
  and "[AGENT unknown]" envelopes.
- Normalize the X-Agent-Id request header (can be string[] when duplicated)
  to a single string before building the sender in /agents/:id/send and
  /interrupt.
- Cache the resolved timezone + Intl.DateTimeFormat at module scope in
  formatTs(); loadGlobalConfig() does sync filesystem reads and formatTs is a
  hot path (every user/agent DM envelope).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@justinchuby justinchuby merged commit 3986904 into main Jun 14, 2026
1 of 3 checks passed
@justinchuby justinchuby deleted the fix/agent-message-attribution branch June 14, 2026 15:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants