0092 — Universal communication suppression (opt-out) across all outbound channels

Context

On 2026-07-15, a Camellia tenant (unit 103) called Clara back after renewal outreach and explicitly asked not to be contacted again (conv_voice_6cd8488c-0377-488f-a975-c2f21e0b3a72). Clara acknowledged verbally and recorded nothing: no revocation row was written, no tool exists for the agent to record one, and the tenant remained fully eligible for future SMS, voice, and email outreach.

That is a live legal exposure, not just a UX gap. The controlling rules (deep-research pass 2026-07-15, all claims verified 3-0 against primary sources — FCC orders, Federal Register, FTC guidance):

  1. Revocation by any reasonable means — FCC 24-24 (47 CFR § 64.1200(a)(10), effective 2025-04-11): a consumer may revoke consent "in any reasonable manner that clearly expresses a desire not to receive further calls or text messages." A verbal opt-out during a call is per se valid. We may not require STOP-by-text or any exclusive method. Non-listed methods carry a rebuttable presumption of validity with the burden on us — which makes a provable audit trail mandatory.
  2. Revocation is cross-channel for voice + SMS — in force since 2025-04-11: "revocation extends to both robocalls and robotexts regardless of the medium used to communicate the revocation." A verbal opt-out on a call legally stops our texts to that number, and a STOP text legally stops our calls.
  3. Our voice agent is a robocall — FCC Declaratory Ruling 24-17 (2024-02-08): AI-generated voices are "artificial" under the TCPA. Clara's voice channel gets zero leniency relative to prerecorded robocalls; the full consent + revocation framework applies.
  4. 10 business days, hard ceiling — revocations must be honored "as soon as practicable," never more than 10 business days from receipt (TCPA for calls/texts; CAN-SPAM independently imposes the same window for email).
  5. One confirmation message allowed — a single opt-out confirmation (no marketing content) is permitted; it does not extend the clock.
  6. Cross-topic "revoke-all" — the provision making one revocation apply to all message topics from the sender is FCC-waived until 2027-01-31, but designing to it now is the recommended posture (and what the existing KB doc already advises).
  7. Twilio only covers part of this — Twilio's automatic STOP handling unifies SMS/MMS/RCS per messaging service. It does not touch voice or email, and it cannot see a verbal opt-out. The legal obligation is broader than the CPaaS default; enforcement must live in our own layer.

What we have today, and where it breaks:

Decision

Build a single communication suppression layer: one append-only store, one check function, enforced at every outbound choke point, fed by agent tools on every conversational channel.

1. Data model: append-only suppression events, identifier-keyed

Generalize the existing SMS-consent pattern (CONSENT#{phone}, append-only, grants never deleted) into channel-spanning suppression:

Each event records: action (revoked | reinstated), sourceChannel (voice | sms | email | pm_dashboard | twilio_21610 | stop_keyword), verbatimSignal (the transcript quote, keyword, or click), conversationId/messageSid provenance, personId (when resolvable), recordedBy (agent tool | webhook | human), and scope (default all). Append-only, retained ≥ 4 years (TCPA statute of limitations) — this is what carries the rebuttable-presumption burden.

Person-level fan-out (best practice, beyond the legal floor): when a suppression is written and a personId is resolvable, also write suppression events for the person's other contact claims (their email when they revoked by phone, and vice versa), with sourceChannel marking the fan-out provenance. One "stop contacting me" means the person, not the identifier. The identifier-level records remain the enforcement keys so an unresolved caller still gets suppressed by number.

2. Enforcement: one check, every choke point, fail closed

New checkSuppression(identifier, channel, category) in src/lib/domain/compliance/, wired into:

Channel Insertion point Today
SMS/MMS checkSmsSendAllowed (extended to read the new store) gated (old store)
Email (SendGrid) sendEmail in src/lib/integrations/email/client.ts not gated
Email (Graph) inside MicrosoftInboxClient.sendReply / sendNewMessage (agents/clara/lib/email/inbox-client.ts) not gated — the bypass
Voice outbound dispatchVoiceCall (renewal) + initiateEmergencyRelayCall not gated

Rules:

3. Message categories and the exemption boundary

Every outbound send declares a category: outreach (renewal offers, prospect cadence, nudges — anything promotional or unsolicited), transactional (replies within a conversation the person is actively holding, work-order scheduling they requested, payment/lease documents they asked for), emergency_safety (gas leak, flood, fire, urgent habitability), staff_auth (MFA).

4. Agent tools: record the revocation where it happens

5. Surfaces

Entity classification (per ADR-0027)

Entity Class Naming Spine trace (canonical) OR derived-from / rebuilt-by / drift-tolerance (derived)
SuppressionEvent canonical bare name Spine trace: via personId when resolved; identifier (PHONE#/EMAIL#) is the enforcement key for unresolved contacts

Consequences

Alternatives considered