0070 — Clara recognizes when a conversation is over (end_conversation)

Context

Clara always takes the last word on SMS. A tenant ends the exchange — "No that is all", "Ok I'll do that as soon as I'm back from work" — and Clara replies with another warm sign-off ("take care, reach out anytime!", "no rush, just text me"). A human would let those rest. The result is unnatural chattiness, Clara always getting the final message, and extra outbound SMS (cost + mild annoyance).

A read-only investigation across the inbound stack found two layers to the cause:

  1. No skip-gate on SMS. Every inbound SMS runs the full Claude agent loop and returns a non-empty reply. The only path that sends nothing is the runaway-ceiling guard (≥40 consecutive automated replies → returns '', which the dispatcher suppresses). Email already has a real gate (src/lib/integrations/email/response-policy.ts shouldRespond'respond' | 'skip'); the SMS/renewal path has no equivalent.
  2. No closing protocol on SMS. The voice delivery rules (getSpokenRules in clara-delivery.ts) include a "CALL CLOSING" block; the SMS rules (getPlaintextRules) have none, so on a closer Clara fills the silence.

Crucially, the send-suppression mechanism already exists: agents/clara/lib/messaging/process-envelope.ts (~line 239) does if (!reply || !reply.trim()) { return; } — when the agent loop returns empty, nothing is dispatched. The runaway-ceiling already uses this by returning ''. So the missing piece is a deliberate way for Clara to choose silence — not new delivery plumbing.

The danger: going silent while a duty is undischarged. The highest-risk case is a renewal decline where Clara has not yet pushed the official Notice to Vacate — the exact Verity Pretendish (Unit 603, 2026-06-24) failure mode. Silence must never strand an obligation.

Decision

Add an explicit end_conversation tool that Clara calls when she judges the conversation is naturally over, following the same pattern as her other decision tools (renewal_accepted / renewal_declined / renewal_escalate).

When the tool fires, its handler:

  1. Returns an empty reply to the agent loop, so the existing process-envelope suppression sends nothing (no new send-path).
  2. Marks the conversation resolved (status = 'resolved', selfServeResolved = true) — the same end-state detectSelfServeResolution already produces for maintenance, so the next inbound opens a fresh conversation cleanly.
  3. Signals the Temporal renewal workflow (when an open renewal saga exists) so the D+2 / D+9 reminder ladder stops — mirroring the leasing reply-bridge (signalProspectReplied). Without this, Clara goes quiet but the system keeps nagging via reminders.

Clara is taught (prompt) to call end_conversation instead of emitting another sign-off when the tenant's last message is a pure closer with nothing pending.

Fail-open guardrail (non-negotiable)

The tool is refused (Clara is told to respond instead of going silent) whenever a duty is undischarged. The check uses signals already computed in conversation-state.ts:

Must-respond condition Detection signal
Decline captured but official NTV not yet pushed theme.toolCalls has a successful renewal_declined AND the last assistant message lacks NTV routing language (ntvSignInInstructions / "notice to vacate" / portal link)
Acceptance captured but signing path not yet delivered successful renewal_accepted AND no follow-up portal/signing instruction
Clara asked an open question theme.openQuestionFromClara is non-null
Unkept "I'll text you X" promise prior assistant turn promised an SMS but the matching tool (send_offer_sms / send_portal_link_sms / renewal_escalate) did not fire
Pending work-order / photo action computeActionSignal() returns non-null
Ambiguous / sarcastic / non-leaseholder renewal hard rules #9/#10 → escalate, never silent

Default is fail-open: if in doubt, Clara responds. Silence is only for a pure closer after every obligation is met.

This ADR proposes no new export interface (the tool's input is a small inline schema), so the entity-classification table is omitted.

Consequences

Alternatives considered