Skip to main content

WiseAI Realtor — Conversation Inbox Expected Output Spec

⛔ STATUS: DRAFT — NOT APPROVED. CLAUDE.md Rule #17 (HARD GATE) is NOT satisfied.

Stage-1 agent research, pre-populated from the realtor design docs + the inbox.html mock. Do not build the customer-facing screen until the founder approves and confirms the open items in a Stage-2 interview.

Sourced from (read-only, 2026-06-29): design-direction.md (Conversation inbox = "3-pane split: filters/saved views | conversation list | thread + context rail (linked record, AI summary, take over from AI)"; transcript/ conversation-view + autopilot-per-conversation components), information-architecture.md (group A "Conversations / Inbox"), data-model.md (re_interactions, re_sms_messages, consent on STOP/HELP), the approved sibling acceptance/ai-front-desk.md (the capture/escalation behaviour this UI surfaces), and realtor-mockups/inbox.html.


0. Scope — what this spec covers, and what it does NOT

This spec covers the Conversation Inbox — the unified workspace where an agent reads AI-handled conversations, sees what the AI escalated, and takes over from the AI mid-conversation across chat + voice + SMS in every language. It is the sibling of realtor-dashboard-mvp.md ("Needs a human" tile deep-links here), realtor-leads-mvp.md, and realtor-contact-record-mvp.md (the context rail links to the full record).

Does NOT cover: the capture / escalation logic itself (when/why the AI escalates, the universal crisis/safety layer, lead writes) — that is the approved ai-front-desk.md (this inbox is the human-side surface of that behaviour); the voice/chat runtime internals (voice-agent-livekit/, api/chatbot/stream); the AI agent configuration screen (a sibling AI-config spec — guardrails, knowledge sources, voices); the full contact record (realtor-contact-record-mvp.md).

Build posture: the local-business platform already has a unified leads inbox (InboxTab.tsx); chat is already multilingual (route.ts) and the RE voice vertical exists. The RE Conversation Inbox extends these into a 3-pane, take-over-capable surface over the re_interactions/re_sms_messages spine — do not rebuild the runtime (do-not-reinvent.md).

Voice-agent guardrail: any change that touches the LiveKit voice agent, SIP, or moderation.py is LIFE-SAFETY tier — owned by the voice-agent-engineer agent, requires independent QA voice review + founder go before any deploy. This inbox is a read/take-over surface; it must not weaken the universal crisis/safety layer (ai-front-desk.md §V7).


1. The mock screen this spec governs + what it reads

Governs: realtor-mockups/inbox.html — a 3-pane layout inside the Workflow-Rail shell:

  • Pane 1 — Views/filters (~196px): saved Views (Needs a human · 4, Unassigned · 7, AI-handled · 31, Assigned to me · 9, All · 47), Channel filters (💬 Chat / 📞 Voice / ✉️ SMS), Language filter (EN · FR · ਪੰ · 中文).
  • Pane 2 — Conversation list (~312px): rows with avatar, name, last-message snippet, timestamp, channel chip + language chip, and an "↗ AI escalated" badge; an AI-handled row shown muted with a "🤖 AI" badge for contrast.
  • Pane 3 — Thread + context rail: the conversation thread (alternating AI vs contact bubbles — "🤖 Aria" labelled — with an inline "Aria escalated: commission is outside its lane (per your AI guardrails). Waiting for you." system row), a thread header with an "AI paused — you're in control" state + Open record + Take over actions, a composer with suggested replies, and a right context rail (Aria's summary, Contact facts: Stage/Score/Source/ Agent, Quick actions incl. "↩︎ Resume AI (Aria)").

Reads (target-state CRM-spine APIs — WS1a; verify exact route names; do not fabricate):

SurfaceUnderlying read
Conversation listre_interactions grouped by contact/conversation (chat_session / call / text), with escalation + assignment state
Channel/language chipsinteraction channel + detected language
Thread (chat/voice/SMS)re_interactions (chat/transcript) + re_sms_messages (SMS thread) for the conversation
Escalation / "needs a human"escalation flag set by the AI (the ai-front-desk.md escalation path)
Context rail — Aria summaryre_contacts.ai_summary
Context rail — contact facts + "Open record"re_contactsrealtor-contact-record-mvp.md
Take over / Resume AIper-conversation autopilot state (+ SMS outbound consent gate via re_sms_messages.consent_checked / re_consent_events)

Demo tenant: Terry & Sheri Real Estate (…0c01). Demo conversations (David Tran / Janet Lowe / Mei Chen / Amrit Brar / Harjit Singh) are demo content; NEVER a real customer (feedback_never_modify_customer_data).


2. The AI-Bridge / honesty anchor (this screen IS the bridge made visible)

Per knowledge/architecture/ai-bridge-principle.md — the inbox is where the "bridge to humans" is operationalized:

  • The AI escalates rather than pretends. Every escalation row states why the AI stopped ("commission is outside its lane, per your AI guardrails") and waits for a human — never bluffs an answer. The RE guardrails (no value/ legal/mortgage/tax advice, no steering — audit P0 #5) are what produce these escalations; the inbox surfaces them honestly.
  • AI vs human authorship is always labelled. Thread bubbles distinguish "🤖 Aria" from the human agent and from the contact; a taken-over thread shows "AI paused — you're in control".
  • Take-over is clean and reversible. Taking over pauses the AI for that conversation; Resume AI (Aria) hands it back. The contact is never double- answered by AI + human at once.
  • Universal crisis/safety is untouched — a personal-crisis disclosure on any RE line still triggers the universal 988/911/DV layer (ai-front-desk.md §V7); the inbox never gates or hides it.

3. Role-based visibility

BucketRolesInbox scope
Brokerage managementbrokerage_owner, broker_admin (brokerage scope)All conversations across the brokerage; "Unassigned" pond; can assign + take over any
Team managementteam_admin (team scope)All conversations within the team; assign + take over within team
Agent / ISAagent, isa (own scope)Own assigned + unassigned pond; "Assigned to me" view; cannot read a peer's assigned private threads (per routing)
Support / externaltransaction_coordinator, marketing_assistant, external_partner (bounded)Read-only or scoped; external_partner (read-only) cannot send/take over

The mock's views ("Assigned to me · 9", "Unassigned · 7") are the agent-scope slices; "All · 47" is a management/whole-tenant slice. Language-based routing (audit P1; Beckett's two agents) may auto-assign by preferred_language.


4. Expected outputs — per pane

4.1 — Pane 1: Views / saved filters / channel / language

Should see:

  • Saved Views with live counts: Needs a human (escalated, not taken over), Unassigned, AI-handled, Assigned to me, All.
  • Channel filter (Chat / Voice / SMS) and a Language filter — the language options are per-tenant/per-agent enabled languages, not a hardcoded set (consistent with the Smart-List language rule in realtor-leads-mvp.md §4.3).
  • Views are dynamic (counts update as conversations are handled/escalated).

Should NOT see:

  • A hardcoded language list unrelated to the agent's enabled languages.
  • A static "AI-handled" view that doesn't update when the AI handles a new one.

Success: the agent can jump to exactly the slice they need ("Needs a human" first thing in the morning), filtered by channel/language.


4.2 — Pane 2: Conversation list (chat + voice + SMS unified)

Should see:

  • One list spanning chat + voice + SMS, each row: avatar, name, last-message snippet (voice rows summarize, e.g. "Seller — wants a callback re timing & staging"), timestamp, channel chip, language chip, and an "↗ AI escalated" badge where applicable. An AI-handled conversation appears muted with a "🤖 AI" badge so the agent can tell what still needs them vs what Aria closed.
  • Selecting a row opens it in Pane 3; the selected row is visually marked.

Should NOT see:

  • Separate inboxes per channel (the value is one unified inbox).
  • An escalated conversation that isn't visually distinguished from an AI-handled one.

Reads: re_interactions (+ re_sms_messages) grouped per conversation with escalation/assignment/AI-handled state.

Success: in one list the agent sees, across all channels and languages, which conversations need them and which the AI already handled.


4.3 — Pane 3: Thread + take-over + composer

Should see:

  • A thread with alternating bubbles clearly labelled contact vs "🤖 Aria" vs human agent, monospace/relative timestamps, and inline system rows for AI events — notably the escalation row ("Aria escalated: commission is outside its lane, per your AI guardrails. Waiting for you."). For a voice conversation the thread shows the call summary + a transcript (with the original-language → English where translated) and a recording link.
  • A thread header showing the conversation's autopilot state — when a human is handling it: "AI paused — you're in control" — plus Open record (→ realtor-contact-record-mvp.md) and a Take over button.
  • A composer with suggested replies (e.g. "Happy to walk you through our commission in a quick call —", "Book a call"), free-text reply, and Send. Nothing auto-sends — the human presses Send (and for SMS, the outbound is consent-gated, §4.5).

Should NOT see:

  • AI and human both replying to the same live conversation simultaneously (taking over pauses the AI for that thread).
  • A suggested reply that asserts a home value / legal / mortgage position (the guardrails apply to suggestions too).
  • A "Send" that fires without the human (no unattended sends — feedback_db_writes_ok_sends_need_blessing).

Success: the agent reads the full thread (any channel/language), sees exactly why the AI escalated, takes over, and replies — with the AI cleanly paused for that conversation.


4.4 — Per-conversation autopilot / take-over / resume (the handoff)

Should see (verify behaviour, not DOM):

  • Take over → the AI is paused for that conversation only; the header flips to "AI paused — you're in control"; the AI will not send further messages on that thread until resumed. (Sample state at ≥2 timepoints — feedback_verify_behavior_not_dom_presence.)
  • ↩︎ Resume AI (Aria) → control returns to the AI for that conversation.
  • The global Autopilot/Co-pilot toggle (dashboard/top bar) sets the default; the inbox provides the per-conversation override (design-direction "autopilot per conversation").
  • A handed-off conversation updates the relevant Views counts ("Needs a human" decrements once taken over).

Should NOT see:

  • A "Take over" that doesn't actually stop the AI (must verify the AI is silent on that thread afterward).
  • A take-over that pauses the AI globally for all conversations (it is per-conversation).

Reads/writes: per-conversation autopilot state; a status_change/handoff re_interaction records the take-over (actor=human) and resume.

Success: take-over and resume durably change which side (AI vs human) answers the thread — confirmed by observing the AI's behaviour after each toggle.


Should see / NOT see:

  • An SMS reply/composer is consent-gated: an outbound SMS to a contact whose SMS consent is withdrawn/expired or who is do_not_contact is blocked with the reason; the gate is enforced before send (re_sms_messages.consent_checked, re_consent_events, audit P0 #3/#6). An inbound STOP/HELP writes a consent withdrawal and is honored immediately (data-model.md §1.9).
  • The inbox never originates an unattended/cron SMS blast (feedback_never_cron_send_campaigns); a human presses Send per message.

Success: STOP suppresses the contact and the inbox refuses further outbound to them; a consented contact can be replied to normally.


4.6 — Context rail (linked record + Aria summary + quick actions)

Should see:

  • A right context rail: Aria's summary (teal accent, the relationship recap for this contact), Contact facts (Stage / Score / Source / Agent), and Quick actions (Book a showing, Send comparables, Resume AI), plus a path to the full record.
  • The rail reflects the same contact as the open thread (linked record), so the agent has full context without leaving the inbox.

Should NOT see:

  • A context rail showing a different contact than the open thread.
  • Commission/financials in the rail for an agent-scope viewer (RBAC, §3).

Success: the agent can answer with full context — summary + stage + history — in the same view as the conversation.


5. Empty / loading / error states

StateExpected output
No conversationsDemo-seeded preview + onboarding nudge ("Your AI front office's chats and calls will appear here once your voice line / chat embed is live") — never a blank 3-pane (anti-pattern #6).
Empty view (0 in a filter)Honest "Nothing in this view right now" with the filter shown + a clear-filter — not blank.
LoadingSkeleton list rows + skeleton thread, not a spinner (anti-pattern #7).
Thread load errorInline "Couldn't load this conversation — retry"; panes 1–2 stay usable.
Voice line / chat not provisionedThe inbox renders with an honest "AI front office not yet connected — Calls/Chats not measured" (mirrors ai-front-desk.md §9), not fabricated conversations.
Suppressed contact / external_partner (read-only) roleComposer disabled with the reason (consent or role).

6. Accessibility (AODA → WCAG 2.1 AA)

  • Landmarks/regions: the three panes are navigable regions; the conversation list is a list with selectable rows (aria-selected); the thread is a log (role="log"/list) with author + time exposed to assistive tech.
  • Take-over state is announced (not color-only): "AI paused — you're in control" is text; the escalation system row reads as text + icon.
  • Channel/language chips convey meaning by text, not color alone.
  • Keyboard: move between panes, select a conversation, focus the composer, Take over / Resume, and Send — all keyboard-operable with visible focus; suggested-reply chips are buttons.
  • Contrast: ≥4.5:1; teal action/links meet AA; gold accent only; the amber escalation row text meets AA over its tint.
  • Motion: typing/skeleton animation respects prefers-reduced-motion.

7. Acceptance checklist (QA runs on the deployed URL)

Behavioural verification on wiseaiagency.com (real host) against the demo tenant (…0c01); never "build passes". Sample at ≥2 timepoints; assert change. Any voice-path change additionally requires voice-agent-engineer review.

// 3-pane + unified list
test.fixme('three panes render: views/filters | conversation list | thread+context rail', () => {});
test.fixme('one list spans chat+voice+SMS with channel+language chips; AI-handled rows muted, escalated rows badged', () => {});
test.fixme('language filter options are the agent/tenant enabled languages, not a hardcoded set', () => {});

// Thread + escalation honesty
test.fixme('thread labels contact vs 🤖 Aria vs human; the escalation system row states WHY the AI stopped', () => {});
test.fixme('a voice conversation shows call summary + transcript (orig→EN) + recording link', () => {});
test.fixme('a suggested reply never asserts a value/legal/mortgage position (guardrails apply to suggestions)', () => {});

// Take over / resume (behaviour, not DOM)
test.fixme('Take over pauses the AI for THAT conversation only (AI silent on the thread after); header shows AI paused', () => {});
test.fixme('Resume AI hands control back; "Needs a human" count decrements on take-over (verify at 2 timepoints)', () => {});
test.fixme('global Autopilot default is overridable per conversation', () => {});

// Consent / sends
test.fixme('outbound SMS to a withdrawn/expired/do_not_contact contact is BLOCKED with a reason; STOP suppresses', () => {});
test.fixme('nothing auto-sends — a human presses Send; no cron/unattended SMS', () => {});

// Context rail + RBAC + safety
test.fixme('context rail shows the SAME contact as the thread (Aria summary + facts + Open record)', () => {});
test.fixme('agent-scope viewer sees no commission/financials in the inbox rail; external_partner (read-only) cannot send/take over', () => {});
test.fixme('a personal-crisis disclosure still triggers the universal 988/911/DV layer (not gated by the inbox)', () => {});

// States + a11y
test.fixme('empty/0-filter/loading/error/not-provisioned states render per §5 (never blank, never a spinner)', () => {});
test.fixme('panes are keyboard-navigable; take-over state announced as text; reduced-motion honored', () => {});

8. Guardrails for agents building against this spec

  • Rule #17 not satisfied — do not build until founder approval.
  • The inbox is the bridge made visible — escalations must state why, AI vs human authorship must be labelled, take-over must cleanly pause the AI per conversation, and crisis/safety must never be gated (ai-front-desk.md §V7).
  • Voice = LIFE-SAFETY — any change touching the LiveKit agent / SIP / moderation.py goes through voice-agent-engineer + founder go; this inbox is a read/take-over surface and must not weaken the universal safety layer.
  • Take-over is per-conversation, not global — verify the AI actually goes silent on the taken-over thread (behaviour, not DOM presence).
  • SMS outbound is consent-gated; no unattended sends — a human presses Send; STOP/HELP withdraws consent immediately.
  • Extend the existing inbox/runtime (InboxTab.tsx, chat/voice) — do not rebuild (do-not-reinvent.md).
  • Migrate-before-usere_*/re_sms_messages are proposals; verify columns via information_schema (Rule #18); founder-gated DDL.
  • Verify on the real host; behaviour not DOM presence; evidence-or-nothing.
  • If code diverges, update the spec first (founder approval), then the code.

End of spec. STATUS: DRAFT — NOT APPROVED. Open items for Stage-2: (1) does the RE MVP include outbound SMS at all (the approved AI Front Desk MVP is email-first / inbound-only — confirm the SMS lane is in scope before building the consent-gated composer); (2) take-over notification (does taking over notify the contact "you're now with a person"?); (3) per-conversation autopilot persistence across sessions.