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 UIThe default surface
TUISame canonical as web
Telegram DMGroups are separate
WhatsApp DMGroups are separate
Slack DMIn-channel mentions are separate
Discord DMServer channels are separate
TeamsDMs only
LINE
SignalVia signal-cli
iMessageVia Blue Bubbles
Facebook Messenger
Matrix1:1 rooms
Mattermost DM
Feishu / LarkDMs
DingTalkDMs
WeCom
Zalo
WebchatEmbeddable widget
EmailThread-per-subject, own session
SMSOwn session
GitHubIssues + PRs are not conversations
WebhookCustom 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.