From dbeba5fab6ed57a83d716b2bcf89bcd88676763f Mon Sep 17 00:00:00 2001 From: Poytr1 Date: Wed, 12 Aug 2026 10:10:13 +0800 Subject: [PATCH] fix(daemon): scope the #800 SendMessage-collision fix to the parent-report directive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime's built-in `SendMessage` is a literal name match for a report-back instruction, and a child that picks it loses its parent report silently (issue #800). The session-wide precedence bullet that first fixed this (#801) regressed ordinary in-thread play and was reverted (#861) — the lesson being that standing-context changes carry an unbounded blast radius. This lands the fix at the niche injection point instead: the `needsParentReply` directive, which is injected exactly (and only) into the sessions where the collision can occur. In-thread conversations never see this text, so the #801 regression class is structurally impossible. Two sentences added to the directive: - use exactly `mcp__agentconnect__sendMessage` by its full name — the similarly-named runtime built-in (a bare `SendMessage`) does NOT reach AgentConnect and anything sent through it is lost; - the parent session IS the delegating agent and this one call is the complete delivery — no redundant direct `toAgent` wake or DM (the route-shotgunning observed in 6/10 measured parent-report trials). Pinned by session-manager.test.ts. Validated per the prompt-change gate (collaboration-arena-baseline.md §4.2) against BOTH gate scenarios; the measured tables are in the PR body. Co-Authored-By: Claude Fable 5 --- packages/daemon/src/session/session-manager.ts | 16 +++++++++++++++- packages/daemon/test/session-manager.test.ts | 10 ++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) 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()