fix(work): a claimed bench card's solve happens IN THE CARD'S ROOM + typed dispatch struct (#425 slice 1) - #2336
Merged
Conversation
…nd the dispatch takes a TYPED struct (#425) MEASURED FIRST, because the doctrine doc's premise is wrong. Every workspace trace record carries `room_id`; counting nil vs real across the 25 newest trace files: **13,209 roomless turns vs 138,088 roomed (8.7%)**. Per citizen: 90e758b2 8,657 nil / 15,814 roomed — 35% of her cognition happened where no room could see it; a20b3ada 895 nil / 489 roomed, more roomless than roomed. WHAT THE DOC OVERSTATES: "a detached agent/solve produces no turns, so none of it reaches the curriculum." It DOES produce turns. `build_workspace_cycle` attaches a `JsonlWorkspaceCaptureSink` for every cycle — live turn, eval fork and solve alike (persona_workspace.rs:551-564); 1,258 trace files, 6.7 GB on disk. #427 is the corroboration and I had already accepted it: its whole subject was benchmark forks writing to the SAME traces file as live turns. You cannot contaminate a corpus you do not write to. SO THE DEFECT IS NARROWER AND STILL SERIOUS: those turns are invisible to the ROOM. No act receipts radiate (`apply_act` skips a nil room BY DESIGN), so no peer, no human and no ViewState ever sees the work — a citizenship defect, not a curriculum-input one. THE ROOM WAS NEVER UNKNOWN. Boards are PER-ROOM, so a card's activity is the room whose board holds it — and the wrong-room claim retry directly above already resolved exactly that, inline. Extracted as `room_holding_card`: ONE resolver, two consumers, replacing a scan in one place and a shrug in the other. `work/claim` now resolves the card's room and dispatches into it. A card no subscribed board holds gets NO detached fallback — the claim stands and a `work.claim.unplaceable_card` probe says why, because inventing invisible work is the thing being removed. THE SIGNATURE, on Joel's two calls today. "Make sure params are well formed structs and constants, good OOP, not random parameter lists." The old one was `(ctx, airc, Uuid, WorkCardId, Option<Uuid>)` — two BARE UUIDs separated only by argument POSITION, so transposing claimer and room compiled cleanly and would have dispatched a citizen's solve under a room's id. Now `StagedSolveDispatch { claimer: PeerId, card: WorkCardId, room: RoomId }`: typed fields make the transposition a compile error, and the next fact this dispatch needs is a FIELD, not a sixth argument. `ctx` + `airc` stay separate parameters deliberately — ambient services, not facts about the dispatch; folding them in turns a value object into a context bag. "If something is required, remove the option… caught at compile time not runtime." `room` was `Option<Uuid>` and `work/claim` passed `None` — the whole defect, expressed as a type that permitted it. It is now `RoomId`, so the roomless dispatch is UNREPRESENTABLE: a caller that cannot name the activity cannot construct the struct. The fallible part is the LOOKUP, which is where fallibility belongs, and it is handled at the boundary before the struct exists. Adjacent smells named, not silently expanded: the dispatch roster still carries bare `(String, Uuid)` tuples (#396's subject) so benchmark.rs types the id AT this boundary; `AgentSolveParams::room` stays `Option` because `agent/solve` is also operator-invocable, and narrowing that param is its own slice. `cargo check -p continuum-core --features metal,accelerate` clean, zero errors. LIVE PROOF OWED and NOT claimed: no dispatched claim has run on this build yet. The falsifiable prediction is that a claim-fired solve's acts now radiate receipts into the card's room, and that the nil-room share of new traces falls. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply
enabled auto-merge (rebase)
August 19, 2026 13:56
joelteply
added a commit
that referenced
this pull request
Aug 19, 2026
…remainder)
`p.room.unwrap_or_else(Uuid::nil)`. That was the whole defect: a nil room makes `apply_act`
skip receipt radiation entirely, so the run executes normally, lands in NO transcript, produces
no room turn, and NOTHING anywhere says so. 13,209 turns went by in that state — 8.7% of all
turns, 35% for one citizen — before anyone measured it, because an invisible run and a visible
one produce identical logs.
A roomless run is still LEGITIMATE and this does not refuse it. A bare `agent/solve` with no
activity behind it has no room to radiate into, and inventing one puts receipts on a phantom —
live-proven 2026-08-12, it stole the single-room chat projection. The defect was never that the
branch exists. It is that the branch was taken in silence.
So `RunVisibility::{InRoom(Uuid), Invisible}` replaces the `unwrap_or_else`, with `resolve` as
the ONE place the param becomes a decision, `room_id()` for the act pipeline (nil is the shape
`apply_act` already keys its skip on), and `warning()` returning the sentence ONLY for the
invisible case — a visible run stays quiet, because a warning that fires on the happy path
trains everyone to ignore it. The call site emits `agent.solve.roomless` + a tracing::warn once,
at the point the branch is taken.
An EXPLICIT `Uuid::nil()` resolves to Invisible too, pinned by its own test: a caller passing nil
means what omitting it means, and treating them differently would reopen the silent branch
through the other door.
This is the same failure shape as the two fixed an hour ago (a fetch cap whose comment promised
it would never truncate while nothing enforced it; a projection that doubled memory without
saying so): the system takes a consequential branch and declines to mention it. An absence is
only measurable if something declares it.
Pairs with slice 1 (PR #2336) — a CLAIMED bench card now solves in the card's room, so the
dispatched path is visible by construction and this covers everything else.
3 tests, green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#425 slice 1. A claimed bench card's solve now happens in the card's own room, and the dispatch takes a typed struct instead of a positional list with two bare UUIDs.
Measured first, because the doctrine doc's premise is wrong
Every workspace-trace record carries
room_id. Counting nil vs real across the 25 newest trace files:Per citizen:
90e758b28,657 nil / 15,814 roomed — 35% of her cognition happened where no room could see it;fe4dac172,133/15,316;e5f4141d1,177/50,273;a20b3ada895/489 (more roomless than roomed).What
BENCHMARKS-ARE-ADAPTERS-NOT-A-RUNNER.mdoverstates: "a detachedagent/solveproduces no turns, so a citizen can burn 12 acts… and none of it reaches the curriculum." It DOES produce turns.build_workspace_cycleattaches aJsonlWorkspaceCaptureSinkfor every cycle — live turn, eval fork and solve alike (persona_workspace.rs:551-564); 1,258 trace files, 6.7 GB on disk right now. #427 is the corroboration and I had already accepted it: its whole subject was benchmark forks appending to the SAME traces file as live turns. You cannot contaminate a corpus you do not write to.So the defect is narrower and still serious: those turns are invisible to the ROOM. No act receipts radiate (
apply_actskips a nil room BY DESIGN), so no peer, no human and no ViewState ever sees the work. A citizenship defect, not a curriculum-input one — which is exactly what #425 is named for.The room was never unknown
Boards are PER-ROOM, so a card's activity is the room whose board holds it — and the wrong-room claim retry directly above already resolved exactly that, inline. Extracted as
room_holding_card: one resolver, two consumers, replacing a scan in one place and a shrug in the other.work/claimnow resolves the card's room and dispatches into it. A card no subscribed board holds gets no detached fallback — the claim stands and awork.claim.unplaceable_cardprobe says why, because inventing invisible work is the thing being removed.The signature, on two calls from today
The old signature was
(ctx, airc, Uuid, WorkCardId, Option<Uuid>)— two bare UUIDs separated only by argument POSITION, so transposing claimer and room compiled cleanly and would have dispatched a citizen's solve under a room's id. Now:Typed fields make that transposition a compile error, and the next fact this dispatch needs becomes a FIELD, not a sixth argument.
ctx+aircstay separate parameters deliberately — they are ambient services, not facts about the dispatch; folding them in turns a value object into a context bag.roomwasOption<Uuid>andwork/claimpassedNone— the whole defect, expressed as a type that permitted it. It is nowRoomId, so the roomless dispatch is unrepresentable: a caller that cannot name the activity cannot construct the struct. The fallible part is the LOOKUP, which is where fallibility belongs, and it is handled at the boundary before the struct exists.Adjacent smells named, not silently expanded
(String, Uuid)tuples (Add claude/snapshot — persistent memory across sessions #396's subject), sobenchmark.rstypes the id AT this boundary rather than reaching into the roster.AgentSolveParams::roomstaysOptionbecauseagent/solveis also operator-invocable; narrowing that param is its own slice, with a comment saying so at the call site.Validation
cargo check -p continuum-core --features metal,accelerateclean, zero errors.Live proof owed and NOT claimed: no dispatched claim has run on this build. The falsifiable prediction is that a claim-fired solve's acts now radiate receipts into the card's room, and that the nil-room share of new traces falls from the 8.7% measured above.
Still open on #425 (this is slice 1, not the card)
ExperienceRecord::from_lived_turnhas zero production callers tree-wide — the experience stream is fed byfrom_kanban_gradealone, so no lived turn (bench or chat) ever becomes an experience record. Wider than Gap analysis: 49 open issues, 20 closed #425.persona::recorder::record_turnis called only frompersona/response.rs(therespond()path);cognition/never calls the recorder, so a solve writes workspace traces but no persona-respond fixture.Co-Authored-By: Claude Opus 5 noreply@anthropic.com
🤖 Generated with Claude Code
https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo