fix: attribute agent messages by sender — user DMs no longer look like system notices#6
Merged
Merged
Conversation
…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>
There was a problem hiding this comment.
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()(withskipStoreto prevent double persistence). - Attribute
/agents/:id/sendand/agents/:id/interruptby caller (dashboard user vs MCP agent viaX-Agent-Id), and fix/messages/sendworker DM path to useskipStore. - Deduplicate
formatTs()intosrc/utils/time.tsand 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.
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-dashboardenvelope viabuildSteer.Bonus bug found on the same path: agent→agent DMs via
/messages/sendwere persisted twice — once by the route (correctly, asagent) and again insidesendToAgent(assystem).Fix
AgentManager.sendToAgent/interruptAgentaccept{ sender, skipStore }:[<ts>] [USER]\nsource: web-dashboard\n---\n<message>[<ts>] [AGENT <id>]\nsource: direct_message\n---\n<message>authorType/authorIdinstead of alwayssystem/agents/:id/send+/interruptattribute by caller: requests withX-Agent-Id(MCP tools) → agent sender; otherwise the dashboard user/messages/sendworker DM path passesskipStore(route already persisted the DM with reply context) — kills the duplicatesystemcopy; channel fan-out keeps storing but with correct agent attributionformatTsdeduplicated intoutils/time.ts(was copy-pasted in LeadManager and Orchestrator; AgentManager is the third consumer)Test plan
tsc --noEmitclean, ESLint clean on changed files🤖 Generated with Claude Code