Proposed — pending Fede's review

Clara kept replying after escalation — root cause & decisions

Incident date 2026-08-03 · Doc published 2026-08-03 (evening) · Evidence: prod conversation records, full-corpus sweep (771 conversations), code recon on origin/main a5f9d218f

What a tenant experienced. A verified tenant finalizing a new lease asked the same lease questions repeatedly over several days. Her thread was forwarded to the property team five separate times — and after every forward, Clara kept answering anyway: 11 replies after the first escalation, culminating tonight in Clara inventing an explanation of how the "1 month free" special is structured ("a rent concession applied across the lease term") immediately after saying the leasing team would confirm. Tonight's email turns were also appended into the wrong conversation — the container minted by a 13-second phone call — and relabeled as a renewal, though she is a new lease.

Blast radius (full prod sweep, all 771 conversations)

PersonWhat happenedWindow
Hayley B. (tenant)5 escalations, 11 post-escalation Clara replies; email turns misthreaded into her voice-call container; fabricated concession mechanics; miscategorized renewalJul 21 – Aug 3 (predates #5304)
Jayson E. (prospect)Escalated Aug 3; Clara then kept going and booked a tour (confirmed Aug 4, 10:00 AM) on the human-owned threadInside #5304 window
Paula D. (tenant)Email reply misthreaded into her SMS renewal container — misthreading is not limited to voice shellsInside #5304 window

Totals: 14 post-escalation replies (6 since #5304 merged), 7 repeat escalations, 2 misthreaded containers, 3 affected humans. No other conversations affected.

Root cause — five stacked failures

This is not one bug. Five mechanisms, none new tonight, stacked:

  1. Escalation is erased by the tenant's next message. findOrCreateConversation reopens any non-active conversation to active on every inbound (conversation-manager.ts:1648). The forward tool's comment claims the escalated status "prevents Clara from continuing to act unilaterally" — nothing has ever enforced that. This is the deepest defect and it predates the recent fixes: Hayley's July 21 post-escalation reply proves it.
  2. The active-thread bypass widened exposure. The Dario fix (#5304, Aug 2; extended #5320/#5337) routes any inbound from a sender with an open conversation straight into the reply loop, skipping triage — and deliberately treats escalated as active. Correct goal (never drop an in-thread reply), missing distinction (a human-owned thread is not Clara's to answer).
  3. Conversation selection is channel-blind and recency-biased. Two co-located pickers — the bypass (most-recent-activity wins) and selectCanonicalConversation (active beats escalated, then recency) — let a fresh 13-second voice shell outrank a 60-message email thread. Voice mints a new container per call attempt, while the phone is still ringing, so shells constantly compete for "most recent".
  4. The topic classifier then cements the error. Thread-scoped classification anchors on the container's existing tags ([already: renewal]), so misfiled email turns inherit renewal, the conversation re-derives its topic set from them, and the wrong context feeds the next reply.
  5. The last line of defense is a prompt rule, post-hoc graded. RULE 9 ("nothing after forward_to_property_manager beyond the routing ack") is prompt-level; the rationale grader caught tonight's violation after the send and blocked nothing.
There is also no human-side mechanism at all: no resolve button, no status control, no reply-as-Clara in the dashboard; "Mark handled" only silences the reminder cadence; a PM replying to the escalation email lands in Clara's generic inbox with no bridge back to the tenant thread. The team literally cannot take over a thread today — which is why "escalate once and let the human take over" has never been an enforceable state.

Fix architecture (what the fleet builds once decisions land)

PR 1 — escalated-thread gate. One code gate at the single choke point every text channel flows through (handleIncomingMessage, alongside the existing runaway-ceiling guard, which proves the shape: persist the inbound, touch no LLM, send nothing). Make escalated sticky — the auto-reopen becomes conditional on an explicit release. Exempt PM dashboard queries; leave life-safety pre-empts and slot-pick confirmations above the gate untouched. The Dario invariant is preserved: the message is always captured and always reaches a human; what's removed is only Clara's unilateral reply.

PR 2 — deterministic conversation selection. Same-channel preference + escalated-aware ranking in both pickers; email can never append into a voice shell. Audit the other first-match callers found in recon (PM-thread reply target, renewal deep-link fallback, outreach cooldown).

PR 3 — release mechanism. Whatever Decision 2 selects, wired through the existing (currently caller-less) PATCH /api/conversations/[id].

PR 4 — the harness. Multi-turn eval reproducing this incident end to end: escalate → tenant replies → assert silence + PM surfaced; Dario case still answered; channel/container assertions; topic assertions; grounding assertion on concession language. Runs on the subscription runner; gates the merges.

Decisions

Decision 1 — What happens when a tenant writes into an escalated thread?

Decision 2 — How does a thread leave escalated (so Clara can serve that person again)?

A. Human release via "Mark handled" / "Resolve"

Not taken: depends on the team actually clicking it, and a forgotten thread stays dark. Revisit if terminal proves too blunt in practice.

C. Auto-release after N days

Not taken: a timer that wakes Clara mid-human-conversation recreates this incident's failure mode on a schedule.

Decision 3 — Repair the misthreaded data (Hayley B., Paula D.)?

B. Fix forward only

Consequence: zero prod-write risk, but both tenants' threads stay split/mislabeled, the dashboard keeps showing the wrong story, and the topic classifier keeps re-anchoring on the wrong container.

Decision 4 — Merge policy for the behavior PRs (gate + selection)

B. Auto-merge on green tonight

Consequence: hole closes before morning traffic; you review the deployed diff after. An agent-behavior change ships without your eyes on the final diff.

Decision 5 — Jayson E.'s post-escalation tour booking (Aug 4, 10:00 AM)

B. Team re-confirms with the prospect

Consequence: safest against a booking made with wrong context, but an extra touch on a prospect for what looks like a correct booking.

What's already done (no decision needed)

PropFlow Docs