docs/adr/0229-room-handoffs-are-visible-parallel-threads.md

ADR 0229: Room handoffs are visible parallel threads

Status: accepted for engineering (James, 2026-10-05), including the Codex fallback decision. Extends ADR 0218. Work: VUH-1672.

Context

Voice ask_clankie work used one continuing room run. Different speakers waited for that run, and a new message could steer another person's work. Discord text shared the same execution problem. A native operator seat attached to Clankie's main conversation did not receive handoffs from other rooms.

Decision

Every admitted voice or Discord text handoff gets a durable conversation record under Clankie. The record retains the original room, delivery ID, authenticated actor ID, source, request, current work, state and result. Actor names are display text. The TUI dock and app expose these records and select the corresponding transcript. The existing room conversation remains the authority and reply destination; the child is an execution record, not a new grant or an ephemeral /btw fork.

A shared admission queue bounds concurrent work to four handoffs globally and two per room, with at most 32 waiting jobs. Excess requests get a clear busy result instead of an unbounded pending record. Live admitted jobs retain their records; inactive and abandoned records follow bounded conversation retention. Completed delivery retries return the saved result, including after restart. Separate handoffs use separate execution sessions and captures; another speaker cannot steer an active request. Exact delivery IDs keep existing receipt semantics. Different requests with identical wording remain different handoffs. An explicit voice join refers only to the same authenticated speaker's existing call in the same realtime conversation. Mouth playback retains its existing serialization and stale-result checks: parallel work does not mean overlapping speech. Durably admitted work may continue after its realtime voice conversation closes; the existing conversation, quiet and stay guards suppress late speech. Its child record remains available for inspection.

Execution follows the live head:

HeadRoom authorityExecutor
Service PiSocial or machineSeparate Pi room thread
Claude CodeSocial or machineNative Claude child with only the scoped room proxy tools
CodexVerified ownerNative Codex child
CodexAny non-ownerSeparate Pi thread under the original room lane and grant

Native requests use the existing parent seat channel. Clankie starts a real native subagent; the service verifies its parent, exact task marker and, for Claude, restricted agent type before admitting scoped proxy calls or displaying a native child reference. A queued request is not evidence that a native child exists. The parent receives only an opaque task payload and releases its turn after spawning. The verified child reads the original request from the scoped catalog; ambient room text does not enter the operator parent's dispatch instructions. The proxy captures its tool bank and original room authority on the host; model arguments cannot choose another actor or destination. Grants and recipient identity are checked again after waiting and before effects or completion.

The operator's builtin tools and MCP catalog are excluded from Claude's restricted room agent. An approval-shaped result keeps the existing authenticated-surface handoff. Ambient voice or text cannot approve privileged work, including by selecting a child transcript. Once a native handoff is taken or uncertain, it is never replayed as a Pi run.

Codex limitation and upstream request

Installed Codex 0.160.0 copies only a bounded set of role feature overrides. Its tests explicitly retain the parent's MCP servers and permissions even when the child role specifies alternatives. Disabling the shell still leaves apply_patch available. Its internal ToolPolicy.allowed_tools could impose the needed tool ceiling, but has no public app-server or configuration API. Sources: role application, inheritance tests, tool registration, internal tool policy.

James selected Pi execution for every non-owner Codex handoff instead of granting operator authority or refusing the work. Individual, guild and channel machine grants authorize the room tool set; none conveys the operator seat's full MCP, shell and file-edit authority. Only the verified owner can use native Codex children. Metadata shows the actual executor. The upstream ask is a public, enforced per-child tool and MCP ceiling, applied at both catalog construction and dispatch, without changing the parent seat's permissions. Full native non-owner Codex execution depends on that capability; hooks that can fail open do not establish this boundary.

Verification

Focused integration covers actual service receipts, persisted child records, bounded concurrency, exact native ancestry and scoped MCP calls. It separates completed computation from confirmed room delivery. The real multi-person voice call and native operator session checks belong to James; exact steps and tested limits live in the VUH-1672 evidence record.

Amendment: a sender's burst steers their own handoff (2026-10-06)

Giving every text message its own handoff dropped ADR 0118's burst coalescing: three messages in seven seconds got three replies, out of order, each asking about what the others had already said. A text message now steers the running Pi handoff of the same authenticated actor in the same room when its resolved grant (plan, owner and machine-grant standing, verified-owner proof and transport) matches exactly. It returns absorbed with the owning delivery's replyDeliveryId and keeps a completed child record naming the handoff it joined. Another speaker, the same speaker under a different grant, a native child, or a run that has already ended starts its own handoff as before; a steer the run never read falls back to a new handoff. Steering therefore never crosses speakers or raises a one-shot social turn's grant. Siblings still in flight are marked in each handoff's quoted room context. The answer stays his: the steered run may still reply, react or stay silent.