Why Ask Clara told Sean that 4 of Camellia's top 5 problems are renewals

Investigated 2026-08-03 · status: root cause confirmed, fix proposed

Verdict: the answer was wrong, and it wasn't the chat model's fault. The nightly "portfolio signals" snapshot that Ask Clara reads flags nine Camellia tenants as renewal risks, each partly because of "30+ days since last conversation." That silence claim is false for at least four of the nine — they had renewal calls and texts with Clara on August 1–2, the day before Sean asked. The snapshot can't see those conversations because it joins on the wrong ID.

What Sean saw

Asking Ask Clara "top 5 problems for Camellia" returns one tour-conversion finding and four renewals findings. Reproduced on prod 2026-08-03 with the same snapshot (reportDate 2026-08-03): the Camellia-scoped snapshot holds 10 findings — 9 renewals, 1 leasing. Ask Clara faithfully summarized what it was given.

Root cause 1 — a broken identity join fabricates the "silence" factor

The renewal-risk signal (src/lib/domain/insights/signals/renewal-risk.ts) counts a tenant as risky when 2+ factors converge, one of which is silence: no conversation in the last 30 days. It looks up conversations by person-spine ID (pers_… UUIDs). But all 149 Camellia tenant records carry no spine personId — their leases still use legacy numeric IDs (e.g. …4620062). The lookup can never match, so every single Camellia lease inside the renewal horizon is automatically "silent", regardless of what Clara has actually been doing.

Proof from prod, 2026-08-03 — tenants the snapshot calls "30+ days silent" with their real last conversations (all topic renewal):

Tenant (legacy ID)Snapshot claimActual last conversation
…462005230+ days silentAug 2, voice + SMS
…462000530+ days silentAug 2, voice + SMS
…462000930+ days silentAug 1, SMS
…462005130+ days silentAug 1, SMS

Conversations DO carry the legacy tenantId — the data to join correctly exists; the signal just doesn't use it. (Checked the 100 most recent Camellia conversations; the remaining five flagged tenants had no conversation in that window, so their silence flag may be genuine.)

The same broken join is why the findings read "tenant 1773625953462…" instead of a name — the name lookup goes through the same missing spine ID.

In plain terms

The nightly report decides "we haven't talked to this tenant in a month" by looking them up in the conversation log under a new ID system. Camellia's tenants are still filed under the old ID system, so the lookup finds nothing and everyone looks ignored — even the tenants Clara called and texted about their renewal yesterday.

Root cause 2 — renewals structurally flood the top-5

Even with a correct join, the ranking favors renewals: the renewal-risk signal emits one finding per lease at the highest priority of any signal (90), while tour conversion, vacancy, lead-funnel etc. each emit one aggregated finding at priority 45–80. Ten per-lease renewal rows vs one row per other problem means "top 5" will almost always read as a renewals list. The second factor feeding it — an outstanding balance — fires at a floor of just $50.

In plain terms

Renewals get one line on the list for every tenant; everything else gets one line total. Ask five things off the top of that list and you'll mostly hear about renewals, whatever is actually going on at the property.

What was right in the answer

The #1 finding — 8 of 8 tours in 14 days produced zero applications — checks out against the live tour data and deserves the attention. And some balances are real (one tenant owes $4,135); the delinquency half of the renewal story is worth a look even though the "no outreach" half is wrong for at least four tenants.

Proposed fix

  1. Join fallback (bug fix): in the renewal-risk signal, resolve last-contact by spine personId or legacy tenantId — conversations already carry both. Same fallback for the tenant-name lookup.
  2. Ranking (design tweak): aggregate per-lease renewal findings into one cluster finding per urgency band (28d / 58d / 89d) so the top-5 stays mixed, and/or raise the $50 balance floor.

Item 1 is a contained code fix; item 2 changes what the ranked list means and is worth a quick founder call before shipping.

Related: today's Ask Clara voice demo failure was a separate bug — the site-wide Permissions-Policy header shipped microphone=(), blocking the mic on every page with no browser prompt. Fixed in PR #5352 (microphone=(self)).

PropFlow Docs