Your Qor has one chat. Full stop. That chat accepts messages from every chat-family surface — web UI, TUI, Telegram, WhatsApp, Slack DM, and more. Each message keeps a channel tag; per-surface views filter to their slice.
This page is the practical companion to architecture/one-qor-one-chat. That page explains the why; this one explains the how for operators.

What it looks like

Unified canonical chat with channel badges
In the web UI and TUI you see every message. On Telegram you only see messages tagged telegram. On WhatsApp you only see messages tagged whatsapp.

Which channels merge

ChannelMerges into canonical?Notes
Web UI✅The default surface
TUI✅Same canonical as web
Telegram DM✅Groups are separate
WhatsApp DM✅Groups are separate
Slack DM✅In-channel mentions are separate
Discord DM✅Server channels are separate
Teams✅DMs only
LINE✅
Signal✅Via signal-cli
iMessage✅Via Blue Bubbles
Facebook Messenger✅
Matrix✅1:1 rooms
Mattermost DM✅
Feishu / Lark✅DMs
DingTalk✅DMs
WeCom✅
Zalo✅
Webchat✅Embeddable widget
Email❌Thread-per-subject, own session
SMS❌Own session
GitHub❌Issues + PRs are not conversations
Webhook❌Custom shape
Groups (any channel)❌Multi-party, group-scoped session

The data model

-- One canonical session per (tenant, agent, user)
CREATE TABLE sessions (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  agent_id UUID NOT NULL,
  user_id VARCHAR(255),
  channel TEXT,                -- "unified" or specific for non-chat-family
  messages JSONB DEFAULT '[]',
  -- ...
  UNIQUE (tenant_id, agent_id, user_id, channel)
);

-- Messages (inside the JSONB array) carry their own channel:
-- [
--   {"role":"user", "content":"...", "channel":"telegram", "sender_name":"Priya", "timestamp":1776787200000},
--   {"role":"assistant", "content":"...", "channel":"telegram", "timestamp":1776787210000},
--   {"role":"user", "content":"...", "channel":"web", "timestamp":1776787300000}
-- ]
The sessions.channel column is set to "unified" (or "web" for legacy installs — both work). Each message inside the JSONB array has its own channel field. An index on (session_id, (messages ->> 'channel')) keeps per-channel reads fast.

Per-channel reads

When Telegram asks “show me the last 10 messages”, Qorven runs:
SELECT jsonb_agg(m ORDER BY (m->>'timestamp')::bigint)
FROM (
  SELECT jsonb_array_elements(messages) AS m
  FROM sessions WHERE id = $1
) x
WHERE m->>'channel' = 'telegram'
LIMIT 10;
Web UI reads everything (no filter). Single source of truth, multiple views.

What about group chats

Groups are fundamentally different — multi-party, shared context between people, often no single user to bind to. Qorven handles them with group-scoped sessions:
  • One session row per (tenant, agent, group_id)
  • Memories tagged with that group’s scope never leak into the Qor’s 1:1 chats
  • Memories tagged with the Qor’s personal scope never leak into groups
  • See Groups vs DMs →

What about email

Email is its own thing:
  • Threads are real (users reply to specific messages)
  • Subjects matter (Qor tracks the subject as part of the thread)
  • Attachments happen
  • Latency is minutes to days, not seconds
So email gets one session per thread, not merged with chat. The Qor’s memory still sees email content (so context carries across), but the conversation shape is preserved. Email channel →

Operator migration

If you upgraded from an old multi-session Qorven, run:
sudo qorven collapse-sessions --dry-run    # preview the merge
sudo qorven collapse-sessions               # apply
This:
  1. Finds every Qor with >1 chat-family session
  2. Picks the most-recently-updated as canonical
  3. Merges all other messages into it, preserving channel tags
  4. Dedupes on (role, timestamp, content) so re-runs are safe
  5. Emits a memory digest per archived session so nothing’s lost
  6. Archives the old rows (status='archived') — not deleted
Full command reference →.

Where next

Groups vs DMs

How multi-party chats stay separate.

Channel routing

Override the default Qor per sender / keyword.

Memory system

How memory stays coherent across channels.

collapse-sessions

Migrating old fan-out sessions.