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.
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.
The full turn trace (agent-traces admin, trace trace_9ee2f992…) shows the pipeline step by step:
| Step | What it did | Verdict |
|---|---|---|
| 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 rewrite | defect |
| 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 alerts | caught, 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:
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.
The options as originally posed, for the record:
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.
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.
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.
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.
ba0c9a81… (Camellia, email) — full thread incl. the bad send 22:39Z and the correcting takeover 22:49Z.trace_9ee2f992… — drafting model's correct text and the fair_housing … Non-compliant — rewritten step.