The Key Question Clara Never Answered
A signed tenant, two days from move-in, asked how she gets her keys — and got a canned "our team has it" text. RCA of the escalation gate that swallowed the question, the feature that should have answered it, and every gap this case chains through.
Incident date 2026-08-18 · Written 2026-08-18 · Status: thread released same day; fixes pending harness results · Conversation 3f859233-f67c-4f51-a53f-3aa411849f0a (Camellia, Hayley Bannochie, unit 612, move-in 2026-08-20)
What happened, in one paragraph
At 04:29 UTC on Aug 18, Hayley Bannochie — approved, lease signed, moving into unit 612 on Aug 20 — texted Clara: "How do I get my key on move in date?" Clara replied with the fixed escalated-thread acknowledgment ("Thanks for your message — our team has it, and someone will get back to you.") and nothing else. The key-pickup scheduling feature, which exists precisely for this question and for which she was fully eligible, never ran: the conversation had been sitting in status: 'escalated' since her Aug 4 lease-date questions, and the escalated-thread gate returns the ack before the agent loop or any tool composition executes. No human was notified that the new question had arrived beyond the generic ack; both PM escalation rows on the thread were still unhandled 14 days after they opened.
Impact
- A real move-in-critical question went unanswered for hours (until a human noticed the thread), two days before move-in. Without intervention it would have gone unanswered indefinitely — nothing in the system re-surfaces a gated inbound.
- No key-pickup appointment was offered or booked; the leasing team received no signal that a mover needed one.
- This is the same resident previously stranded for two weeks by the lease-archive-churn sync bug (fixed via PR #5728, Trello UXHdqQ7k) — her journey has now hit two distinct system failures at the signed-lease stage.
Timeline
| When (UTC) | What happened |
| Jul 18 19:44 | Website inquiry (1-bed, short-term lease, move-in special). Clara answers with pricing and terms. |
| Jul 21 | Tour booked for Jul 27 2:00 PM. Tour-confirmation email + Slack go to the leasing team (pm_tour_confirmation_email in the thread's kinds). |
| Jul 27 | Tour happens; post-tour application nudge sent. |
| Aug 1 19:51 | Hayley: received lease, has term-date questions. Clara forwards to PM (PmEscalationAction pmesc_7580eb2c). This row is still unhandled as of Aug 18. |
| Aug 2 00:15 | Second forward: lease shows 09/01 effective date vs agreed 8/20 move-in, and a 5-month term on a 6-month lease. |
| Aug 3 06:27 | Hayley chases: "I have not had anyone reach out to me yet." |
| Aug 3 14:06 / 22:31 | More forwards: approved, post-approval questions, still no human response; she emails the office directly. |
| Aug 4 01:47 | Lease was re-sent corrected (6-month term). Final forward re: prorated 8/20 move-in + how the 1-month-free special applies (pmesc_9467148f — also still unhandled Aug 18). |
| Aug 4–5 | Conversation ends up status: 'escalated' (last row update 2026-08-05T03:46Z, version 24). A human PM reply exists in the thread (pm_inbox_reply kind), but nothing releases the status. |
| Aug 18 04:29 | "How do I get my key on move in date?" → gate fires → escalated_thread_ack. Agent loop never runs. |
| Aug 18 (day) | Fede spots the thread. Investigation (this doc). Thread manually released to active; takeover reply drafted for approval. |
Root cause chain
RC1 — Escalated status is sticky by design, and nothing ever released it
Conversation.status === 'escalated' is the single source of truth for "humans own this thread" (agents/clara/lib/agent/escalated-gate.ts:35-38). Every other non-active status auto-reopens on the next inbound; escalated deliberately does not (escalated-gate.ts:40-52) — a fix for the 2026-08-03 incident where a tenant's own reply cleared an escalation. The only exit is a deliberate human PATCH to active/resolved (src/app/api/conversations/[id]/route.ts:62-71). There is no timeout, no auto-release on PM reply, and no dashboard nudge for stale escalated threads. A PM replied in this thread and both escalation rows stayed open; the status sat escalated for ~14 days and would have forever.
RC2 — The topic-scope carve-out did not release the turn
The one automatic escape (2026-08-09 decision) releases a turn only when (a) the escalation has a nameable topic derived from the thread's own forward_to_property_manager reason/summary (conversation-manager.ts:1108-1147, escalated-topic.ts:143-170) and (b) a Haiku classifier positively rules the new message a 'separate' topic (escalated-topic-classifier.ts:130-225). Any null topic, 'same', 'unknown', error, or timeout holds the ack — "releasing requires positive evidence… holding requires nothing at all." A key-pickup question from the tenant whose lease was being discussed is exactly the kind of adjacent-but-answerable message the classifier's bias-to-'same' will hold. Open question for the investigating agent: pull the gate decision log / trace for the Aug 18 04:29 turn and determine which branch held it — null topic or classifier verdict — and what the derived topic string was.
RC3 — The gate runs before capability composition, so eligibility can never argue back
The gated branch (conversation-manager.ts:3335-3420) writes the inbound + ack rows and returns before the agent loop, capability composition, or any tool runs. So the facts that would have made this trivially answerable — verified sender, signed lease via pending occupancy, move-in inside the 30-day window, keyPickupTools granted by the INCOMING_RESIDENT capability (capabilities/incoming-resident.ts:49-61) — were never consulted. There is no carve-out for "clearly answerable, move-in-critical intents." That is fail-safe by design, but this case is the cost made concrete.
RC4 — The gated ack is a dead end for everyone
When the gate fires, the tenant gets a content-free template (buildEscalatedAckText, conversation-manager.ts:956-961) and the team gets nothing new — no email, no Slack, no re-notification that a gated thread received another inbound. Combined with both PmEscalationAction rows sitting unhandled since Aug 1/Aug 4, the message had no path to a human. The reminder cadence tied to those rows either did not fire or did not land — second open question for the investigating agent (check the escalation-reminder cadence state for pmesc_7580eb2c / pmesc_9467148f).
What should have happened
Key-pickup scheduling shipped 2026-08-07 (PR #5547; 15-minute events via #5713). Eligibility (src/lib/domain/move-in/key-pickup.ts:281-401) re-checks fail-closed on both calls: verified person → executed or pending lease → PMS move-in date → within −30/+7 days of move-in → office hours + connected leasing calendar. Hayley passed every gate. The prompt rule (MOVE_IN_KEY_PICKUP_PROMPT) treats "keys or move-in-day logistics" as an immediate get_key_pickup_slots trigger. Had the turn reached the agent loop, Clara offers real slots, books via signed slot-token, writes the Outlook event ("Key pickup — Unit 612 · Camellia Apartments"), and confirms truthfully.
Known gaps this case chains through (from the original build history)
| # | Gap | Status |
| 1 | No fabrication protection. Aug 13 journey-harness caught Clara inventing key-pickup confirmations on voice + email ("Key pickup is confirmed for Thursday, August 20th at 9:30 AM" — no booking existed). The honesty guard (#5731) over-blocked real tour replies, broke prod, and was fully reverted Aug 15 (#5790/#5791) with its follow-up fix (#5767). The post-RCA guard registry (#5796) deliberately excludes it. Verified absent at repo tip. | OPEN — deliberate, unreinstated |
| 2 | Email lane never fires key-pickup tools even with a visible lease (SMS + voice book fine). Named blocker "(B2)" in the Aug 14 overnight-run notes. | OPEN |
| 3 | Slot freeness not re-read at booking — two movers offered the same slot inside the 10-min token TTL can both book it. Documented "KNOWN LIMIT" in handle-book-key-pickup.ts:22-30; same gap exists in tour booking; fix belongs in a shared re-check. | OPEN — consciously deferred |
| 4 | AppFolio→PropFlow occupancy-mirror lag blinds the just-signed stage ("(B1) THE highest-leverage journey fix"). Sync cadence tightened 15→5 min (#5753); upstream reporting lag remains. | PARTIAL |
| 5 | No team email for key-pickup bookings. Tours send pm_tour_confirmation_email + Slack (send-pm-tour-confirmation.ts:84-250, fired from apply-tour-intent.ts:449/515/708). Key pickup writes only the calendar event — no email, no Slack, no registered outbound kind. Never discussed or deferred; simply out of scope for #5547. Fede has now asked for it (2026-08-18). Build plan: mirror send-pm-tour-confirmation.ts (recipient via resolvePropertyNotificationEmail, new template + pm_key_pickup_confirmation_email kind in pm-internal-kinds.ts/sent-as.ts, fire-and-forget after recordKeyPickupEvent in handle-book-key-pickup.ts:~355). | NOT BUILT — approved to build after harness |
| 6 | Camellia go-live bar unmet. The bar: "key pickup green all 3 channels, zero fabricated confirmations, zero wrong in signed stage, correct ≥2× baseline 47→95+." Last measured run (Aug 14): flat vs baseline; correct answers 47→41. | OPEN |
Remediation
Done (2026-08-18)
- Thread released:
PATCH /api/conversations/3f859233… {"status":"active"} — verified status: active in DDB.
- Takeover reply to Hayley drafted for Fede's approval (manual-outbound rail; answers the key question and invites her to book a pickup time, which Clara can now handle on the released thread).
Decided (Fede, 2026-08-18)
- Comprehensive end-to-end retest harness first: inquiry → tour → application → signed lease → escalation → key question, across SMS/email/voice, asserting: pickup booked with a real slot, truthful tenant confirmation, team notified, and the new scenario — a key question on a still-escalated thread must not vanish.
- Team key-pickup email built after the harness baseline exists (gap #5 above).
- Fixes for the sticky-escalation design and the reverted honesty guard wait for harness results — not yet in scope.
For the investigating agent — open questions, in priority order
- Which gate branch held the Aug 18 turn? Find the gate decision log/trace for conversation
3f859233… at 2026-08-18T04:29Z: was escalationTopic null (unnameable escalation), or did classifyTopicMatch return 'same'/'unknown'/error? What topic string was derived from the Aug 4 forward_to_property_manager row? This determines whether the fix is topic-derivation, classifier prompt, or policy.
- Why are both PmEscalationAction rows (Aug 1, Aug 4) still unhandled? A
pm_inbox_reply exists in the thread — did a human answer without the rows being marked handled (the 2026-08-12 reminder-chasing incident pattern), and did the reminder cadence fire at all between Aug 4 and Aug 18?
- How many other prod threads are sitting escalated > N days with later inbounds? Query conversations with
status = 'escalated' and lastMessageAt after the escalation point — this case is unlikely to be unique. Each is a tenant currently being stonewalled.
- Does the takeover after-care standing rule (re-escalate after every takeover send) interact badly with RC1? The skill escalates threads by default and relies on humans remembering to release — the same sticky state, minted from a different door.
- Design options for a release path (for a later decision doc, multiple-choice per the docs standard): auto-release on PM-marked-handled; stale-escalation dashboard surfacing/nudges; a move-in-critical intent carve-out that composes capabilities read-only before deciding to hold; classifier prompt tuning with the replay harness (#5796) as the merge gate.
References
- Conversation
3f859233-f67c-4f51-a53f-3aa411849f0a · person pers_c09787e2-0a01-402d-b169-3ad3b66fbc17 · property 1773625953462 (Camellia) · escalation rows pmesc_7580eb2c…, pmesc_9467148f…
- Gate:
agents/clara/lib/agent/escalated-gate.ts, escalated-topic.ts, escalated-topic-classifier.ts, conversation-manager.ts:3283-3420 · exit: src/app/api/conversations/[id]/route.ts:62-71
- Key pickup:
agents/clara/lib/agent/tools/key-pickup.ts, src/lib/domain/move-in/key-pickup.ts, src/lib/tools/handlers/handle-{get-key-pickup-slots,book-key-pickup}.ts, src/lib/domain/calendar/sync-key-pickup.ts · tests key-pickup-gauntlet, key-pickup-window, sync-key-pickup, calendar-read-failure · voice scenarios key-pickup-move-in.yaml, routing-mover-key-pickup.yaml
- Tour team email (template to mirror):
src/lib/domain/leasing/send-pm-tour-confirmation.ts, pm-tour-confirmation-template.ts, pm-internal-kinds.ts
- PRs: #5547 build · #5713 15-min events · #5728 lease-archive churn · #5731/#5747/#5767 honesty guard (reverted via #5790/#5791) · #5753 sync cadence · #5796 guard merge gate · journey harness
scripts/journey-harness/