diff --git a/packages/daemon/src/session/session-manager.ts b/packages/daemon/src/session/session-manager.ts index c2664eaca..55cf20019 100644 --- a/packages/daemon/src/session/session-manager.ts +++ b/packages/daemon/src/session/session-manager.ts @@ -858,12 +858,26 @@ export class SessionManager { // guidance rather than in the delivered text (which the model may summarize away). Deliberately // scoped to a terminal report: nothing here asks for progress narration, which would turn every // delegated task into channel chatter. + // + // The full-name/built-in warning and the this-call-is-complete sentence live HERE, not in the + // standing collaboration guidance, on purpose (issue #800): on the Claude Code runtime the + // 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; children + // also tended to shotgun a redundant direct `toAgent` wake around the correct parent reply. + // Both hazards exist exactly (and only) in needsParentReply sessions, so the directive is the + // niche injection point: in-thread conversations never see this text, which is what makes the + // #801 regression class ("plain replies reach nobody" taught session-wide, reverted by #861) + // structurally impossible here. const parentReplyAppend = needsReplyToParent ? `# Reporting back to your parent session\n` + `Another session delegated this work to you and is waiting on the outcome. When you finish — or when you ` + `cannot finish — reply to it with ` + `\`sendMessage\` \`{"sessionId":"${effectiveOriginSessionId}","message":"..."}\`, saying whether you ` + - `succeeded or failed and what the result was (on failure, what went wrong). Send it exactly once, at the ` + + `succeeded or failed and what the result was (on failure, what went wrong). Use exactly this tool — ` + + `\`mcp__agentconnect__sendMessage\` — by its full name: your runtime may offer a similarly-named ` + + `built-in (a bare \`SendMessage\`) that does NOT reach AgentConnect, and anything sent through it is ` + + `lost. The parent session IS the agent that delegated this to you, and this one call is the complete ` + + `delivery — do not also message that agent directly (no \`toAgent\` wake, no DM). Send it exactly once, at the ` + `end; do not report progress along the way, and do not skip it because the task was small or unsuccessful. ` + `Your ordinary assistant response in this child session is not delivered to the parent. Do not write the ` + `result before or after the tool call; after the tool reports successful delivery, end your turn immediately ` + diff --git a/packages/daemon/test/session-manager.test.ts b/packages/daemon/test/session-manager.test.ts index 13d1652d8..46c406cf2 100644 --- a/packages/daemon/test/session-manager.test.ts +++ b/packages/daemon/test/session-manager.test.ts @@ -737,6 +737,16 @@ describe('SessionManager', () => { expect(metaArg).toContain('{"sessionId":"origin-sess-9","message":"..."}') expect(metaArg).toContain('after the tool reports successful delivery, end your turn immediately') expect(metaArg).toContain('without repeating the message') + // #800 collision warning, scoped to THIS directive (niche: only needsParentReply + // sessions carry it — never the session-wide guidance, per the #801/#861 lesson): + // the full tool name, and the runtime's similarly-named built-in reaches nothing. + expect(metaArg).toContain('`mcp__agentconnect__sendMessage` — by its full name') + expect(metaArg).toContain('a bare `SendMessage`') + expect(metaArg).toContain('anything sent through it is lost') + // Dual-identity clarification: the parent session IS the delegating agent, and the + // one sessionId call is the complete delivery — no redundant direct wake or DM. + expect(metaArg).toContain('this one call is the complete delivery') + expect(metaArg).toContain('do not also message that agent directly') // Persisted, so later turns and resumes keep it. expect(store.getSession(sessionKey('slack', 'C1', '100.1', 'bot-a'))?.needsParentReply).toBe(1) store.close()