Mute v1 — adversarial verification

Independent audit of the person-mute shipped last night (PR #6526 + #6528), by a session that built none of it. Fede's two questions answered, with evidence.

2026-08-29 · 6-agent adversarial fleet + hand verification · read-only prod, sandbox bench only · spec audited = the design page at commit 7fae7bf4 (silence + 3 exceptions: deliberate human send, emergency/safety, one team notification on inbound)

Q1 — does it cover all channels? No. The core wire gate is real and most lanes traverse it, but five verified leaks remain — one live in production today for a real Camellia tenant. Q2 — is it the smallest solution? No, but the core is close. ~1,000 of ~3,560 added lines serve the voice mechanism Fede already rejected and can be deleted as a unit; one design change (check the person, not the identifier) both simplifies the feature and closes its worst leak.

The one thing live in production right now

Channel coverage matrix

Egress / trigger pathVerdictEvidence
SMS replies (inbound → agent loop)coveredMUTED PERSON GATE at conversation-manager.ts:4196 persists and returns before any model call; bench-proven live (zero assistant rows).
Email repliescoveredSame gate, same code path; the pre-gate model call is a tool-less classifier (no side-effecting tools reachable). Lane-normalization bug was caught in review and fixed pre-merge.
SMS/MMS outbound wirecoveredsendSms/sendMms → checkSmsSendAllowed → checkSuppression; muted blocks even category transactional.
Email outbound wire (Graph, SES, SendGrid)coveredBoth Graph send doors + the shared email client gate on checkSuppression with a declared category.
Outreach cadence, chase, re-engagement, renewals, collectionscoveredEach Temporal activity calls the gate with category outreach before sending; renewal voice dial gated too.
Voice outbound — callback/renewal lanescoveredEscalation callback checks the gate as transactional (blocked under mute); renewal dial checks as outreach.
Voice outbound — vendor dialinguncoveredThe whole chain (dial-approved-session.ts → initiate-vendor-call.ts → initiate-outbound-call.ts) contains zero suppression references. A muted person's phone still rings if they're dialed as a vendor. Contradicts the builder's "all raw lanes traverse the gate" claim.
Contact added after the muteuncoveredLive-proven on bench: mute fans out per identifier once, at mute time. A new email/phone claim added later (PMS sync, application) has no row — the inbound gate AND the outbound wire both treat it as unmuted, while isPersonMuted() correctly says muted.
Ops-feed Slack postsleaksA muted person's inbound text/email posts their name, unit and message content to the property Slack channel (conversation-manager.ts:3054/:3248, before the gate at :4196), and again at voice call-end. Team-facing, so partially aligned with the new one-notification exception in spirit — but content-bearing, un-deduped, fires multiple times, and nobody designed it as the notification.
Voice inbound (as merged)weak, slated for deletionEnforcement is prompt-only (no code-level tool hard-stop), and the context-blanking list names variable keys the personalization route doesn't actually emit — real resident context still reaches the model. Fede has already re-scoped voice to a pre-agent forward at the phone layer; these defects confirm that call.
Escalated-thread team noticescoveredperson_muted outcome suppresses the rail unconditionally — note this is the opposite of the newly decided one-notification exception, which has zero implementation in main yet (002's lane).
Escalation matters / tour machinery on muted inboundcoveredBlocked pre-tool-loop for SMS/email; one gap: the tour-day-fallback team email in the voice no-destination posture has no mute check.
Model self-servicecoveredClara can never mute or unmute anyone: muted is excluded from every model-selectable scope list, with a compile-time exhaustiveness check.
Calendar invitesdormant riskNo production code sets attendees on calendar events today, so Outlook/Google never emails invites. The day someone wires attendees in (a natural tour-reminder ask), those emails come from Microsoft's servers, fully outside our gate.

Bypass flags and failure modes (not live leaks, but real)

Simplicity: what to delete, what to keep

The two commits added ~3,560 lines across 41 files. They split cleanly:

Dormancy verdict

Decisions for Fede

DecisionOptions
ADaniel Brehm's "full mute" order isn't in effectA1 (recommended): re-record him through the new mute path now — one prod write, makes the record match the order, and is the feature's first real arming. A2: leave as-is until the planned human contact happens (he keeps receiving Clara's replies meanwhile). A3: have a human reply first, then mute.
BEnforcement grainB1 (recommended): switch the gates to person-level isPersonMuted() — closes the new-claim leak and deletes the fan-out complexity. B2: keep per-identifier rows and add a fan-out hook on every new-claim write (more machinery, same behavior).
CThe muted-inbound team notification you decided todayC1 (recommended): reshape the existing ops-feed post into it — it already fires on the right event to the right channel; add mute-awareness, dedupe per person per window, strip to who/what/pane-link, and suppress the duplicate voice call-end post. C2: gate ops-feed off for muted people and build the notification separately (002's current lane — coordinate so we don't ship both).
DVendor dial laneD1 (recommended): add the suppression check to initiate-vendor-call (~5 lines, category transactional), matching the callback lane. D2: accept that mute is resident-only and enforce that at the mute API instead (reject vendor identifiers).
EUnknown-category fall-throughE1 (recommended): block unknown categories under a mute (one branch in the matrix). E2: leave as-is and rely on TypeScript.

Method and honest boundaries

Six independent agents (egress client sweep, lane map, inbound trigger map, adversarial bench, simplicity audit, dormancy re-check) plus hand verification of every load-bearing finding by the coordinating session. Bench: all 8 of the builder's scenarios re-run green on merged main, then 8 new adversarial probes on sandbox-only synthetic identities, every created row cleaned up. Boundaries: outbound suppression in the harness proves the decision at the gate, not carrier delivery; no live voice calls were placed (per mandate); the Temporal chase lane was exercised at its gate call site, not via a full workflow invocation; a subscription rate limit capped one agent's tail-end probing. Builder reports were treated as claims and four were contradicted (raw-lane coverage, takeover proof, the pinning drift test, the magic-number state); the rest held up.

PropFlow Docs