fix(daemon): scope the #800 SendMessage-collision fix to the parent-report directive - #905
fix(daemon): scope the #800 SendMessage-collision fix to the parent-report directive#905Poytr1 wants to merge 1 commit into
Conversation
…eport directive 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 <noreply@anthropic.com>
There was a problem hiding this comment.
Reviewed the exact PR revision dbeba5fab6ed57a83d716b2bcf89bcd88676763f.
The change correctly keeps the collision and complete-delivery guidance inside the needsParentReply report-back directive, so ordinary in-thread sessions do not receive the new standing instruction. The fully qualified mcp__agentconnect__sendMessage name matches the daemon’s injected AgentConnect MCP server, and the existing session-manager coverage now pins both the collision warning and the single-delivery clarification.
No blocking issues found. Note: I could not run the targeted test locally because this checkout has no installed Vitest/pnpm dependency cache; the GitHub Build and Check jobs for this revision are passing, with some broader jobs still in progress.
sent by review-bot (Codex · gpt-5.6-terra) · open in session
|
Parked by the user's decision, not abandoned: prompt changes are held until the cross-surface eval infrastructure (activation parity suite, in progress) can validate them on more than the Slack-shaped leg. This PR passed the current two-scenario gate (parent-session 5/5, in-thread 6/6), but those scenarios run on one surface — the incident this fix addresses surfaced on webchat, so it waits for the webchat leg. Keep open; do not merge. |
|
Acknowledged. The code review approval stands, but this PR should remain open and unmerged until the activation parity suite validates the webchat leg. sent by |
…io + single-conversation Werewolf (#941) * refactor(evals): extract shared werewolf rules (roles, action parsing, win) into werewolf-rules.ts Pure pieces only, behavior identical — the Slack-shaped game delegates to the shared module so the upcoming webchat composition cannot drift on the rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(evals): webchat night-collection scenario + single-conversation Werewolf (scripted referee on the real tool surface) Stage 1 — the night-collection scenario (evals/games/night-collection.ts, evals/test/webchat-night-collection.test.ts, in eval:collab:contracts): ONE multi-agent webchat conversation at the daemon seam; the referee's conversation session issues THREE concurrent needsReply postless calls while players post public filler. Pins, on current main: a child reply sent via sendMessage {sessionId} wakes the referee exactly once (own-turn or coalesced, both recorded — a coalesced wake's turn input still carries the reply); a child answering in PROSE is a LOST reply (the #905 validation cell, pinned lost); the referee-mediated wolf relay round-trips. The env-gated real-model variant (webchat-night-collection-real.test.ts) measures the loss rate with real local players and a scripted referee driven through the puppet ACP adapter (evals/games/puppet-acp-agent.mjs + puppet.ts) — the same brain the CI variant runs in-process, acting through the real MCP control socket. Stage 2 — webchat Werewolf (evals/games/webchat-werewolf{,-runner}.ts, evals/test/webchat-werewolf.test.ts, in eval:collab:contracts): the full game on the live single-conversation topology — public day speech and votes as ordinary conversation posts carried by the #906 continuation, role delivery and night actions as postless toAgent+needsReply calls, the night kill referee-MEDIATED (propose → agree/counter relay), a reactive human HOST pacing the phases. Scripted 5-player and multi-round 6-player games play to a winner deterministically; canaries never surface in the conversation. The Slack-shaped Werewolf is untouched and keeps pinning the other composition. An env-gated real run (webchat-werewolf-real.test.ts) reports a winner or an honest stall (stalledAt + unanswered needsReply rows) — with #905 parked, night reply loss is the expected pre-#905 baseline. Surfaced by the webchat composition and recorded in the doc: #926 posts a child's needsReply report into the conversation view (private night traffic is room-visible, pinned privateReportsPostedPublicly > 0); all of one caller's pairwise a2a sessions share one transcript thread (siblings see each other's private calls in context); a {sessionId} reply carries no sender label in the delivered text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): give the puppet referee seat managed memory (none needs a runtime-verified off-switch) The puppet ACP adapter is an unregistered runtime, so 'none' memory fails the static admission gate at host build; 'managed' is a no-op for the adapter and the evaluation capability profile keeps memory off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(evals): wire ROUND_LIMIT_PATTERN and NIGHT_ALIASES into their consumers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): record the measured stage-1 night-collection baseline (3 trials, 9/9 needsReply replies lost in prose) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(design): record the stage-2 webchat Werewolf real run (honest night-1 stall on one prose reply, 8/9 needsReply answered) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(evals): bind reply verdicts to the daemon's wake-admission evidence (review) The scorer classified any marker sighting as 'coalesced', but under #926 a child's report is also committed as a conversation post whose context copy fans back to the referee — a later unrelated wake could expose the marker even if the reply's own queued wake was dropped, masking exactly the regression this scenario gates. Verdicts now bind to agentReplyWakeEvidence: a reply wake is discriminable in the daemon's own events (turn.accepted with source:'agent' and a bare-UUID deliveryId suffix — continuation wakes carry 'postId#target' ids, host turns are source:'user', messageAgent calls use monotonic-ts ids). 'own-turn' needs the delivered form in a turn STARTED on an admitted reply wake; 'coalesced' needs visible content AND an unconsumed coalesced reply wake; anything else — including visibility with no admitted wake — is 'lost'. The werewolf runner reports replyWakesAccepted/Coalesced and the CI games assert answered rows == admitted reply wakes. Validated against the real-run artifacts: the stage-2 game shows exactly 8 accepted reply wakes for its 8 answered calls (0 coalesced); the all-lost night-collection trials show 0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Cross-surface validation on the webchat night-collection cell (the eval infra this PR was parked for — now on main via #941): pre-registered criteria NOT met. Leaving parked. Setup: current main + this PR's commit cherry-picked; 3 real trials of the webchat night-collection scenario (scripted referee via the real tool surface; three concurrent cold
Pre-registered target was ≥7/9 delivered. Directive delivery is confirmed, not assumed: the one correct child used the directive's exact injected Reading. The niche directive holds where it was originally measured (a warm session whose whole task is the delegation: 5/5 first-attempt in the 2026-08-12 runs) and its collision half works (0 built-in attempts here, vs 3 built-in attempts by one child in the full-game run without it). But it does not rescue the cold single-turn a2a child population: given a task with an answer-format instruction ("answer with a single line starting …"), the model answers the question as told and never reaches for any messaging tool — 7 of 9 children made zero messaging-tool calls. That failure is mechanism-shaped, not prompt-shaped: it needs #800's still-open mechanism work (inferred reply / adapter-level handling of a headless child's final answer), which no directive wording can substitute for. Corroborating cross-surface datum: the full webchat Werewolf real game (role-ack leg first, so each night call landed in a session with one successful report-back already in context) delivered 8/9 — session precedent mitigates what the directive alone does not. |
|
Agreed—leave this parked and do not merge. The cross-surface result shows the directive prevents the name collision but does not reliably produce cold-child report-back; that needs the mechanism-level work in #800. My earlier approval should not be treated as merge readiness. sent by |
…n ends without its report (#984) * feat(daemon): infer the parent reply when a needsReply delegation turn ends without its report (#800) The mechanism half the parked directive fix (#905) could not substitute for, measured on the webchat night-collection cell: a COLD needsReply child mostly answers its delegation as its ordinary assistant response — a correct answer delivered to nobody — and never reaches for any messaging tool (9/9 lost at baseline, 8/9 still lost with #905's directive). The pi-intercom pattern flips the delegate-and-forward red pin: a headless child's answer is never silently dropped. maybeInferParentReply runs at clean turn completion, while the turn's activeTurnCallMeta is still installed: if the turn's OWN trusted CallMeta carries needsReply + an origin and the obligation is still 'awaiting', the child's final ordinary output is delivered to the parent through the UNCHANGED replyToSession path (origin authorization, hop charge, queue/ coalesce, markChildParentReply), prefixed with an explicit '[inferred reply]' marker so the parent and the artifacts can always tell it from a real report. Empty / no-response finals become an explicit 'finished without reporting' wake instead of silence. Niche boundary: delegation turns only (human follow-ups, plain calls, continuations never infer); failed/suppressed turns keep their own semantics; sessions with live background tasks defer to the bg-task wake. Sanctioned expectation flip: the scripted night-collection prose-loss cell now pins 'delivered-inferred' (was 'lost' — that WAS current main's truth, and changing it is this fix's entire point), with the scorer distinguishing inferred deliveries by the marker inside an admitted reply wake. New unit pins in inferred-parent-reply.test.ts: prose answer delivered+marked; empty answer → explicit no-report wake; a real report is never doubled; a plain call never infers. Refs #800 (mechanism half; adapter disallowedTools finding noted there), #905 (stays parked — directive alone measured insufficient). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(daemon): close the settled-task race in the inferred-reply background guard (review) A task that just settled leaves lease.tasks before its wake timer fires, and that wake defers while the current dispatch finalizes — a tasks-only check saw zero and inferred the turn's narration while the bg-task wake was still owed. Guard on armedWakes too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Closing: superseded by the mechanism fix, and the evidence went against this approach. Measured on the webchat night-collection cell (the eval infrastructure this PR was parked waiting for): with this directive, 1/9 owed replies were delivered against a pre-registered target of >=7/9. 7 of 9 children made no messaging-tool call at all, so the loss is mechanism-shaped and directive wording cannot close it. What did close it: #984 (inferred reply) — a child that ends its turn without reporting has its final output delivered to the parent, marked inferred. Across five real werewolf games afterwards, every owed obligation resolved except one misdelivery case (#998). #800 stays open for the remaining mechanism work (suppressing the runtime's built-in messaging tools for subject sessions). Reopen this if a scoped directive ever becomes useful alongside that. |
|
Closing makes sense. The cross-surface evidence rejected the prompt-only approach, while #984 addresses the actual failure mode. Keeping #800 open for built-in tool suppression and #998 for the remaining misdelivery cleanly separates the follow-up work. sent by |
What
The niche re-land of the #800 fix after #801's revert (#861): two sentences added to the
needsParentReplyreport-back directive insession-manager.ts— nothing added to any standing session-wide context.mcp__agentconnect__sendMessageby its full name; the runtime's similarly-named built-in (a bareSendMessage) does NOT reach AgentConnect and anything sent through it is lost.toAgentwake, no DM).The directive is injected exactly (and only) into sessions created with
needsParentReply— the population where the collision occurs. In-thread conversations never see it, so the #801 regression class ("plain replies reach nobody" taught session-wide) is structurally impossible. Diff:session-manager.ts+ its test pin, nothing else.Prompt-change gate (§4.2, both scenarios measured before opening this PR)
Pre-registered criteria, fixed before the runs: parent-session 5 trials/arm → built-in
SendMessageattempts 0 across all trials, zero lost replies, success ≥ 4/5 per arm; in-thread-count 3 trials/arm stays clean. Harness: PR #791 A/B apparatus + this change cherry-picked (measurement state preserved as branchclaude/ab-800-measurement); real local Claude Code over ACP (claude-agent-acp0.64.0,sonnet,permissionMode: default); artifacts under~/arena-runs/parent-report-fix-2026-08-12/on the measurement host.parent-session, new directive (2026-08-12)
sendMessage, the shipped surface)postfaçade)Historical: pre-#801 4/6 success, 3/6 built-in attempts, 0/6 first-attempt, 6/10 redundant double-sends; #801 (session-wide bullet) 10/10 success but 4/10 first-attempt — and a live in-thread regression.
in-thread-count, same tree
Honest verdict: pre-registered criteria NOT all met — do not merge yet
SendMessage— the Claude Code's built-in SendMessage tool collides with the AgentConnect messaging tool and silently swallows parent reports #800 hazard in a population this directive deliberately does not cover, invalid per the harness's pre-registered validity rule but a real finding); B-5's child answered only via redundant direct private posts and the parent never received the answer (a lost reply); success 2/3.Reading: the niche directive works where it is injected (arm A n=5 flawless), but this sample surfaced that the collision also fires at the caller side, which no needsParentReply directive can reach — that population needs the mechanism-level fixes still open on #800 (adapter-level built-in suppression, inferred reply). Arm B's child-side failures are façade-surface behavior worth its own look in #791.
Per the pre-registration: leaving this PR open, not merging. Full per-trial artifacts preserved; measurement tree on
claude/ab-800-measurement.Refs #800. Context: #801 (reverted by #861), gate scenario PR #791.
🤖 Generated with Claude Code