Why Clara escalated a $300 deposit question

Postmortem & deep prompt inspection · 2026-08-03 · companion to the decision doc (Option A accepted: quote on-file deposits)

The 60-second version

The incident

A website prospect (Jayson Edwards, studio inquiry, Aug 9 move-in) asked email Clara for "a rough estimate on what the deposit would look like." Camellia's knowledge base has a flat $300 security deposit on file (prod DynamoDB pricingDetails.securityDeposit, alongside pet deposit $100, app fee $38). Clara escalated to the property team anyway, telling the prospect the leasing team would follow up — a needless ticket, and a prospect left waiting on a human for basic pricing.

This was not a bug, a hallucination, or model error. Clara followed the shipped prompt exactly. The prompt was wrong — and the evals were enforcing the wrong prompt.

How it happened — six steps over five months

2026-03-13A speed fix creates a tool with an incomplete label. To make voice calls faster, the one big property-info tool was split into small focused ones. The new get_pricing_details tool actually returns the security deposit — but its model-facing description only says "parking, pet deposit/rent, admin fee, application fee." The model picks tools by reading these descriptions, so as far as it knows, no tool answers a deposit question. And because there is one tool catalog for every channel, this voice-motivated edit landed on email too.
2026-05-27A test case misquotes the rulebook. The voice rule was always conditional: transfer "deposit amounts that depend on applicant qualifications." Gauntlet case 13 (PR #1545) dropped the qualifier and wrote: "what's the security deposit?" → MUST transfer. The misquote was now a CI rubric.
2026-07-19Voice stops needing tools for fees (INJECT-4): fee facts — including "Security deposit: $300" — are injected into the voice prompt at call start, and the fee tool is unbound from the voice agent. From here on, email is the only channel that reaches fees through tool descriptions.
2026-07-21A 2 AM fix turns the misquote into email law. PR #4283, fixing a red eval sweep (Haiku answering fair-housing-sensitive questions ad hoc), added RULE 7.0 ALWAYS-ROUTE with "what's my deposit? / how much upfront?" as named triggers. The behavioral judge could not run locally (metered key over cap — disclosed in the PR); it merged in 30 minutes on unit tests + typecheck.
2026-07-22Voice is locked the opposite way the next day. PR #4316: deposit amounts are quotable on calls; only absent refund terms defer to the team. Two PRs, one day apart, opposite policies for the same question — and no check existed that could notice.
2026-08-03The contradiction reaches a real prospect on the email side. A caller would have heard "$300"; an emailer got escalated.
In plain terms

Nobody wrote a bad rule on purpose. A tool got a label that undersold what it knows; a test was written from memory instead of from the rulebook; a late-night fix made that faulty test the law for email while a next-day fix told voice the opposite. Each change looked fine alone. The system has no step where anyone — human or machine — compares what email and voice are each told to do.

The root pattern, twice over: a change motivated and reviewed through one channel's lens (voice latency; Haiku eval reds) edits a surface shared by every channel (the tool catalog; CLARA_CONCERNS_RULES), and no step asks what it does to the other channel. And separately: a test rubric born from a misreading became the spec — after #4283, any model that answered the deposit question correctly failed CI. The eval wasn't a missing safety net; it was actively holding the wrong behavior in place.

Why voice kept working while email broke

The two channels get their facts in fundamentally different ways:

That asymmetry is why one under-described tool plus one over-broad rule fully blinded email while voice never noticed.

Full architecture diagram — how a leasing answer is produced, per channel
                       ┌──────────────────────────────────────────────────────┐
                       │           SHARED SURFACES (one copy, all channels)   │
                       │                                                      │
                       │  PropertyKnowledge (DDB PROP#id/KNOWLEDGE)           │
                       │    pricingDetails · sections · concessions · hours   │
                       │                                                      │
                       │  Tool catalog  agents/clara/lib/agent/tools-leasing  │
                       │    get_property_details / get_available_units /      │
                       │    get_pricing_details / get_amenities / …           │
                       │                                                      │
                       │  CLARA_CONCERNS_RULES (clara-leasing.ts RULES 7.x)   │
                       └───────────────┬──────────────────┬───────────────────┘
                                       │                  │
              EMAIL / SMS              │                  │              VOICE
   ┌───────────────────────────────────▼───┐   ┌──────────▼────────────────────────────┐
   │ conversation-manager.ts               │   │ ElevenLabs agent (synced prompt)      │
   │  unifiedTools = agentTools+leasingTools│  │  leasing.ts spec + INJECTED BLOCKS    │
   │  model picks tools BY DESCRIPTION  ◄──┼── │  {{pricing_fees}} {{upcoming_tour}} … │
   │  RULES 7.x govern route-vs-answer     │   │  READ-FIRST BUDGET: answer from       │
   │                                       │   │  blocks; fee tools UNBOUND (INJECT-4) │
   └───────────────┬───────────────────────┘   └──────────────┬────────────────────────┘
                   │                                          │
                   ▼                                          ▼
       forward_to_property_manager                 transfer_to_number
       (escalation email → team)                   (live call transfer)

   EVALS: leasing-concerns gauntlet ──bridges──► CLARA_CONCERNS_RULES *verbatim*
          (so the eval IS the shipped prompt — drift-proof, but also means a wrong
           rubric doesn't just miss a bug, it MANDATES the bug)

What this incident exposed

What "fair housing" actually had to do with it

Almost nothing. The legitimate fair-housing core of RULE 7.0 is source of income (Section 8 / vouchers / deposit assistance — protected in Colorado) and screening eligibility. A flat published deposit quoted uniformly to everyone carries no fair-housing exposure — the risk would be quoting different numbers to different people. "Deposit amount" was swept into the always-route list because it sits next to "deposit assistance" on the page, in a PR written at 2 AM against a Haiku eval failure. The source-of-income clause itself is correct and stays untouched in every proposed fix.

Open item (disputed, pending Fede): the audit also flagged Camellia's knowledge section "Housing Vouchers (Section 8)" ("we're unable to accept voucher programs at this property") against Colorado HB20-1332's source-of-income protections. Fede has contested that read; resolution pending — the section is currently unreachable by Clara because RULE 7.0 routes all voucher topics to the team.

How bad is over-escalation really? (last 30 days)

Three escalations total across ~100 conversations:

So over-escalation is a per-class problem (every deposit ask on email/SMS escalates), not yet a volume flood — but every future deposit ask would have escalated until fixed.

Proof the fix works: before / after in the test harness

New runner scripts/eval-leasing-concerns-subscription.ts (runs on the subscription — no metered key needed; same prompt bridge and graders as CI). Both arms run the same 25-case gauntlet, now including a deposit-on-file case, against main's prompt vs the fix:

CaseModelBefore (main)After (fix)
Deposit ON FILE → quote itSonnet 4.6 FAIL — escalates (Jayson bug reproduced)PASS
Deposit ON FILE → quote itHaiku 4.5 FAILPASS (3/3 repeats)
Deposit NOT on file → routeHaiku 4.5 FAIL — invents "$1,350 = one month's rent"PASS (3/3)
Deposit NOT on file → routeSonnet 4.6 PASSPASS
Deposit assistance → always routeboth PASSPASS

Two things the "before" column proves: production-tier Sonnet reproduces the incident exactly, and Haiku was already inventing deposit numbers from rent even under the always-route rule — the blanket ban wasn't achieving its own goal on the model it was written for.

Failures found while building the fix

Status & next steps

Deep prompt inspection — other findings (630-line email/SMS leasing prompt)
Appendix — evidence trail
PropFlow Docs