Personal triage
"i would like you to pre-triage all my emails and imessage and whatsapp and show me categories such as where i should reply, where i should take action or decide, the threads i am working on" · "especially triage out what you consider spam or uninteresting commercials, what you consider important to read, where action is required" · "this adds the most value to my life" · "try to distinguish between what's important for me vs what i can ignore" · "maybe add tags? similar to github issues?" — the maintainer, 2026-10-13.
Status: building — the live Outlook source, its triage verdicts and the opt-in Outlook copy are built (below). Decided with the maintainer on 2026-10-13: the triage model is the instance's EU model catalogue; messages are read live from their provider, never replicated ("it should take from graph, not necessarily replicate"); the side menu shows the triage on top, the kinds (sources) below.
What is stored, and what is not
| stored in the mesh | read live from the provider | |
|---|---|---|
| the message (body, attachments) | never — unless the person switches the opt-in copy on (below): then the body, never the attachments | yes — opened and searched through the provider |
| the VERDICT | yes — {user}/_Triage/{provider}-{messageId} |
— |
A verdict holds only what the inbox row needs and what the triage decided:
| field | meaning |
|---|---|
provider |
an open string: outlook, gmail, imessage, whatsapp |
messageId |
the provider's own IMMUTABLE id — how the verdict is joined to the live row |
from, subject, receivedAt |
the row (receivedAt is the row's time — InboxQueryLeg.TimeField) |
webLink |
the message in its provider's own client — offered as Open in … on the reading view, never where a click goes |
importance |
an open string: important or ignorable |
tags |
open strings, several per message (below) |
reason |
one line: why this verdict — shown on the row's tooltip |
model, triagedAt |
which model decided, and when |
correctedBy, correctedAt |
the person who last corrected importance or tags (null while they are the model's) — a person's edit is never overwritten |
Importance and tags
Importance answers the one question that matters most: is this for me, or can I ignore it? Ignorable — spam, promotions, newsletters, automated notices nobody asked for — is out of sight by default, one switch away.
Tags are GitHub-style labels, OPEN strings (never an enum — a package or a person adds their own), several per message:
| tag | the message… |
|---|---|
act |
asks you to do something |
decide |
needs your decision or approval |
reply |
waits for your answer |
read |
is important to read, nothing to do |
working-on |
belongs to a thread you are active in |
waiting |
you replied; the other side owes the answer |
fyi |
is informative, nothing owed |
promo |
is commercial |
spam |
is unwanted |
| topic | the agent proposes topics — a client, a project, billing, travel |
The flow
new message (Graph change notification / poll) ──▶ triage job (the user's delegated credential)
──▶ the EU model: importance + tags + reason ──▶ verdict node {user}/_Triage/{provider}-{id}
──▶ the personal inbox: a leg over {viewer}/_Triage, filtered by importance and tags
- Reading uses the person's OWN delegated credential (
IEaGraphAuthfor Outlook — the Executive Assistant's; see ExecutiveAssistantCredentialReads). Nothing is read with an application credential, and the triage sees only what the person can. - Thread context: the triage reads the message and its conversation's earlier messages,
including the person's own replies — that is what tells
working-onandwaitingapart. - Corrections teach: a tag the person adds or removes is kept (
correctedBy), and the last corrections are given to the model as examples on the next run. - Idempotent: one verdict per message (its id is derived from the provider's message id); a redelivery is refused, never duplicated — the inbound lane's claim rule.
In the inbox
- The side menu: Important first, then each tag with its count, then the kinds (Mail, Conversations, Approvals, …). Ignorable is one entry at the bottom.
- Select many and act — shift-click a range, cmd/ctrl-click to toggle — and the command row offers the actions every selected message has in common: Archive, Delete, Mark read, Tag, and for approval requests, Approve and Reject. Actions call the provider (Graph's move, delete, mark read).
- Each row shows its provider's symbol; a provider's switch is three-way: only this, show, hide.
- Search goes to the provider's own search (Graph
$search), merged with the verdicts.
Built: live Outlook source
The person's Outlook Inbox is a source of the unified inbox (/{user}/Inbox), read live — nothing
is replicated. It ships in the MeshWeaver.Mail.MicrosoftGraph module (Inbox/):
| piece | what it does |
|---|---|
OutlookMailboxQueryProvider |
answers the reserved namespace {user}/_Mailbox/outlook (nothing is stored there) by calling Graph GET /me/mailFolders/inbox/messages — $top = the query's limit (at most 100), $orderby=receivedDateTime desc, and only id, subject, from, receivedDateTime, isRead, webLink, conversationId, bodyPreview. The LIST never reads a body; the reading view reads one message's body when it is opened (below). |
| search | the inbox's search box becomes Graph's own $search. Graph refuses $search together with $orderby, so a search asks without it and the rows are ordered newest first here. |
| one message → one node | {user}/_Mailbox/outlook/{id} (the id base64url-encoded), node type Mailbox/OutlookMessage, title = subject ((no subject) when empty), content receivedAt (UTC), from (display name, else address), fromAddress, isRead, webLink (the reading view's Open in Outlook), conversationId, preview. |
| the leg | source Outlook 📧, category Mail, band NeedsYou, timed by receivedAt, anchored on the viewer's own partition. Listed only for a viewer who CONNECTED a mailbox (below). |
| the account | every row carries account — the address of the mailbox it reached — which core's inbox shows in its Account column (owner, 2026-10-15: "we have many emails => show which mail account it is"). The leg reads the PRIMARY mailbox, so that is its address; a grant written before addresses were recorded has none, and the column stays empty rather than inventing one. The address is read beside the messages from the stored grants (IEaGraphAuth.GetMailboxes); a read that fails or takes over 5 s answers the rows without it (logged). |
| connected mailboxes | {user}/_Mailbox/outlook-accounts/{accountId}, node type Mailbox/OutlookAccount — one node per mailbox whose grant is not positively not connected, primary first; content accountId, address, primary, never a token. A sort: on name, path or id orders the nodes BEFORE the limit; any other sort is refused by name. Answered from the stored grants, never from Graph, and only to the owner. An empty answer means nothing is connected (also on a host with no Microsoft connection configured); a grant list that cannot be read is an error, never an empty list. |
| Open (the reading view) | a click on a row ALWAYS opens the message INSIDE the portal — core's inbox reading view, /{user}/Inbox/{item} (owner, 2026-10-16: "when clicking, it should always give me a memex view with the content, not redirect me"). OutlookInboxItemReader reads it LIVE when it is opened: Graph GET /me/messages/{id} with Prefer: outlook.body-content-type="text" (and the immutable-id preference), $select=id,subject,from,receivedDateTime,body,webLink. The view shows subject, sender, the account, the time in the viewer's zone and the body as plain text (an HTML answer is stripped); Open in Outlook (webLink) is a secondary link under it. Nothing is stored; only the owner is answered; a message Graph no longer has, or a mailbox not connected, is said. |
| Delete / Undo | OutlookInboxItemHandler: Delete is Graph's move to Deleted Items, Undo moves it back to the Inbox. Nothing is written to the mesh. Every call asks Graph for immutable ids, which survive a move, so the row's path still names the message after Delete; the id a move returns is remembered too and preferred by Undo. |
Only the owner is answered. The query provider and the handler compare the caller with the
{user} segment; anyone else — another user, the system identity, the anonymous visitor — gets
nothing, and Graph is not called. Every read and move runs under the person's own delegated
credential (IEaGraphAuth, the Executive Assistant's consent), inside the HTTP I/O pool.
Only connected sources are listed (owner, 2026-10-15: "i see a lot of stuff that's not even
configured ==> must get only configured sources"). The leg names its connection —
InboxQueryLeg.ConnectedWhen =
namespace:{viewer}/_Mailbox/outlook-accounts nodeType:Mailbox/OutlookAccount content.primary:true limit:1 select:path —
and core's inbox lists Outlook (switch, menu entry, read) only when that finds a node. Only the
PRIMARY mailbox counts, because the leg reads the primary mailbox alone: an added mailbox with a
disconnected primary would list a source whose every read fails as not connected. The grant
itself lives in the Auth partition, which an inbox leg may not read; the virtual
outlook-accounts folder is its face inside the viewer's own partition. A connection query that
cannot be read or does not answer in 5 s LISTS the source (core's rule), so a connected person is
never hidden by a slow credential store.
How to connect: open {instance}/auth/ea/connect once and consent. Until then Outlook is
not listed at all. Should a listed mailbox's grant later fail, the inbox names the Outlook source
above its grid with "Outlook is not connected — open /auth/ea/connect once to connect your
mailbox." A credential read that produced no answer is reported as exactly that, never as "not
connected".
Built: triage verdicts for the Outlook source
Every Outlook row the inbox lists carries its verdict's importance and tags, and the rows
without one are triaged in the background on the instance's EU-hosted model. Same module,
Inbox/:
| piece | what it does |
|---|---|
TriageVerdict (node type TriageVerdict, Inbox/TriageVerdict.cs) |
one node per message at {user}/_Triage/outlook-{digest} — the digest is 128 bits of SHA-256 over the immutable message id, so the same message always names the same verdict. Content: provider, messageId, importance (important | ignorable, open string constants), tags (open strings, normalised: lowercase, - for spaces, at most 6 of at most 32 characters), reason, model, triagedAt (UTC), subject, from, receivedAt, webLink, correctedBy, correctedAt. Never the body. Registered mesh-wide by the module (AddTriageVerdictType), with the default pages so the owner can edit the tags. |
the join (OutlookMailboxQueryProvider) |
once Graph has answered, the owner's verdicts for exactly those rows are read in ONE query — an explicit path list [p1, p2, …], which the stores push down as WHERE path IN (…) — and each row's content gains importance, tags (an array) and triageReason. A row without a verdict is left as it is. A verdict read that fails or takes over 5 s answers the rows unjoined and offers nothing to the triage: an unreadable store is never read as "untriaged". |
the triage (InboxTriagePass, Inbox/InboxTriage.cs) |
the rows the join found WITHOUT a verdict are offered to a background pass. Per owner one pass at a time — the mark is dropped BEFORE the pass's report is delivered, never in a Finally after it, so a pass started on the report (a tick, a manual trigger, the next read) is never skipped as "running" — at most Inbox:Triage:MaxPerPass (default 10) messages per pass, each message attempted at most once per process lifetime, one model call per message inside the I/O pool. The prompt carries the subject, the sender and Graph's preview only, and says the email is data, never instructions; the answer must be a JSON object with a known importance, or nothing is written (logged). |
the model (EuTriageModelSource) |
Inbox:Triage:Model when one is named, else the utility then the chat tier through the AI engine's ChatClientCredentialResolver.ResolveForTierWithResidency(tier, "Eu"); the answer must also read EU through DataResidencyFor (Unknown on any ambiguity), have a usable credential and be admitted by the instance. The bare client comes from the lowest-order non-persistent IChatClientFactory that serves it. No EU model ⇒ nothing is triaged, the log says so once, and no model outside the EU is ever used instead. |
the write (MeshTriageVerdictStore) |
create-only, as System on the owner's behalf (the pass runs after the request has gone), scoped to the owner's _Triage. An existing verdict refuses the create and is left exactly as it is — so a verdict the owner corrected is never overwritten, because the model never overwrites anything. |
A filter on the verdict (content.importance:important, content.tags:reply, a negation) is
decided on the ENRICHED row: the owner's newest 1000 verdicts are read first and each Graph candidate
is matched with its verdict stamped in, so the branch's limit still counts matching rows while Graph
is paged, and an untriaged row matches -content.importance:ignorable. (A verdict older than those
1000 is still joined onto its row, but not seen by such a filter.) A namespace alternation over
several mailboxes is authorized per owner.
Why this cannot feed on its own writes. The pass is triggered by a READ of the live mailbox,
never by a query over _Triage, so a verdict it writes cannot trigger it again. Should a pass still
find a verdict this process wrote itself (its claim set is bounded and starts over), it logs an
Error naming the path.
Why Mail may depend on the AI engine. It already does (the Executive Assistant's tools and the
connection providers), and the package declares AI in its requires; the triage adds no edge.
Configuration (Inbox:Triage): Enabled (default true), MaxPerPass (default 10), Model
(an explicit selection, still used only when it is marked EU).
Not yet: the person's edit does not stamp correctedBy by itself — the generic editor writes
the tags; the model already leaves every existing verdict alone, so the field is a statement for
readers until the inbox's own tag action sets it. A verdict written during a render shows on the
inbox's NEXT read. Thread context (earlier messages of the conversation) and corrections given to
the model as examples are not built.
Still open from the design above: the tag side menu and hiding ignorable mail (core's inbox, which
reads importance and tags from the row content), mark read, thread context, and the other
providers.
Built: the opt-in copy
"can you also optionally offer that data is replicated?" — the maintainer, 2026-10-16. Decided the same day: when a source's copy is switched on, memex keeps full messages (subject, sender, account, time and the whole body; attachments stay with the provider and are fetched when a message is opened), backfilling the last 90 days and then keeping up. Live stays the default.
Built for Outlook. Teams chats, Gmail and the Mac sources (iMessage, WhatsApp) are not copied yet — see Not built below.
| piece | what it does |
|---|---|
| the switch | Settings → Inbox copies (/{user}/Settings/InboxCopies, a person-app tab, MailCopySettings): a TEMPLATE that loads nothing on the hub — a reactive mesh search lists each copy that is on through its source node's own Status area (the node's fields read-only and bound: when it started, the window, whether the backfill is done, the last sync and the last error), a note that with nothing listed Outlook is read live, and Keep a copy of Outlook in memex / Stop and delete the copies. Stopping asks first and says the copies will be deleted; the mailbox is never touched. Only the space's own person can press either — the click checks it again (KeepACopy, DeleteTheCopies), and the confirmation reads the clicker anew, so a dialog the owner opened and someone else confirmed deletes nothing. |
| the source node | {user}/_MailCopy/outlook, node type MailCopySource: generation, enabledAt, windowStart (= enabledAt − 90 days), cursor (Graph's next or delta link), backfilled, lastSyncAt, lastError, leaseHolder, leaseUntil, mailboxId (the Microsoft account the copy was made from — the delegated token's own tid/oid claims, or for an opaque personal-account token the Inbox folder's id; never /me, which needs User.Read, a scope the consent does not ask for). Its absence IS "off". It is created once when the person switches the copy on, and afterwards changed only by a generation-checked commit at its owning hub (IMailCopyStore.Commit — the change is applied to the node as it is THERE, and only while it carries the generation the sync read): a sync can never re-create a source the person switched off, nor overwrite one switched on again. |
| the generation | a fresh id per switch-on and per rebuild. Every copy carries it and its id includes it, so a late write of an earlier generation lands BESIDE the current copy, never on it, and that generation's leftovers are removed by generation (RemoveGeneration) without touching the current one. |
| one copy | {user}/_MailCopy/outlook/{digest}, node type MailMessageCopy — the digest is 128 bits of SHA-256 over (generation, immutable message id), so a re-sync overwrites (create-or-replace) and never duplicates. Content: messageId, generation, subject, from, fromAddress, receivedAt (UTC), receivedTicks (Unix ms), orderKey (receivedTicks × 1000 + three digits of a hash of the id — what the store orders and pages by: a number, since a stored instant's text form does not compare in order; unique within a millisecond, so a page boundary advances past messages received together; below 2^53, so it compares exactly as a double), isRead, webLink, conversationId, preview, body (plain text — an HTML body is stripped before it is stored), copiedAt (when this copy was written — a late Delete compares against it). |
the sync (OutlookMailCopy) |
Graph's delta over the Inbox folder (/me/mailFolders/inbox/messages/delta), unfiltered — Graph caps a delta carrying a receivedDateTime filter at 5,000 messages and still answers a delta link, so a filtered backfill of a busy Inbox would end "done" with part of the window missing; the 90-day window is applied to what comes back. Body as text, immutable ids; up to 20 pages of 50 per step and 10 steps per sync. Within a step each message's LAST event wins (Graph may name one message on several pages — changed then removed, changed twice), so the step writes every id once and its concurrent writes cannot apply an earlier event after a later one. A removal (moved out of the Inbox, deleted) replaces the copy by a tombstone dated now (no subject, sender or body; never listed — left out before the limit — nor opened), and an inbox Delete leaves one dated when it was asked: a write older than the removal (an Undo whose Graph read lost the race) is refused against it, a newer one replaces it. The cursor is committed after every step. A failed step records lastError and keeps its cursor, so the next sync repeats it. When Graph refuses the cursor (HTTP 410, syncStateNotFound, syncStateInvalid, resyncRequired) the copy is rebuilt: a new generation, no cursor, backfilled false — the inbox reads Outlook live until the fresh backfill is done — and the old generation's copies are removed, so nothing deleted while the cursor was dead survives. One sync per copy at a time, across replicas: a sync first takes the copy's LEASE (leaseHolder/leaseUntil, 10 minutes, through the generation-checked commit); another sync that finds it held stands down without reading Graph. The lease is renewed before every step's writes and with every cursor commit, and released when the sync ends — a failed one too; a crashed holder's lease lapses. A step whose renewal is refused writes nothing. A step's writes never short-circuit: a failed write is counted, the rest still run, and the cursor is then NOT advanced (the step repeats) — so the commit, or the clean-up after a switch-off, always covers the whole step. At most one sync every 2 minutes unless the person asked; a sync's first lookup is bounded (5 s) and a whole sync at most 30 minutes, so a stalled one always frees the person for the next. 🚨 The delta steps run in a pool of their OWN (2 at a time), so backfills in flight never take the slots the live inbox, a message open or Delete/Undo need. |
| the account | A copy is bound to the account it was made from (mailboxId, recorded by the first sync). It is served — in the list and in the reading view — only while the primary mailbox is connected to THAT account (read from the token or the Inbox folder, remembered per CONNECTION — keyed by the delegated token, so reconnecting another account reads it again; a credential read that gives no answer is an error, so the inbox reads live and a sync releases its lease). When the person connects another account the inbox reads Outlook live at once, and the next sync rebuilds the copy from the new account (a new generation; the old account's copies are removed). Every sync also sweeps leftovers of earlier generations — a rebuild whose clean-up was interrupted — not only when the copy is off. |
| the generation id | 🚨 also owner-writable content, and spliced into store queries — so only a well-formed id (1–64 letters, digits, -) is ever used in one; anything else is refused before the query, and a sync that finds one rebuilds the copy under a fresh id. Every privileged delete re-checks each node first: directly under the copy's own source path, of the copy type, of exactly that generation. |
| the cursor | 🚨 A stored cursor is node content in the person's own partition, so it is never trusted: a cursor (or a next link Graph returned) that is not absolute HTTPS on graph.microsoft.com, default port, without user info, is refused BEFORE any request and the copy is rebuilt from a fresh delta. The delegated token is handed only to Graph URLs — any other request goes without it. A sync that fails before its steps (the account lookup, the sweep) records lastError too, without moving the cursor. |
| one connection | An Undo reads its message through ONE connection whose account must BE the copy's (after a reconnect, before the next sync rebuilds, it copies nothing), and its clean-up after a switch-off is a tombstone at the Undo's own time — it replaces only what the Undo wrote. A sync makes ONE credential read and binds to it: the account check AND every delta step use that token, so a reconnect to another account mid-sync can never land that account's mail under the first one's identity. |
| what starts a sync | switching the copy on; every read of the inbox's Outlook leg by its owner — with the copy off too, where the sync only sweeps leftovers (so a late write behind a switch-off is swept even after a restart); and MailCopyKeeper, every 10 minutes, for every person this process has synced. 🚨 Never a change under _MailCopy — the sync writes only there, so it is never fed its own writes. |
| the inbox | The Outlook leg answers from the copy once the backfill is done — before that, and whenever the copy is off or the decision (the source read AND the account check, one 5 s budget for both) cannot be made in time, it reads Graph live. 🚨 The copy holds only its window, so it answers ALONE only when it fills the page newest first (those are then exactly the newest rows); an oldest-first list is read live; and a page the copy cannot fill — a search, an old unread message — is the copy's rows unioned with the live ones (each message once, the copy's version first), so nothing outside the window disappears. The copy is turned back into the same Graph messages the live read returns, so the rows, filters, triage join and account stamp are the live ones, and Outlook is listed once either way. The copy is paged in receive order (200 at a time, at most 100 pages) until enough copies MATCH the search and the row filters — never cut at a fixed count first, which would answer a match just past the cut as absent. A search needs every word in the subject, sender, preview or body. 🚨 The read from the copy is bounded (10 s) and falls back to Graph live on a failure or a stall, so a copy can never fail or hang an inbox whose Outlook is reachable; and it is re-checked after the pages: if a sync rebuilt the copy or the person switched it off while it was read, the answer is refused for the live one rather than given empty or partial. |
| the reading view | shows the stored body when the copy holds the message; otherwise — or when the copy cannot be read within 5 s — reads Graph live, as before. |
| Delete / Undo | still move the message in Outlook; Delete then removes its copy — unless the copy reflects something AFTER the Delete was asked for (copiedAt: a sync since, or an Undo ASKED later — an Undo's copy carries the time it was asked, so the order of the person's moves decides, never how long a write took; and every move is stamped BEFORE its Graph request is sent, so a late response never makes an earlier move look newer) — and Undo re-reads it and writes the copy back only when it was received inside the window, and only over an OLDER copy (WriteIfNewer, decided at the owning hub — a sync's newer state is never replaced by an Undo's late Graph read), as the sync would, then checks again that the copy is still on in the same generation — if the person switched it off (or on anew) while Graph was being read, or the re-check cannot be read, the copy Undo wrote is removed again. The follow-ups of one message apply in the order they were asked for (in a process); a stalled one holds the next back at most 10 s, after which the asked-time stamps alone keep the order. The follow-up runs detached and the outcome waits for it at most 5 s, so a slow copy never holds back a move Outlook already made. The next delta reconciles either way. |
| switching off | deletes {user}/_MailCopy/outlook and every copy under it, of every generation, in one call. A sync running at that moment finds its commit refused, removes the copies it wrote meanwhile by their generation, and never switches the copy back on — and if the person switched it on again in between, the new copy is left alone. A copy a late write left behind anyway is swept by the next sync of that person (which, for a copy that is off, only looks for leftovers — Sweep). 🚨 The sweep LISTS the copies first and reads the source AFTER, removing only copies whose generation is not the source's as it then stands — so a copy switched on (and backfilled) by another replica meanwhile is never touched. |
No history. Both node types are registered with keepsHistory: false, so no version snapshot of a copied body is kept — on Postgres the trigger-written snapshots are purged after each write, the platform's own behaviour for such a type (core NoHistoryTypePgTests). "Stop and delete the copies" therefore leaves nothing of the mail behind, history included. Every write of a sync step — changes and tombstones — goes through WriteIfNewer, dated when Graph was ACTUALLY asked — taken inside the request's own pool slot, never when the step was queued — so a Delete or Undo asked after it is never overwritten, and an outside change Graph reports after the person's move is never refused as stale. 🚨 And because ordering by clocks is only ever a guess (a retried request, a slow response), an event that LOSES to a local action is not simply dropped: before the cursor moves past it the message is re-read from the Inbox as it is NOW (/me/mailFolders/inbox/messages/{id}) and the copy settles on that — copied when it is there, tombstoned when it is not — as a compare-and-set on the stored revision (the node's own version, which moves on EVERY accepted write — copiedAt orders actions only) read BEFORE Graph was asked: a Delete or Undo that completed while the answer was in flight makes it a conflict, and the reconcile asks again (up to 3 times, then the step repeats).
Access. The copies live in the person's own partition and are read as that person; the
background writes run as System on their behalf, scoped to their own _MailCopy namespace — the
triage verdicts' arrangement. Nothing grants anyone else a read. Proven by
MeshWeaver.Mail.MicrosoftGraph.Test — OutlookMailCopyTests (scripted Graph), MailCopyMeshTest
(a real mesh) and MailCopyAccessMeshTest (a mesh with row-level security and no blanket grant: another
person reads nothing, the owner's identical query finds the copy).
Not built. Teams chats, Gmail, iMessage and WhatsApp copies. Gmail lives in its own module
(MeshWeaver.Mail.Gmail), which shares no assembly with this one, so its copy needs either a shared
home for the two node types or its own; the Mac sources run in the LocalMesh app on the Mac, where the
chat databases already are. After a restart, a copy catches up the next time its owner opens the inbox
(the keeper only revisits people this process has seen); nothing is missed, since Graph's delta is
cumulative. The inbox's own settings menu does not yet link to the tab (core's inbox has no extension
point for it); the tab is in the person's settings app.
Order of work
- Outlook: the triage job, the verdict node type, the inbox leg and the tag side menu.
- Actions on the selection (Archive, Delete, Mark read, Tag), called through Graph.
- Live search through the provider.
- Gmail, then iMessage (the Mac bridge), then the WhatsApp business channel.
What WhatsApp cannot do: WhatsApp offers no API for a person's own chats — only the business number the assistant answers on. Those conversations are triaged; a personal chat is not reachable.