Skip to content

Inbox is for work, /agent is for chat — and XMPP is the one chat protocol #1471

Description

@asim

The model this comes from

Four layers, distinguished by how much the consumer already knows rather than by capability:

We supply They supply Layer Standard
state the exact call services HTTP API — open
a described menu the goal and the reasoning tools MCP — open, and it won
the reasoning too the goal agents chat — universal form, no open protocol
the occasion too a policy, once work forming (A2A tasks, MCP Tasks)

Two rungs have open standards. The third has a universal form and every implementation of it is proprietary, because chat is the product surface and nobody opens their product surface. Serving XMPP is how that rung gets an open one: the same interaction model everyone has, over a protocol any existing client already speaks.

Then: agents via chat, work via mail. Mail is the work protocol — asynchronous, durable, an inbox is a queue, a thread is a task's history, and forward/cc/reply-all is delegation. People have handed each other work over it for forty years without needing to define "work".

Two genres, not two data sets

internal/thread holds both. What differs is how a conversation reads:

  • Thread — a correspondence. Composed messages with envelopes: subject, addresses, quoted history, signatures. Opened, read top to bottom, replied to. Latency assumed.
  • Chat — a stream. Short spoken turns, no subject, no quoting. You are in it; the unit of attention is the conversation and the messages are its texture.

This is not cosmetic. The genre decides whether the page is legible at all. A mail thread rendered as a chat stream repeats quoted history on every line. A chat rendered as a thread is fifty one-line messages each with a header. One conversation cannot be shown both ways with either working.

The discriminator already exists — thread.Client — and nothing renders from it. So:

genre (thread.Client) carriers page reads as
mail SMTP, IMAP, submission /inbox threads
chat XMPP, WebSocket, the web page /agent a stream

The client is the genre. The carrier is the transport, and a detail.

No new page

/messages was considered and rejected. thread.Message is a struct; naming a page after it exposes the storage, exactly as /threads would. A page is named for what it is for, not for what it holds — /inbox and /agent survive replacing internal/thread, and /messages does not.

And "agents via chat" means the agent page is the chat page. The rail on /agent is already every conversation on every client (see the package comment on inbox/conversation.go). A third view of that list is the duplication this repository has spent a lot of effort deleting.

What is actually wrong today

1. Two chat implementations in one package.

service/chat/chat.go       1494 lines  bespoke websocket protocol, room_<id>.json, pruned at 24h
service/chat/xmpp*.go       ~790 lines  XMPP, writes to internal/thread

Two protocols, two stores. xmppRoom() prefixes xmpp_ so the namespaces cannot collide, which is the admission. Mail has one model — SMTP and IMAP are carriers over one store.

The fix is already named in service/chat/xmpp.go's own package comment: "The rooms in this package are a bespoke websocket protocol with a bespoke roster; MUC is the standard version of exactly that."

  • RFC 7395 — XMPP over WebSocket. The browser speaks XMPP. The web page stops being a second protocol and becomes a second carrier. This is what Converse.js and Movim do.
  • XEP-0045 — MUC. Article rooms become MUC rooms. Presence, roster, join and leave, history: specified, not hand-written.
  • One store. internal/thread. room_<id>.json and its 24-hour prune go.
  • agent/chat becomes a participant that joins a room, exactly as agent/mail reacts to mail arriving — a JID in the room rather than a special case wired to an event.

2. Presence does not exist.

It is the one thing XMPP gives that mail structurally cannot, and it is what a chat surface should be built around. A message list without it is a slower inbox. The rail should say who is here — the agent always is, which is the point of it.

3. Nothing renders by genre.

inbox/conversation.go branches on who wrote a message — agent gets markdown, a person gets escaped text, mail gets mail.Rendered. It does not branch on genre, so a chat conversation opened from the rail arrives with thread furniture: address lines, quoted-text folds, and a Reply control offering to send mail.

4. A person-to-person conversation has nowhere to be.

internal/thread records it and Parties knows who is on it. The rail treats a conversation as being with an agent.

5. A chat has a subject it should not have.

service/chat/xmpp_record.go calls thread.Name with the other party's address. A chat has no subject; the field was filled because it was there, and it renders as a title on a page that should not have one.

Scope

  1. XMPP is the one chat protocol, including for the browser (RFC 7395)
  2. Rooms are MUC (XEP-0045); room_<id>.json is deleted and rooms live in the record
  3. Presence, on /agent
  4. Rendering by genre: t.Client chooses thread or stream
  5. /agent's rail is chat; /inbox keeps mail
  6. agent/chat joins rooms as a participant rather than subscribing to an event
  7. Drop the subject on chat conversations

Closes #59 when done. Folds in #24 — "make /agents a conversation, not a token inventory" is the same observation arriving from the model: the plural page is an inventory about a thing whose entire interface is conversation.

Done when

A conversation with your agent reads the same in Conversations, in Dino, and in the browser, because all three are XMPP clients of the same server — and opening one from /inbox is not possible, because that is not where chat lives.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions