test(evals): webchat leg of the game matrix — night-collection scenario + single-conversation Werewolf - #941
Conversation
…, 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>
…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>
… 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>
…onsumers Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…3 trials, 9/9 needsReply replies lost in prose) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ght-1 stall on one prose reply, 8/9 needsReply answered) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Requesting changes for one blocking eval-validity issue. The new night-collection scorer cannot distinguish the intended needsReply wake from ordinary webchat transcript fan-out: under the current #926 behavior, a child's report is committed as a conversation post and the fixture immediately fans a context copy back to every other seat, including the referee. A later unrelated wake can therefore expose the marker as a bracketed context row even if the reply's own queued wake was dropped. Because the scorer classifies any such sighting as coalesced, the scripted assertions—and the full-game accounting that absorbs the same context rows—can pass while the regression this PR is intended to gate still exists. Please correlate the report with evidence for its accepted/started parent wake (or otherwise instrument the coalesced queue path) instead of using generic transcript visibility as delivery proof.
I inspected the exact current head 1519db8f0b7ab2c683c48ab3ff925b6fc8970ac4 through read-only GitHub data because the local worktree was not at the trusted revision. At review time, all nine GitHub build/test jobs had passed; only the review check remained pending.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
| const deliveredPromptSightings = inputs.refereePrompts.filter((text) => delivered.test(text)).length | ||
| const contextRowSightings = inputs.refereePrompts.filter((text) => contextRow.test(text)).length | ||
| const mode: ReplyMode = | ||
| ownTurnStarts > 0 ? 'own-turn' : deliveredPromptSightings + contextRowSightings > 0 ? 'coalesced' : 'lost' |
There was a problem hiding this comment.
This produces a false coalesced result for an unrelated context refresh. WebchatArena.fanOut() sends every committed child report back to the referee as context, independently of whether that report's queued needsReply wake starts or is coalesced into a turn. If that wake is dropped but another child/public wake later refreshes the referee, contextRowSightings > 0 still makes this test pass. The ownTurnStarts <= 1 assertion does not close the gap. Please require correlation to the report's actual parent wake (including a positively identified coalesced wake), rather than treating any later transcript row as delivery evidence.
There was a problem hiding this comment.
Fixed in e0ec4b8 — you were right that transcript visibility was doing delivery's job. Verdicts now bind to the daemon's own wake-admission evidence (agentReplyWakeEvidence in evals/games/webchat-fixture.ts): a {sessionId} reply wake is discriminable in the evaluation events (turn.accepted with source:'agent' and a bare-UUID deliveryId turn-id suffix; continuation wakes carry postId#target ids, host turns are source:'user'). own-turn requires the delivered form in a turn started on an admitted reply wake; coalesced requires visible content and an unconsumed coalesced reply wake (turn.cancelled / coalesced_into_turn on an admitted id); visibility with no admitted wake — your masking case — now scores lost. The full-game accounting gets the same treatment: the runner reports replyWakesAccepted, and both CI games assert answered rows == admitted reply wakes, so a #926 context echo of a dropped wake cannot pass as an answer. Discriminator 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.
There was a problem hiding this comment.
Confirmed on e0ec4b83. The admission/lifecycle correlation closes the transcript-visibility false positive, and I’ve approved the revised head.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
…ce (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>
There was a problem hiding this comment.
Approved. The prior blocking issue is resolved at e0ec4b833becae65f166e60b1a6562345e413dd3: reply verdicts now require daemon-side wake-admission evidence, own-turn delivery is tied to the matching admitted wake's turn.started input, and coalesced delivery requires a matching coalesced_into_turn lifecycle event rather than transcript visibility alone. The full Werewolf results also cross-check answered calls against the number of admitted reply wakes, so a #926 context echo cannot independently satisfy the gate.
I inspected the exact synchronized revision through read-only GitHub data because Git cannot initialize in the provided sandbox. Build, Check, image, sandbox, and integration jobs were green at review time; Unit Test was still running. I found no remaining blocking correctness issue in the revision.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
… agent (#967) (#969) Every postless toAgent child of one caller shares the synthetic a2a:<caller> channel + the caller's thread, so all pairwise sessions of one caller land in ONE physical transcript thread — and the §8.5 catch-up and turn-context refresh, built for genuinely shared conversations, replayed SIBLINGS' private deliveries (role assignments, canaries included) and reports into each child's prompts. Measured in the webchat Werewolf real run (#941). The sessions are pairwise; the reads now are too. isSyntheticA2aChannel (cp-collab-routes, beside the coordinate minting) marks the synthetic channel; the store gains transcriptSince(Revision)ForAgent — the same sender/recipient/transcript_recipient delivery scope the console session views already use, factored into one shared predicate — and the three context seams (§8.5 catch-up in session-manager, the thread-context refresh, localInvalidatingEvents) route through the scoped reads exactly when the session sits on a synthetic a2a channel. Every a2a writer already stamps recipient, so a pair's own rows are unaffected. Ordinary shared conversations (channels, webchat rosters, co-hosted participants) are untouched. Pinned by packages/daemon/test/a2a-transcript-privacy.test.ts (red without the fix: the sibling's canary appears in the child's prompt) and, at game level, by the new canaryCrossVisibility=0 audit in the scripted webchat Werewolf gate (every player prompt scanned across conversation AND pairwise sessions). Closes #967. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What
The webchat leg of the collaboration game matrix — the live single-conversation Werewolf topology (one multi-agent webchat conversation; role delivery and night actions as postless
toAgent + needsReplycalls; referee-MEDIATED wolf coordination; a model-shaped referee), reproduced as evals in two stages. The referee is a scripted subject agent acting through the real tool surface (sendMessage/needsReplyover the MCP control socket) — never the trusteddeliverRefereeEventcontrol path — so eval composition == live composition except the referee's brain is deterministic.Stage 1 — the night-collection scenario
evals/games/night-collection.ts+evals/test/webchat-night-collection.test.ts(added toeval:collab:contracts): the host posts night-start; the referee's conversation session issues THREE concurrentneedsReplypostless calls (wolf-A propose / seer inspect / doctor protect) while players post public filler; wolf-A's proposal is relayed to wolf-B. Scripted cells pin, on currentmain:sendMessage {sessionId}) wakes the referee exactly once — as its own turn or coalesced into an in-flight turn whose input still carries it; both shapes distinguished and recorded (own-turn/coalesced), with a forced-coalesce cell that holds the referee's turn open while a reply lands;lostbecause that IS current main;Real-model variant (
webchat-night-collection-real.test.ts, env-gated; real local Claude Code children, scripted referee via a puppet ACP adapter running the same brain): 3 trials, 9/9 owed night replies LOST — every child explored (ListAgentsin all nine cases) and then answered its delegation as an ordinary assistant response (a correctly formatted line, delivered to nobody). Referee woken zero times; the wolf-B relay never fired. At/above the historical delegate-and-forward baseline (2/5–3/5), and exactly the live night-1 failure shape.Stage 2 — single-conversation webchat Werewolf
evals/games/webchat-werewolf{,-runner}.ts+evals/test/webchat-werewolf.test.ts(in the gate): the full game — role delivery + night actions private vianeedsReply, mediated night kill (propose → agree/counter), public sequential day + votes carried by the #906 continuation, a reactive human HOST answering the referee's "open night N" asks (which also keeps each round inside the webchat hop budget). Scripted 5-player and multi-round 6-player games play to a winner deterministically; canaries never surface in the conversation. Rules/roles/win logic shared with the Slack game viaevals/games/werewolf-rules.ts; the Slack-shaped Werewolf is untouched and keeps pinning the other composition.Real run (seed 1, 5 real players, scripted referee): an honest stall at night 1 —
awaiting protect→player-5— which is the expected pre-#905 baseline, with unusually rich evidence:needsReplycalls answered correctly, including the full mediated kill leg ("We kill player-4 tonight." → relay → "I agree.") and the seer's inspection;SendMessagethree times (the Claude Code's built-in SendMessage tool collides with the AgentConnect messaging tool and silently swallows parent reports #800 collision), recovered viaToolSearch— and still lost the night reply to prose in the same session;What the webchat composition surfaced that the Slack-shaped one hid
{sessionId}report into a conversation-origin parent is posted live into the conversation and fanned to the roster — the night kill statement is readable by every player (pinnedprivateReportsPostedPublicly > 0;canaryLeaks: 0still holds since canaries ride only the never-posted role calls).a2a:<caller>+ one thread, so a child's context refresh shows the referee's calls to its siblings (role assignments included) and their replies.{sessionId}reply carries no sender label in the delivered text — a referee holding several concurrentneedsReplycalls cannot attribute a direct reply wake from the text; coalesced replies (context rows) ARE attributable.All four are recorded in
docs/designs/collaboration-arena-baseline.md§3.5 (the new webchat leg of the game matrix, what each composition validates) with the explicit note that the stage-1 real loss rate is the pre-#905 baseline. PR #905 itself stays parked and untouched.Verification
pnpm eval:collab:contracts— 17 files / 119 tests (was 15/115)pnpm eval:parity23,pnpm eval:contracts46 — greenpnpm typecheck,pnpm lint, evalstsc— greenpackages/daemon(git diff origin/main -- packages/daemonis empty)Refs #905 (parked; this coverage is its gate), #906, #926, #800.
🤖 Generated with Claude Code