PropFlowAI · Engineering Brief

Inbound email: what dropped Denise, and the deterministic-first redesign

2026-07-24 · Investigation + 50-real-email replay harness (250 decisions per arm) · Prototype lives in fede/ingest-harness (local, unpushed)

TL;DR

A real Zillow lead (Denise Holguin, Camellia) was silently dropped by the inbound-email pipeline on 2026-07-24. She was recovered same-day through the real prod path — Clara replied at 4:44 PM MT. The investigation found the drop is systemic, not an edge case: 76% of 8,182 prod ingestion rows end in silent skips, and policy_skip is an unalerted, unrecoverable sink fed by at least five independent mechanisms.

A redesigned pipeline — deterministic-first, LLM only for genuinely ambiguous human free-text — was prototyped and benchmarked against the current pipeline on 50 real prod emails, 5 trials each:

96.0%correct disposition (vs 91.2% current)
66%of emails decided with zero LLM calls (vs 32%)
0%disposition flips across repeat trials (vs 4%)
0.34LLM calls per email (vs 1.36) — 4× cheaper

Under an injected total LLM outage the prototype still replies to both guest-card leads; the current pipeline answers neither. One earlier causal claim did not survive the experiment — see the correction below.

Head-to-head results 50 emails × 5 trials × 3 arms

MetricCurrent (contaminated menu)Current (clean menu)Prototype
Dropped reply-worthy (silent)0.0% (0/95)0.0% (0/95)0.0% (0/95)
Correct disposition91.2% (228/250)92.4% (231/250)96.0% (240/250)
Disposition flipped across 5 trials4.0% (2/50)4.0% (2/50)0.0% (0/50)
Label flipped across 5 trials6.0%8.0%2.0%
Decided with zero LLM calls32.0%32.0%66.0%
Mean LLM calls per email1.361.360.34
Total LLM calls (250 decisions)34034085
Harness errors000

Named failures (no bare “passed”)

Denise & Jay guest cards, per arm

All three arms landed reply / tour_request 5-of-5 in the harness. The difference is mechanism and cost: the current pipeline spends 10 LLM calls across the pair and the answer is a sampled token — prod sampled differently on 2026-07-24 and never replied to Denise. The prototype resolves both at zero LLM calls via the guest-card body parser. With a dead LLM endpoint injected, the prototype still replies to both; the current pipeline files both as operational and answers neither.

Correction: the contamination link is not supported

The original root-cause chain claimed the classifier picked appfolio_ntv_processed because deterministic-handler bookkeeping strings contaminated its DB-derived category menu. The experiment does not support that link. In 750 current-pipeline decisions, policy_skip was reached zero times; a focused 160-call probe never once got the classifier to select appfolio_ntv_processed even with the string sitting in the menu; and the contaminated-vs-clean ablation showed no measurable difference (91.2% vs 92.4%, same five failures).

What remains verified fact: Denise’s prod row is classified appfolio_ntv_processedpolicy_skip, and she got no reply. Open hypothesis (unverified): that string exists nowhere in the current source tree — it’s legacy data in prod rows — so a legacy deterministic handler stamping the row directly is a live alternative explanation. This needs re-verification before the mechanism is asserted anywhere.

The systemic finding stands regardless of which mechanism stamped the row: an open classification vocabulary feeding an exact-string response allowlist means any unrecognized label — from contamination, LLM flake, keyword fallback, or a novel-but-valid category — becomes a silent drop.

Flaw inventory from 3 adversarial reviews

Held security P0 — needs Fede’s explicit go-ahead, not shipped. The webhook path performs zero email authentication (DMARC/SPF parsed into headers, never checked), and isTrustedAppfolioSender uses an unanchored substring match — …appfolio.us.attacker.com is trusted, and the attacker’s own domain passes SPF/DKIM legitimately. Deterministic AppFolio handlers run before any AI gate and the tenant resolver matches on unit number alone, so a crafted email can file a fake notice-to-vacate (killing a renewal, spawning a turnover) or unpublish live listings. Proposed fix: separate PR — exact-address equality plus a shared isAuthenticatedSender(headers) DMARC gate. Held because it changes sender trust on the live Camellia mailbox.

The redesign: deterministic-first, LLM-last

Design directive (Fede, locked): maximize determinism — the LLM is a fallback for genuinely ambiguous human free-text, never the default path. Low confidence goes to a human, never to a guessed category.

flowchart LR
  A[Inbound email] --> B{Sender-class router
deterministic} B -->|guest card / aggregator /
system mail / bounce / internal| C[Closed category
0 LLM calls] B -->|structured body| D[Parsers
guest-card, portal relay] D --> C B -->|unknown human sender,
free text| E[One structured LLM call
category + confidence] E -->|high confidence| C E -->|low confidence / failure /
empty-after-strip| F[needs_review
never drops] C --> G[Action map over closed enum
unmapped = compile error] F --> G
Every known machine sender and structured body resolves with zero LLM calls; the single decision call runs only on the residual.

Honest caveats

Decisions needed & next steps

Artifacts: ~/.claude/propflowai-ingest-harness/harness/REPORT.md · immutable results in harness/results/final/ · prototype in harness/prototype/. Branch is local-only by design; nothing has been pushed or merged.

PropFlow Docs