Clara answers three kinds of rental questions: company policy (pets, fees, screening), home facts (neighborhood, schools, parking), and listing facts (price, availability). This page tracks how well that works.
Updated 2026-09-15 · Western Slope's scattered homes, pattern applies portfolio-wide · nothing live at a customer
| Channel | Status | Proof |
|---|---|---|
| Phone | Partly | Books, reschedules and cancels. Quality broke rulings on 2 of 3 calls: budget asked twice, stacked questions, stated our home count, implied recognizing a first-time caller; one reschedule hit the wrong day. No bench script yet for pets or insurance by voice. |
| Text | Yes | All five steps pass as a prospect: right home, rent, size; property pet rule beats company rule; company insurance rule holds; tour booked, moved, cancelled, calendar cleared. Test tenant recognized by name and home; leak gets a photo request; rent question goes to the team, not invented. |
| Blocked | Clara composes the right reply, but it never arrives: our Microsoft test tenant cannot send outside itself, so every reply bounces. Test-looking sender addresses are also dropped silently. Blocked until the restriction lifts. | |
| Knowledge | Yes | Company policy reaches every channel; property policy wins on the same topic; proven live by text and by a bench call. |
| Calendar | Yes | Events land on the agent's Outlook and clear on cancel; zero leftovers after the run. |
Full step-by-step results with message and tour ids are in the appendix.
Open: one question. When a lead names a home's street or a company but not which unit, what should Clara do?
Recommendation: (a). Keeps Clara answering instead of stalling, and never guesses on a customer's behalf.
| Decision | Options considered | Decided |
|---|---|---|
| How we hear a shared company mailbox | (a) live subscription · (b) poll it · (c) customer forwards to us | 2026-09-15, Fede: (a), flexible per client — a property with its own mailbox is subscribed and answered there; a shared mailbox is subscribed once and Clara replies from it |
| Which address a reply comes from | (a) the customer's own leasing mailbox · (b) our address | 2026-09-15, Fede: (a) — always the mailbox the lead's message arrived in |
| What stays dark in week one | (a) only new leads coming through the customer's portfolio · (b) the whole mailbox goes silent | 2026-09-13 ruling: (a) — anyone who emails us directly still gets a full reply; only new leads from the customer's own listings are held back until Fede turns them on |
| Company tour length in Settings | (a) add the field to the company Settings screen next week (recommended) · (b) leave at the default 30 minutes until a customer needs a different length | 2026-09-15, Fede: the company tour length is set from the company Settings screen, not a script. Not built yet. |
Proposed, pending Fede's review. Every channel assumes an apartment building, wrong for Western Slope.
9,875 real staff messages mined, 4,337 + 5,538:
| Word | Western Slope (all houses) | Situs Group |
|---|---|---|
| unit | 1,151 | 776 |
| property | 1,796 | 2,159 |
| home / house | 77 / 51 | 474 |
| apartment | 32 | 150 |
| showing vs tour | 1,831 : 59 | 2,258 : 658 |
| leasing office | 0 | 1 |
Decided (Fede, 2026-09-15). Their messages set the facts: "unit"/"property" not "home," "showing" not "tour," one exact rent, never a range; not the voice. Fede on their robotic opener: "this is a robot text, i think we could do better." Ours: greet by name, answer what was asked, name the home with rent and a fact or two, then offer two times.
What staff write on the second and third follow-up message: appendix.
Kinds. Four kinds (community, single home, small plex, mixed) live on the home; a marker on the company flags "leasing runs from one central number." The kind is a fact about the home, not the ad. Western Slope's 49 homes are single homes; their shared front door becomes the company's phone line, not a home.
Wording and script. For a single home: "unit," "property," or "the house on street," never apartment or floor plan; quote that home's own rent, never a range; address not office; say showing, not tour. One shared script, kind picks vocabulary. Spreads to follow-up texts, reminders, team notes. Ships Thursday on every home; script merge and plex-as-kind later.
What does Clara do next?
Recommendation: (b), wording only for Thursday. Ships Thursday: one time, yes/no ask, beds/baths/rent once, link after a showing. Later: matching each message's language, noticing a no-show before an apology.
His design makes every home its own building, phone line on the company. We use a stand-in home record as front door; our test company's 14 homes are units in one record. Agree on substance; need to settle before it's written twice.
Verified against origin/main at 8ccfb886 on 2026-09-15; read-only.
agents/clara/lib/voice-agents/leasing.ts (building) and agents/clara/lib/voice-agents/leasing-many-homes.ts (many homes). Per-home pricing rule: leasing-many-homes.ts:262, reinforced at :633–634.conversation-manager.ts → getUnifiedSystemPrompt (clara-unified.ts:829) → agents/clara/lib/agent/clara-leasing.ts. "find the right apartment" :1417; apartment-type ask :1466; worked example "a couple of 1BRs open running $1,550–$1,625/mo" :1470; "Lead with a price/size range" :1484; [AVAILABLE UNITS] block :111, :117, :1484; "leasing office" :1486, :1489, :1580; walk-in :1611; buzz code :806, :936, :942, :1592. (That file's own header comment naming only clara-text.ts as its caller is stale.)agents/clara/lib/messaging/transports/tour-reply-template.ts:67, :139.src/lib/i18n/templates.ts:960.propertyName ?? 'our community' — conversation-manager.ts:10946, src/lib/domain/promises/outbound-guard.ts:178.deriveLeasingIntakeMode (src/lib/domain/leasing/listing-resolution.ts:153) has three rungs: reach > 1 (dormant); isPortfolioProperty = property.type === 'scattered_site' (:59–64); and the company rung companySaysManyHomes(). isSingleBuildingProperty (:87) forces 'single' for apartment / mixed_use / commercial — nearest wins, which is the precedent this proposal follows. Correction to this morning's inspection: scattered_site is not the only switch.PROPERTY_TYPES = apartment, single_family, commercial, mixed_use, scattered_site — src/lib/domain/properties/property-type.ts:41–49. single_family (:43) is excluded from ONE_FRONT_DOOR_TYPES and hits neither branch; its only read is cosmetic, agents/clara/lib/agent/tools-leasing.ts:2209.Property.assetType is dead. Declared src/lib/data/types.ts:2955 ("OPTIONAL and unwritten today"); projected in a GSI attribute list (src/lib/data/dynamo/proporg-gsi.ts:99) and documented in src/lib/domain/portfolio/registry/keys.ts:335–342; no code reads it off a Property record.organization.operatingModel === 'centralized' read in companySaysManyHomes() — src/lib/domain/leasing/portfolio-leasing-scope.ts:90–97, behind PORTFOLIO_LEASING_RULES_LIVE = false (:82).d7d94c06): a real reply opened with studio and one-bedroom price bands — the range-first behaviour this proposal keeps for communities and forbids where one address is named.~/appfolio-dumps/westernslopepm/guest_cards/ and 5,538 in ~/situs-data/situs-dump-2026/guest_cards/. Counts in the table above are term frequencies over those two sets.single_family property rows plus one scattered_site front door. Fairhaven (test) is scattered_site with 14 homes as UNIT rows, $1,395–$2,950.This fixes the biggest gap: company policy today lives where only the front-door property can read it. Moving it up lets any home answer a policy question. Tracked on the go-live tracker.
Source material: file paths, PR numbers, ids, call-by-call reports. Nothing here changes the picture, table, or decisions above; it proves them.
Fede's rule: company (org-level) policy is injected and respected everywhere, but a property-level policy on the same topic always wins. Built and proven today on Fairhaven Residential (TEST), org org_53047a06-2c49-4231-92c2-6f2a7009149a, property fairhaven-homes (bench line +17208071724). The company knowledge base is fabricated for this test per Fede's instruction — the real customer is still writing theirs.
| Before | After | |
|---|---|---|
| Company-level policy store | Did not exist — no writer, no seeded rows for any org | 12 policy sections seeded for Fairhaven's company (application fee, screening, pets: "no pets allowed company-wide", deposits, office hours, lease terms, utilities, parking, smoking, renters insurance: required, $100k liability, holds, tours) |
| Property's own Pets section | "Cats and dogs are allowed at nearly every home currently listed. Breed, size, deposit and pet-rent details are not published." | "Two pets allowed per home, cats and dogs. A one-time pet fee of $300 per pet applies, due at move-in." — deliberately conflicts with the company's "no pets" line on the same title |
| Rendered knowledge block (voice, text, email — one shared builder) | Company sections never reached the builder (no reader wired) | Renders the property's Pets line, not the company's; the company-only Renters insurance line still shows through untouched |
| Live phone call asking "do you allow pets?" | n/a — no company policy existed to conflict with | "Yes, we do allow pets! We allow up to two cats or dogs per home, and there's a one-time pet fee of three hundred dollars per pet due at move-in." (property wins) |
| Same call, "do I need renters insurance?" | n/a | "Yes, renters insurance is required — you'll need a policy with at least one hundred thousand dollars in liability coverage, and you'll need to provide proof before move-in and keep it current throughout your lease." (company-only policy survives) |
Voice and text/email are proven identical because they call the exact same renderer (buildLeasingKnowledgeBlock, fed by the same loadOrgKnowledgeSectionsSafe helper) — confirmed by rendering it directly against the live Fairhaven row and comparing to the live call transcript, both quoting the property's pet-fee language verbatim. Live call: EL conversation conv_8401m2k509v9efnb5n56yjz01d77, +17208071724, 2026-09-15. Shipped as two small PRs: #8633 (company knowledge store + seed script) and #8638 (wired into all four leasing readers). Both dark by default — a company with nothing seeded renders exactly as before.
Ten real guest-card emails were run through the actual reply-writing step (nothing sent, nothing saved) — five re-addressed to Fairhaven's real homes, five composed for the property they originally named, Camellia, so we could see today's real Camellia reply shape too. Every reply graded clean on facts (right home, right price/beds/baths/availability, no invented numbers, nobody's name given out); the single-family wording rule (no "apartment", "unit", "floor plan", "community", a price range once a home is named, or "leasing office") held on all five Fairhaven replies. The two soft misses below are both the same pattern: Clara promising an internal follow-up ("I'll let the property team know…") that nothing is wired to yet.
| Card | Property | Home named? | Facts | Wording |
|---|---|---|---|---|
| 1 | Fairhaven | Yes (1704 Emberly Way) | Pass — 2bd/1bd, $1,450, cats-only, 10/1/26 all match the live unit record | Pass |
| 2 | Fairhaven | No — whole-homes fallback | Pass — quoted range ($1,395–$1,975, 980–1,210 sqft) matches only the homes available NOW, correctly excluding two homes not ready yet | Pass — no banned words; "our office is open…" for scheduling is a soft miss worth watching (not one of the named hard-fail phrases) |
| 3 | Fairhaven | Yes (118 Quillwood Lane) | Pass — 3bd/2ba, $2,150, 1,480 sqft, both pets, all match | Pass |
| 4 | Fairhaven | Yes (146 Quillwood Lane) | Pass — prospect asked about a 1-bedroom; Clara correctly says none is available rather than inventing one | Pass — soft miss: "I'll let the property team know… so they can follow up" is a promise nothing acts on yet |
| 5 | Fairhaven | Yes (1265 Sparrowgate Road) | Pass — 3bd/2.5ba, $2,395, 1,720 sqft, both pets, all match | Pass |
| 6 | Camellia | Named address is not one of Camellia's own | Pass — correctly says it can't find that address instead of guessing | Pass (apartment language is correct here) |
| 7 | Camellia | Named address is not one of Camellia's own | Pass — falls back to Camellia's real $1,000–$1,200 / 386–585 sqft range, sourced from the live unit list | Pass |
| 8 | Camellia | Named address is not one of Camellia's own | Pass — same real range; asks the prospect to confirm before booking anything | Pass |
| 9 | Camellia | Follow-up on card 7 (utilities question) | Pass — utilities/Xcel/Conservice answer matches the property's own knowledge text word for word | Pass |
| 10 | Camellia | Named community is not Camellia | Pass, soft miss — invents no Camellia facts, but promises "the leasing team for [that community] will follow up" — nobody confirmed that will happen | Pass |
Composed read-only against propflow-prod data through the merged compose stage (PR #8622) plus a same-shape scratch runner for the Camellia leg (Camellia is a real customer, so it cannot route through the bench credential the merged script uses — the scratch runner reuses the identical knowledge/prompt/read-only-tool code and bills only the capped eval-workspace key). Nothing sent, no row written.
699 threads where staff wrote twice or more. Most second messages chase silence rather than answer a question, and they come fast: a median 11–19 minutes against roughly two hours on turn one. Staff name one concrete time and ask yes or no. They volunteer the home's beds, baths and rent unprompted — "By the way, Pomona units are 3 bed, 2½ baths and rent is $2,395." They switch to Spanish message by message, hand a thread to a named colleague, and answer a no-show with an apology and a new time in one message, offering a video walk-through. The application link goes out on turn two or three. Nobody uses a lockbox or self-showing. About a third of threads die right after that second message, usually just after a time is proposed.
| Situs Group | Western Slope | |
|---|---|---|
| Threads with 2+ staff turns | 390 | 399 |
| Turn 2 follows silence | 69% | 53% |
| Median turn-2 reply gap | 19 min | 11 min |
| Thread dies after turn 2 | 42% | 30% |
| Application link on turn 2 or 3 | ~30% | ~10% |
A listing is the for-rent sign in the yard — pull the sign when the unit rents, and the house, the neighborhood, and the driveway don't go anywhere.
ORG#…/LISTING#… rows, no Property row. One anchor property holds contact/screening/fees/pets/snow/line-limit sections for the whole company. Tour-arrival sections are queued on the anchor, keyed by address text, not a stable id.enrich-listing-neighborhoods.ts cron; isSharedBuildingAddress address-string regex in tour-arrival-info.ts.)knowledge.section holds company policy and inherits down; each home becomes its own Property carrying neighborhood/schools/geo/drive-times/parking/arrival as property-level knowledge; line, calendar, and mailbox stay attached at the org.ws-home-knowledge-2026-09-14, #engineering-bridge ts 1789411702.102439.Proves: registry keys.ts (knowledge block, D6-A "structured knowledge INHERITS"), store.ts getPropertyKnowledge, tour-arrival-info.ts, the model chapter's invariant "one node runs a building… nothing under PROP# ever moves," and the Western Slope go-live tracker's anchor-property row.
| Level | Voice | SMS | |
|---|---|---|---|
| (1) Org / company sections | Not at all — effective-sections.ts:215,278, zero importers repo-wide, darkness pinned by __tests__/effective-sections.test.ts:380,388; no rows written either (effective-sections.ts:41-45) | Not at all — same module, zero importers | Not at all |
| (2) Property sections | Up front, but only after hand-off — renderPropertyPolicies at src/app/api/voice/personalization/route.ts:1154, emitted as property_policies at :3121, consumed by leasing.ts:301 / leasing-many-homes.ts:669 / lease-and-billing.ts:138; triage.ts declares no {{property_policies}} | Up front, every turn — conversation-manager.ts:7000,7193 → clara-unified.ts:1163, no hand-off gate | Identical to SMS — one builder, clara-unified.ts:805 called from conversation-manager.ts:9105 |
| (2b) Property pricing / fees | Up front, subset — paymentFees, rentersInsurance, customFees are voice:false (property-knowledge-block.ts) | Up front, full set — clara-unified.ts:1191 | Identical to SMS |
| (3) Listing card | Up front, unconditional, rich — formatPortfolioHomes at leasing-context-injection.ts:573; parking :670, "Around the home" :691 | Mid-conversation only, thinner — gated by tour classification or a single named apartment (inbound-router.ts:918,1010,1039); no deposit, pets, amenities, parking, or drive times unless a tool is called | Identical to SMS — same router branch (inbound-router.ts:856) |
| (3b) Scattered-homes enumeration | orgHomesForLine shares one source with mid-call tools (org-offerable-homes.ts:403) | Up front preload reads the anchor property's fetchLeasingAvailability, which has no unit rows — empty | Same |
| # | What a prospect experiences | Smallest fix | What it folds into |
|---|---|---|---|
| G1 | Retired sections leak via get_property_details (tools-leasing.ts:2182 returns raw sections, no retiredAt filter; up-front block filters at property-policies.ts:31) | Return screenedSectionsForVoice(...) / a shared retiredAt filter; reuse src/lib/domain/escalation/voice-knowledge-gate.ts | One knowledge-block builder — the filter runs once for all three readers |
| G2 | Triage has no property-knowledge reader — triage.ts declares no {{property_policies}} | Closed, won't do (Fede, 2026-09-15): the greeter works like prod and transfers leasing questions to the leasing Clara, who has the one block. Not a gap. | — |
| G3 | The per-home block (aroundTheHomeLines) is voice-only; no text module imports it | Have the text prompt render the same per-home lines from the same producer | One knowledge-block builder |
| G4 | Listing card up front on voice, conditional on text (inbound-router.ts:1010) | Populate the text preload from orgHomesForLine/listingCardFacts on any leasing turn with a property | One knowledge-block builder |
| G5 | Scattered-homes text preload reads the anchor property's roster, which has no unit rows | Same fix as G4 — route the preload through orgHomesForLine | Migration PR 1 (Property per home) |
| G6 | Org tier reaches nothing; rows don't exist yet | Wire resolveEffectiveSections into the one knowledge-block builder, behind the same filters | Migration PR 3 (company sections move to org) |
| G7 | Voice pricing subset — paymentFees, rentersInsurance, customFees are voice:false by design | Flip the three flags — its own voice behavior change, needs its own PR and a live-call proof | Its own PR, outside this migration |
Source: knowledge-matrix.md, code-grounded audit with file:line citations, 2026-09-14.
| Step | What happens | Code |
|---|---|---|
| Arrives | Microsoft notifies us about the property's mailbox; the owning property is found by scanning all properties for the subscription id. A second door matches recipient address. | webhook-processors.ts:419, match-property.ts:98 |
| Recognised | A card needs an appfolio.com sender and a "CONTACT INFO" body; eight listing-site domains are handled separately. | parse-lead-source.ts:837, :55 |
| Parsed, routed | Name, email, phone, bedrooms, move-in, budget, comments, source, raw unit label, then the tour lane. | parse-lead-source.ts:922, process-inbound-message.ts:1420 |
| Person, home | The save-prospect tool creates the prospect, a deterministic pass fills what it dropped, and the raw unit label matched to that property's unit rows — no match leaves it unset. | apply-parsed-lead-facts.ts:277, :171 |
| Knowledge | One prompt builder for all channels, given the property's policies. | clara-unified.ts:805, property-policies.ts:27 |
| Reply | Fresh message to the renter, not to the relay, through the property's own mailbox. | decide-email-send-mode.ts:114, property-graph-sender.ts:279 |
| Dark | Suppressed by a per-property switch, the leasing stage, or the pipeline allowlist. | process-email-record.ts:47, capability-stage.ts:20, operating-mode.ts:50 |
Honest finding. No Camellia address, org id or property id is hard-coded in the parse-and-reply path. The shortcuts are structural: it assumes one mailbox per property — the assumption a many-home company breaks.
| Shortcut | Why it breaks Western Slope | Replacement |
|---|---|---|
Inbound wired only to Property.emailIntegration — outlook-subscription-renewer/handler.ts:113 | Their mailbox belongs to the company, not to one of 15 homes; nothing subscribes to it. | Subscribe on the company hook, ORG#/ATTACH#mailbox |
Company mailbox send-only — company-mailbox.ts:36 | We can write from leasing@ but not hear it. | The lifecycle the per-property door has |
| Mailbox → property by scanning every property | One mailbox, many homes: no right answer. | Gera's doorbell, ADDR#email#<mailbox>/HEAD → one company, one node |
Home is a unit row — apply-parsed-lead-facts.ts:171 | Scattered homes have no unit rows; the home never resolves. | The home's own Property, via org-offerable-homes.ts:240 |
Listing scraping one hard-coded row — listings-sync.ts:24 | Only JP&Co's listings page is in the array. | Each company's listings source on its record |
Rent-roll source a hard-coded id set — process-inbound-email.ts:78 | In the email entry file; the next customer needs a code edit. | A property field |
Ingress. A Microsoft change-notification subscription on the company mailbox, polling, or the customer forwarding AppFolio's notifications to us. Recommend the subscription — webhook, renewal job and token vault all exist; only the target list is per-property. Forwarding is the fallback if consent is blocked.
Home resolution. The mailbox gives the company, through the doorbell row. The card gives the home, through its address or its "Interested in" line, matched against that company's offerable homes — the home's own Property, never a unit row. An unknown address fails closed and logs; we never invent a property.
Identity. One Person per company, found or created by the in-company email claim first, phone second — the resolver a phone call already uses.
Reply. Sent as the customer from their leasing mailbox, to the renter not the relay, threaded on our conversation. Dark means one thing: no proactive answer to a card from the customer's portfolio until Fede turns it on. Anyone writing to the mailbox directly still gets a full reply.
| # | One concern, dark | Size | Thursday |
|---|---|---|---|
| 1 | Doorbell row: mailbox address → company and node | ~200 | yes |
| 2 | Subscription, renewal, webhook lookup for a company hook | ~250 | yes |
| 3 | Home resolution from a card, fail-closed | ~250 | yes |
| 4 | Email turn reads the one knowledge block | ~200 | no |
| 5 | Reply from the company mailbox, threading, the dark rule | ~150 | no |
| 6 | Replay harness leg, Fairhaven fixtures | ~250 | no |
Thursday's floor is hearing, resolving and recording — PRs 1, 2, 3, dark. Separately, the fact-ownership migration:
Proves: the standard rollout playbook (dark, ≤300 lines, one concern per PR) already governs every PropFlow PR chain; registry writable-at levels for knowledge.<fact> and knowledge.section.<id>.
| Channel | How we learn the home | What Clara knows before her first reply |
|---|---|---|
| Email guest card | The listing site names the property and usually the exact home in the card. | Company policy, property facts and the listing card, loaded before she reads the message. Behaves like Camellia: the home is known up front. |
| Text from a listing site | The tracking number or link carries the home; the first text usually names it. | Same as email once the home is matched; company policy only until then. |
| Phone call to the company line | Only the company is known. The home comes out of the conversation, by street address or by criteria. | Company policy only, then property and home facts once the caller names or picks a home. |
Bench proofs, all run on Fairhaven Residential (TEST), our own bench company, no real prospect involved: (1) a home answers with its own knowledge on a bench call; (2) an org-level fee answer inherits down with no per-home copy of it; (3) re-listing a home keeps its facts and costs zero re-enrichment spend. Each proof is an artifact — a call, a database read, a log line — not a claim.
Same script run repeatedly on Fairhaven Residential (TEST): ask a policy question, a home-fact question, a listing-fact question, then try to book a tour that lands on the leasing agent's own calendar.
Phone: pet policy answered correctly pre hand-off; the neighborhood/distance-from-downtown question about 875 Kestrel Bend was asked twice and Clara stalled both times instead of answering or saying she didn't have it; listing facts matched the card. Booking failed twice — home_not_resolved on the named address, then unit_not_available_for_tour on a substituted home — so no tour exists. Text and email: not wired yet (no Fairhaven scenario existed in either harness). Report: scripts/voice-harness/dry-run/results/voice-reports/many-homes-2026-09-14T23-39-26-418Z.json.
Confirmed live that the org's "book on people's calendars" setting was already on — a prior note calling it off was stale. Made no difference: the tour never got that far. Phone: the caller asked the pet-policy question before naming a home and Clara deflected to "each individual property" instead of answering from company knowledge — the known gap where triage hands every such question to leasing. Home and listing facts both failed the same way: caller named 875 Kestrel Bend, the lookup tool returned nothing, and Clara told the caller "that one isn't coming up in our system," then failed a second general search too. No tour attempt fired at all — a step back from pass 2. Sentry stayed clean because the failures returned empty answers instead of throwing errors, so nothing paged. Report: scripts/voice-harness/dry-run/results/voice-reports/many-homes-2026-09-15T08-27-29-464Z.json.
Phone. Call conv_2601m2j55m46ejyaadp0w7wtxwfg, goal score 7/10. Pet policy yes/no answered but breed/size/deposit specifics deflected to leasing rather than saying "not published" — a real remaining fail. Home fact and listing fact for 875 Kestrel Bend matched the card exactly. Tour booked for Oct 15, 1:00pm, guest card name "Devin Ashford" (caller said "Devon" — an ASR spelling miss, not a defect); tour row 6586d0f9-1f4e-4398-949a-9888bfaf7420 read directly from the database, then cleanly cancelled by the harness's own cleanup. No team email fired (none was composed for this call); confirmation text couldn't be checked because texting is shadowed on this bench config.
Text. Now wired for Fairhaven. 12 of 13 checks passed over 6 turns — pets, bedrooms, rent, availability, garage question all correct. The one "failure" was the check tool giving up 7 seconds before the tour actually landed in the database (8183ddb1-0ff3-402f-a5f3-1bf4a30409c3) — a timing bug in the test tool, not a booking failure. Report: ~/.claude/sms-stress/reports/SMS-1789463642436.md.
Email. Now wired for Fairhaven and the inbound message was accepted, but Clara never replied inside the 8-minute test window. This overlapped a live rate-limit error ("This request would exceed your account's rate limit") on the shared background AI pool that also degraded the text run's automated grading. Filed as a hypothesis (the capped pool starving the actual reply call), not a diagnosed root cause. Report: ~/.claude/email-stress/reports/STRESS-1789463635325.md.
Reschedule/cancel by phone. A booked tour was "moved" by a live call — Clara said "you're all set" and the reschedule tool fired — but the tour record shows no change at all: same date, same time, no history entry. The caller was told a change happened that did not happen; this is a real, unfixed defect (src/lib/tools/leasing.ts:704-786, documented as "cancels the old tour and creates a new one," neither occurred). A follow-up cancel call on the same booking worked correctly. Sentry stayed clean because the no-op didn't throw an error.
Calendar discrepancy, unresolved. Every tour above is bound to the same calendar-agent person, whose channel record resolves to a specific Outlook mailbox — confirmed live. Querying that exact mailbox directly for the tour's event, both by id and by date range, returned zero matching events both times. Either the calendar write isn't reaching the mailbox Microsoft's own lookup shows, or the two point at different underlying accounts despite matching addresses. Not diagnosed further this pass; candidate files: src/lib/domain/calendar/sync-tour.ts:733, src/lib/domain/calendar/outlook-client.ts:506.
Separate from the scripted dry run above: every real AppFolio guest-card notification held in our own mailbox, replayed through the production parser and resolver with sending disabled — nothing sent, nothing written, no model called. Corpus: 40 real guest cards from one scattered-homes company, deduped by message id. Harness: scripts/email-stress/guest-card-replay/.
| Step | Before | After | What changed |
|---|---|---|---|
| Renter's name read | 23/40 (57%) | 40/40 (100%) | The second-visit notification ("New Interest on an existing guest card") now reaches the parser. |
| Home the card is about | 23/40 | 40/40 | Same cause. |
| Move-in date | 0/40 (present in 30 emails) | 30/30 | The parser now knows the section the relay prints it under. |
| Unit label | 32/40 set, 25 wrong | 8/40, none wrong | A field label ("Move In Date: 09") is no longer mistaken for a unit number — a wrong unit was worse than none, since it drove a real lookup downstream. |
| Renter's own words, clean of noise | 23/40, all polluted with template text | 38/40, clean | The relay's own boilerplate is no longer appended to the prospect's message as if they'd said it. |
| Matched to the right home | 7 correct, 5 wrong, 28 none | 33 correct, 5 wrong, 2 none | The lane now reads the home the card names in its own banner before guessing from the subject line. 95% resolve, up from 30% for the company match and 18% for the correct home. |
| Company resolved | 12/40 (30%) | 38/40 (95%) | Derived from the home match. |
| Renter questions we could answer | 27/40 (67%) | 32/40 (80%) | Most of the remaining gap is phantom questions disappearing — a field the relay itself prints was being miscounted as something the renter asked. |
| Company policy in the reply | 0/40 | 0/40 — unchanged on purpose | Policy lives on one company record today and nothing reads it per home; that's the target-shape migration above, not a parser fix. |
| Listing facts in the reply | 43% at best | 43% at best — unchanged | A listing fact only exists while the ad does; matching the home correctly doesn't conjure facts nobody stored. |
Still wrong: 5 cards match the wrong home when two roster rows tie on the same street name; 2 name a home with no record at all; and this measurement covers only one of our two inbound email lanes.
Proves: harness PRs #8588, #8594, #8595, #8613; fixes #8603, #8604, #8607, #8610 — each number is printed by scripts/email-stress/guest-card-replay/run.ts over the real corpus, and each defect is pinned as a test that fails on the fix (passes on today's wrong behavior) so it turns red first.
PR #8622 (merged) adds the actual reply-composing stage, through the exact functions the live email lane calls, pointed at our subscription bench credential rather than the metered production key. Two review rounds found and fixed real gaps before merge: read-only tool calls were silently refusing without a live conversation id, and each tool call was writing an audit row into the production table even though this stage claims to be read-only. The shared pool this stage's model calls ride was capped at merge time (same constraint as the email dry-run above); it reset and the 5-card proof ran clean below.
Fairhaven Residential (TEST) is a 14-home scattered-site portfolio, not one building: 7 addresses in Fairhaven, CO (80402/80403), 7 in Westmarch, CO (80517/80518) — a Westmarch address on a card is a real home in this portfolio, not a data error.
| Card | Home named | Facts right | Policy line | No invented facts | No apartment-complex wording | No staff name |
|---|---|---|---|---|---|---|
| Jordan Q. — new lead | 118 Quillwood Lane | pass — 3bd/2ba, 1480 sqft, $2,150, now | pass — $300/pet fee, 2 pets, from the Pets policy section | pass | pass | pass |
| Jordan Q. — return visit, asked about utilities + a tour | 1265 Sparrowgate Road | partial — beds/baths/sqft/pet policy right, rent never stated, utilities question dropped silently instead of answered or deferred | fail — no policy line in this reply | pass | pass | pass |
| Alex Rivera — no home named | none (whole-homes fallback) | pass — rent/sqft range correctly bucketed from live inventory; omits one qualifying home (avail. 12/1) without saying more exist | pass — office hours | pass | pass | pass |
| Jayden Hust — asked a 1BR move-in date | 146 Quillwood Lane (an actual 2BR) | correct that no 1BR exists anywhere in the portfolio; never mentions 146 Quillwood is a real, available 2BR — confusing rather than wrong | fail — no policy line | pass | pass | pass |
| Seth Perry — mentioned a pet, asked for an application | 1704 Emberly Way | pass — 2bd/1ba, 900 sqft, $1,450, cats-only, avail. 10/1 | fail — doesn't state the pet fee or address the "Pet Type: Other" ambiguity | pass | pass | pass |
Net: 5/5 correctly named and priced the right home with no invented facts, no apartment-complex language, and no staff name — the harness's own safety bar. 2/5 dropped the org policy line; 2/5 left a piece of the renter's actual question unanswered instead of naming the gap. No card in this sample presented a policy conflict, so the property-wins-on-conflict rule is still unexercised here. Full replies and file:line for each gap: PR #8622's Proof section.
| Fact | Today (key path) | Target owner | Who writes it | How often |
|---|---|---|---|---|
| Neighborhood summary | ORG#/LISTING#<id> listing.neighborhood.summary[] | PROP#<home>/KNOWLEDGE knowledge.<fact> | scripts/enrich-listing-neighborhoods.ts + cron drain | once per new address, re-run only on address change |
| Schools | ORG#/LISTING#<id> listing.neighborhood.schools | PROP#<home>/KNOWLEDGE knowledge.<fact> | same enrichment path | same |
| Geo (lat/lng) | ORG#/LISTING#<id> geo{lat,lng} | PROP#<home>/KNOWLEDGE knowledge.<fact> | same enrichment path | same |
| Drive times | ORG#/LISTING#<id> driveMinutesTo{<addressKey>: minutes} | PROP#<home>/KNOWLEDGE knowledge.<fact> | same enrichment path | same |
| Parking | parsed live, on the call, from the listing description | PROP#<home>/KNOWLEDGE knowledge.section.<id> | enrichment path (write-once, migrated off live parse) | currently on every call; target: once, on address change |
| Company sections (contact, screening, fees, pets, snow, line limits) | anchor PROP#western-slope-front-door/KNOWLEDGE knowledge.section.<id> | ORG#<org>/KNOWLEDGE knowledge.section.<id> | manual authoring, onboarding | on policy change |
Tour arrival (tour-arrival, tour-arrival-shared-building) | anchor property, matched by shared-building address text | PROP#<home>/KNOWLEDGE, matched by property id | manual authoring / queued | on tour-arrival policy change |
Sources: registry keys.ts (knowledge block), src/lib/data/store.ts getPropertyKnowledge (~line 4080), src/lib/domain/leasing/tour-arrival-info.ts, scripts/enrich-listing-neighborhoods.ts, src/app/api/cron/neighborhood-enrichment-drain, and the Western Slope go-live tracker's anchor-property row.
| Channel | Step | Result |
|---|---|---|
| 1. Availability | Partial fail — reply composed ("118 Quillwood Lane is available now... rent runs $2,150/mo... Thursday 1, 2, or 3 PM") but bounced, error 550 5.7.708. Gmail message id 1a0a6b8227d8981d; conversation PROP#fairhaven-homes/CONV#d585bafa. | |
| 2-5 | Blocked — never sent, same sending restriction. | |
| Text | 1. Availability | Pass — "118 Quillwood Lane is available now! 3bd/2ba, 1,480 sqft... $2,150/mo." Conversation 78ae3ee7-3c42-4987-9cb6-d33a9aaa7369. |
| Text | 2. Pets | Pass — property's $300 per pet rule beat the company's no-pets rule. |
| Text | 3. Insurance | Pass — $100,000 liability, correctly answered from the company-only rule. |
| Text | 4. Book | Booked Thursday, September 17 at 2:00 PM. Tour TOUR#78ab76e2-c5ac-4f85-9c18-14893a8ebc8e. |
| Text | 5. Move and cancel | Moved to Friday 2:00 PM, then cancelled. Clean history, no leftovers. |
| Phone | 1. Availability | Partial — the real unit and price were quoted correctly, but the call never played out the full scripted scenario. ElevenLabs conversation conv_0601m2kasavgeq7. |
| Phone | 2-3. Pets, insurance | Not run — no bench script covers these by voice yet. |
| Phone | 4. Book | Booked 3:15 PM at 402 Thistledown Court, tour 146d47d5. Quality issues noted above (budget asked twice, stacked questions, stated home count, implied recognizing a first-time caller). |
| Phone | 5. Move and cancel | Reschedule landed on Wednesday instead of the requested day, and the confirmation sentence cut off mid-way. Cancel was clean and the calendar event was removed. |
| Text | Tenant recognition | Pass — recognized Terry Testwell, a seeded test tenant at 146 Quillwood Lane. Conversation ecedc003. A rent question was correctly forwarded to the property manager rather than answered. |
| Phone | Tenant recognition | Not attempted — needs a caller phone number set up first. |