Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/daemon/src/session/session-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -817,8 +817,17 @@ export class SessionManager {
// reach humans, post at a channel root, or reply into a parent session. It has no
// visible in-thread form: speaking in the current conversation is an ordinary reply.
// `toAgent` without a `channel` is the postless, channel-invisible wake.
// The precedence bullet leads for a measured reason (issue #800): on the Claude
// Code runtime the session also carries the runtime's own built-in `SendMessage`
// (agent-teams messaging) — a literal name match for a report-back instruction —
// and a child that picks it loses its parent report silently. Costs ~80 standing
// tokens per session.
const collabAppend =
`# Collaborating with other agents\n` +
`- AgentConnect's tools (the \`agentconnect\` MCP server, e.g. \`mcp__agentconnect__sendMessage\`) are the ` +
`ONLY channel that reaches other agents and humans here. Your runtime may offer built-in tools with similar ` +
`names (e.g. a bare \`SendMessage\`) — those do NOT reach AgentConnect and anything sent through them is ` +
`lost. Never use them for messaging, reporting back, or collaboration.\n` +
`- To reach a specific agent privately, call \`sendMessage\` with ` +
`\`{"toAgent":"<agent id>","message":"..."}\` — it wakes ONLY that agent, delivered directly to it ` +
`(nothing is posted to the channel). That bare form is FIRE-AND-FORGET: the peer answers inside its own ` +
Expand Down
24 changes: 24 additions & 0 deletions packages/daemon/test/session-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1934,6 +1934,30 @@ describe('SessionManager — collaboration preamble', () => {
store.close()
})

it('leads the guidance with the tool-precedence rule naming the built-in SendMessage hazard (issue #800)', async () => {
// Measured (tool-surface A/B, 2026-08-09): a Claude Code child session also
// carries the runtime's own built-in `SendMessage` — a literal name match
// for a report-back instruction — and a child that picks it loses its
// parent report silently. The standing context must say, before anything
// else about collaboration, that AgentConnect's MCP tools are the only
// channel that reaches anyone and that similarly-named built-ins are lost.
const store = newStore()
const host = { newSession: vi.fn(async () => 'acp-1') } as any
const sm = new SessionManager({ store, hostFor: async () => host, agentById: () => agent, memory })
const { blocks } = await sm.handle('bot-a', msg({ ts: '100.1', text: 'hi' }))
const first = (blocks[0] as any).text as string
expect(first).toContain('ONLY channel that reaches other agents and humans')
expect(first).toContain('mcp__agentconnect__sendMessage')
expect(first).toContain('a bare `SendMessage`')
expect(first).toContain('do NOT reach AgentConnect')
expect(first).toContain('Never use them for messaging, reporting back, or collaboration')
// It LEADS the collaboration section: the collision fires exactly when the
// model is choosing a messaging tool, so the warning must come first.
const section = first.slice(first.indexOf('# Collaborating with other agents'))
expect(section.indexOf('ONLY channel that reaches')).toBeLessThan(section.indexOf('To reach a specific agent'))
store.close()
})

it('states the needsReply rule in the standing context, not only in the tool descriptor', async () => {
// The descriptor is one input among many; this context is ALWAYS present and used to
// present the bare `toAgent` form as the normal way to reach a peer privately, with no
Expand Down