The fair-housing gate rewrote a correct reply into a wrong one

Why a hot Camellia lead who asked to tour tomorrow was told to wait two weeks — root cause, proof by replay, blast radius, and the decisions it raises.

2026-08-30 · Camellia Apartments, prospect email thread · incident corrected same evening by operator takeover · status: SHIPPED — PR #6662 live in prod 2026-08-30 evening, live-verified

The drafting model was right; the safety gate made it wrong. Clara (Sonnet 5) correctly drafted "The latest spot I have tomorrow, Monday, is 4:45 PM." The pre-send fair-housing screen — a Haiku classifier — flagged that draft while itself stating it found no fair-housing issue, then substituted its own hallucinated rewrite ("no tours until Saturday, September 12th"), which is what the prospect received. Deterministically reproduced.

What happened

A Zillow lead (studio, no pets) had been trying to book a tour since Friday. On Sunday evening she wrote: "I will be available for a tour tomorrow afternoon after all. What is the latest spot that you have available?" Monday afternoon was wide open — the calendar tool returned 27 free slots, the latest at 4:45 PM, and Clara drafted exactly that answer. What actually went out was: office closes at 5, no tour tomorrow, next opening Saturday September 12th. An operator takeover corrected it ten minutes later and the prospect was offered the real 4:45 PM slot.

Root cause — proven, not inferred

The full turn trace (agent-traces admin, trace trace_9ee2f992…) shows the pipeline step by step:

StepWhat it didVerdict
Drafting model (claude-sonnet-5, thinking on)Reasoned "tomorrow = Monday Aug 31, check the calendar", called check_availability, drafted "The latest spot I have tomorrow, Monday, is 4:45 PM. Would that work for you?"correct
Pre-send fair-housing gate (claude-haiku-4-5, temp 0)Flagged the draft non-compliant and substituted its own rewritedefect
Async post-hoc judge (~15s after send)Stamped the sent message: "the reply rejects the Monday 4:45 PM slot returned by check_availability… with no support" — but judges are post-hoc only; nothing holds or alertscaught, silent

Replaying the gate's exact call (same model, same prompt, temperature 0, subscription lane) reproduces the verdict identically on repeated runs. The judge's own output:

"compliant": false … "reasoning": "The assistant contradicts the prior message from Clara, which explicitly stated the office closes at 5:00 PM on weekdays… This is a factual/operational error, not a Fair Housing violation."

Three defects stack here:

What it was not

Blast radius

All agent turns from Aug 16–30: 1,874 fair-housing checks ran; 11 rewrote or blocked a draft. 10 of the 11 were synthetic test numbers at The Willows (harness traffic deliberately probing service-animal and Section-8 replies). This incident is the only real-customer rewrite in two weeks — the gate is quiet, but when it misfires there is no alert, and its substituted text carries invented facts. Separately, 144 promise-backing blocks fired in the same window (not examined here).

Same-thread precedent, same day: the staff answer to this prospect's natural-light question was also held by a fair-housing screen hours earlier — the escalation sat until an operator delivered the substance by takeover. Two customer-visible misfires of fair-housing screening in one thread in one day.

Decision — made 2026-08-30, same day

The options as originally posed, for the record:

D1 — Honor the judge's own classification recommended

When the verdict is non-compliant but violated_class is null (as here) or the reasoning names no protected class, treat it as a pass: by the gate's own rubric there is no violation to block. Deterministic, one-file change in the policy wrapper, preserves the fail-closed classifier-error path. Would have prevented this incident outright.

D2 — Strip quoted reply-history before judging recommended

Feed the judge only the prospect's new text, not the quoted trail of prior emails. The replay proves this flips the verdict. Cheap, and it removes a whole class of "judge polices the draft against stale quotes" failures.

D3 — Never ship judge-authored rewrites

On a genuine flag, fall back to the canned safe reply (which invents nothing) or re-draft through the real loop with the violation as an instruction — instead of sending prose written by a context-blind classifier. Bigger change; matters only on true positives, which are ~5/month and mostly harness traffic today.

D4 — Alert when the post-hoc judge refuses to back a sent reply

The async judge diagnosed this exactly, 15 seconds after the send, and told no one. Keep judges out of the live path (standing constraint) but page a Slack channel on a refusal stamp, so a human can take over in minutes instead of by luck. Alert-only; no send-path risk.

Shipped & live-verified (same night)

Evidence trail

PropFlow Docs