| Person | What happened | Window |
|---|---|---|
| Hayley B. (tenant) | 5 escalations, 11 post-escalation Clara replies; email turns misthreaded into her voice-call container; fabricated concession mechanics; miscategorized renewal | Jul 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 thread | Inside #5304 window |
| Paula D. (tenant) | Email reply misthreaded into her SMS renewal container — misthreading is not limited to voice shells | Inside #5304 window |
Totals: 14 post-escalation replies (6 since #5304 merged), 7 repeat escalations, 2 misthreaded containers, 3 affected humans. No other conversations affected.
This is not one bug. Five mechanisms, none new tonight, stacked:
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.escalated as active. Correct goal (never drop an in-thread reply), missing distinction (a human-owned thread is not Clara's to answer).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".[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.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.
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.
Clara never replies on an escalated thread, on any channel. Whether the team gets an extra notification depends on where the message arrived:
Email: capture only, no escalation bump — escalations themselves are delivered by email, and the tenant's reply already lands in the shared property mailbox the team reads; a forward on top of it is spam in the same inbox.
SMS / voice: capture + bump the existing escalation — these channels are invisible to the team unless surfaced, so the new message must reach them.
Implementation principle (Fede): the rule is not "email never bumps" — it's suppress the bump when the inbound channel is one the team already sees via the escalation delivery path. Today escalations deliver by email, so email replies are the quiet case; if escalation delivery expands (e.g. text), the bump logic follows the delivery config, not a hardcoded channel.
Consequence: the tenant hears nothing until a human replies — the cost of making "the team will follow up" true. The bump (where it fires) keeps the wait visible to the team.
The thread stays human-owned forever. There is no release path, no resolve control to wire, and no timer. A genuinely new topic starts a fresh conversation, which is what keeps this from stranding the person: the escalated thread is closed to Clara, the tenant is not.
Consequence: an escalated person permanently loses Clara on that thread — heavy for a one-question escalation, and accepted deliberately. Terminal is the smallest thing that cannot fail: every release mechanism is a way for Clara to resume at the wrong moment, which is exactly what happened here. We ship the version with no such mechanism, watch how often a terminal thread is the wrong answer, and add release only if the evidence asks for it.
What this deletes: no status-flip endpoint work, no Mark-handled wiring, no auto-release sweep. The gate reduces to one question at the inbound chokepoint — is this conversation escalated? — with no second state machine behind it.
Not taken: depends on the team actually clicking it, and a forgotten thread stays dark. Revisit if terminal proves too blunt in practice.
Not taken: a timer that wakes Clara mid-human-conversation recreates this incident's failure mode on a schedule.
No message-mover primitive exists today. Build one: version-checked writes, scoped to the named conversation ids, dry-run output reviewed in-session before execution; re-stamp topics and re-escalate the canonical thread.
Consequence: a new class of prod write — mitigated by dry-run + scoping + review. Leaves the record clean for the team actually handling these two tenants.
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.
Fleet takes both PRs to CI-green + bot-reviewed + harness-proven, then stops. You review and merge.
Consequence: the live hole stays open until you review (Clara still replies post-escalation until deploy). Given all affected threads are being watched and it's night hours, exposure is low.
Consequence: hole closes before morning traffic; you review the deployed diff after. An agent-behavior change ships without your eyes on the final diff.
The booking is real and benign; the team gets a heads-up (internal note/Slack) that it was made on an escalated thread so they own the relationship from here.
Consequence: none for the prospect; team briefly context-switches.
Consequence: safest against a booking made with wrong context, but an extra touch on a prospect for what looks like a correct booking.