@ai-matrx/messaging 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,78 @@
1
+ # Changelog — `@ai-matrx/messaging`
2
+
3
+ ## 0.1.0 — 2026-08-31
4
+
5
+ First release. Enterprise, AI-native in-app messaging for Matrx clients, built to the ratified
6
+ work order at `common-docs/systems/communications/messaging/HANDOFF.md`, on top of
7
+ `@ai-matrx/realtime` (D5: zero hand-rolled `.channel(` in this package).
8
+
9
+ ### Added
10
+
11
+ - **Core (`@ai-matrx/messaging`)** — framework-free; the only runtime dependencies are
12
+ `@ai-matrx/realtime` and `@ai-matrx/agents`.
13
+ - `createMessagingRepository` — THE data contract, and the only file in the package holding a
14
+ table or RPC name. Atomic direct-conversation creation through
15
+ `dm_get_or_create_direct_conversation` (read-then-insert is structurally absent), keyset
16
+ pagination terminated by a unique column, explicit `organization_id` on every write, a single
17
+ session retry that never touches a permission denial, and an in-flight-deduped profile cache.
18
+ - `createMessagingStore` — the one door every incoming message uses. Collapses the optimistic
19
+ bubble onto its confirmed row by client key, drops out-of-order updates, orders by
20
+ `(created_at, id)`, and forces the active conversation's unread count to zero.
21
+ - `createOutbox` — a message you typed is never lost: persisted before the send, one
22
+ idempotency key across every attempt, strict order, durable across a reload, and a bounded
23
+ failure that keeps the message with a reason and a retry door.
24
+ - `createMessagingEngine` — channel wiring with a backfill door on every recovery path,
25
+ optimistic send, read receipts, and conversation lifecycle.
26
+ - `createActionRegistry` — actionable messages with idempotence enforced by the registry, and
27
+ forward compatibility on unknown kinds and versions. The seam `@ai-matrx/meet` extends.
28
+ - `createMessagingAi` — catch me up, summarize, extract action items, draft a reply, executed
29
+ through the platform's agent system. Machine content travels as named variables, never
30
+ `user_input`.
31
+ - Matrx references (`splitText`, `summarizeText`, `extractReferences`, `composeFence`),
32
+ effective-actor resolution, the ingress-boundary projection layer, the typed error
33
+ classification, formatting/grouping helpers, and the two channel namespaces.
34
+ - **React (`@ai-matrx/messaging/react`)** — the complete product, not a kernel:
35
+ `MessagingProvider`, `MessagingInbox`, `ConversationList`, `ConversationView`, `Composer`,
36
+ `MessageBubble`, `MessageActionChips`, the parts (`Avatar`, `ReferenceCard`, `DeliveryTick`,
37
+ skeletons, empty states), fifteen inlined SVG icons (C19 — no icon-library dependency), and the
38
+ hooks for hosts that compose their own page layout.
39
+ - **Styling** — `@ai-matrx/messaging/styles.css` (structural: layout, motion, `100dvh`, safe-area
40
+ insets, 44px touch targets, a 16px composer minimum so iOS Safari does not zoom on focus) and
41
+ `@ai-matrx/messaging/tokens.css` (a complete default palette, light and dark, host-overridable).
42
+ Hardcoded colors in the structural sheet fail the release gate.
43
+ - 113 tests, each named for the failure it prevents, plus a packed-tarball canary that exercises
44
+ ESM + CJS, both entries, the `"use client"` banner per chunk, the `globalThis` slots across
45
+ module graphs, the stylesheets' presence and export-map resolution, and the stability of the
46
+ table/RPC names.
47
+
48
+ ### Deliberately not in this release
49
+
50
+ Recorded in `FEATURE.md` § Deliberate omissions and in the work order, not quietly dropped:
51
+ reactions, per-message read receipts, edit history and pinning (all need a DB change — the
52
+ canonical schema has no columns for them); attachment UPLOAD (reads and durable-ref rendering
53
+ ship; the upload path belongs on `@ai-matrx/data/files`); a separate thread surface; moderation
54
+ affordances; and a `/native` entry (C25 — built at the first real Expo consumer, inside this
55
+ package).
56
+
57
+ ### Consumer action
58
+
59
+ None required — this is a first release with no existing consumers.
60
+
61
+ Adopting it in a repo that has its own messaging feature (matrx-frontend is the intended first
62
+ consumer):
63
+
64
+ - Wrap the messaging route in `<MessagingProvider client={supabase} userId={…}
65
+ organizationId={…}>` and render `<MessagingInbox />`. Import `tokens.css` then `styles.css`.
66
+ - **Delete, do not wrap** (C9): the host's own realtime subscription and channel bookkeeping, its
67
+ message dedup and echo suppression, its optimistic-send reconciliation, its typing and presence
68
+ timers, its conversation-list cache and in-flight dedup, and its `find-then-insert`
69
+ direct-conversation path. A second copy beside the package is the named failure.
70
+ - Re-register the host's actionable-message kinds as `ActionHandler`s on the provider. Each
71
+ handler must re-resolve its authorizing row server-side; the payload is the request, never the
72
+ permission.
73
+ - Wire `onOpenReference` to the app's router, or reference cards render inert-but-labeled.
74
+ - Pass `transport` + `agents` to light up the AI actions; without them they do not render.
75
+ - `resolveSession` enables the single retry on a missing session — pass it, or a read during
76
+ sign-in reports `session-unavailable` instead of recovering.
77
+ - This package supersedes host code that talks to `communication.dm_*` directly. Anything still
78
+ reading those tables outside the package should be moved onto it or deleted.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AI Matrix Engine
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,288 @@
1
- # @ai-matrx/messaging
1
+ # `@ai-matrx/messaging`
2
2
 
3
- Reserved for the AI Matrx real-time direct and group messaging package. This `0.0.0` release reserves the canonical package name; it does not contain an implementation yet.
3
+ Enterprise, AI-native in-app messaging for every Matrx client: direct, group, and org-scoped
4
+ conversations, with agents as first-class participants — delivered as a drop-in package with the
5
+ polish of iMessage and the power of Slack.
6
+
7
+ One provider, one component, under an hour to full production messaging including the AI.
8
+
9
+ ```bash
10
+ pnpm add @ai-matrx/messaging
11
+ ```
12
+
13
+ Built on [`@ai-matrx/realtime`](../realtime/README.md). There is **zero hand-rolled `.channel(`
14
+ code in this package**, and there must be none in yours.
15
+
16
+ ---
17
+
18
+ ## Why this package exists
19
+
20
+ Messaging is the feature everyone assumes is easy and nobody ships correctly twice. The hard
21
+ parts are not the bubbles; they are the eight ways a message can be lost, duplicated, reverted,
22
+ or attributed to the wrong person. Every one of those has a name in this README and a regression
23
+ test beside it.
24
+
25
+ We are deliberately **inflexible**. There is no provider abstraction, no transport adapter, no
26
+ "bring your own store". The package does exactly what we want it to do, and that is the product.
27
+
28
+ ---
29
+
30
+ ## The doctrine
31
+
32
+ ### 1. A message can arrive FOUR ways. It renders once.
33
+
34
+ The optimistic bubble, the insert's own response, the broadcast, and the Postgres Changes row —
35
+ in any order. Identity is `id` **or** `clientMessageId`, and the confirmed row *collapses onto*
36
+ the optimistic one in place.
37
+
38
+ The tempting shortcut — matching on `(sender, content)` — silently merges two genuinely different
39
+ "ok" messages into one. The client-minted idempotency key exists so identity never has to be
40
+ guessed. It is minted once per queued message and reused by **every** send attempt, which is also
41
+ what makes a retry safe when the previous attempt actually reached the database.
42
+
43
+ ### 2. Older never overwrites newer.
44
+
45
+ An out-of-order UPDATE carrying an older `edited_at` than the copy already held is **dropped**.
46
+ Without this, an edit visibly reverts itself a second later and then re-applies.
47
+
48
+ The one exception is deliberate: a persisted row always supersedes an optimistic one, even when
49
+ the optimistic timestamp reads later. That timestamp is a guess from the sending device's clock,
50
+ and a skewed clock must not strand a bubble as permanently "sending".
51
+
52
+ ### 3. Reconnect means RE-READ. Realtime has no replay.
53
+
54
+ Every channel this package opens declares an `onBackfill` door, and those doors re-read from the
55
+ database — the conversation channel from its own high-water mark, so it fetches what it missed
56
+ rather than everything. A reconnect without a re-read leaves a permanently wrong thread that
57
+ looks perfectly healthy. That is the most expensive bug in messaging, and it is structurally
58
+ impossible here.
59
+
60
+ ### 4. A message you typed is NEVER lost.
61
+
62
+ The outbox persists the draft **before** the network call, not after it succeeds. Guarantees:
63
+
64
+ - **Durable across a reload.** A tab closed mid-send comes back with the message still queued.
65
+ The default storage is `localStorage`; when it is unavailable the package falls back to memory
66
+ and **says so** rather than pretending to be durable.
67
+ - **In order.** Message 2 never overtakes a retrying message 1.
68
+ - **Bounded and honest.** Attempts back off; after the last one the entry stays in the queue,
69
+ marked `failed`, **with a reason** and a retry door. Never silently dropped, never retrying
70
+ forever.
71
+ - **Visible.** A queued message renders as an optimistic bubble immediately. An invisible queue
72
+ is a lost message from the user's side.
73
+
74
+ ### 5. Direct conversations are created ATOMICALLY, in the database.
75
+
76
+ Via `dm_get_or_create_direct_conversation`, which advisory-locks the unordered user pair. The
77
+ banned pattern is read-then-insert: two tabs, a double-click, or two batched system
78
+ notifications race it into two "direct" conversations for one pair, forever.
79
+
80
+ ### 6. Authorization is the database's. Org is explicit.
81
+
82
+ RLS plus auth-checked SECURITY DEFINER RPCs. There is not one permission branch in this package —
83
+ an RPC that says no becomes a `forbidden` error, never a client-side re-decision. And every
84
+ conversation and message write carries an explicit `organization_id`; the package refuses the
85
+ write rather than letting an unscoped row reach the database.
86
+
87
+ ### 7. A missing session is a lifecycle moment, not an error.
88
+
89
+ A read that lands between sign-in and token arrival retries **once** after the host resolves a
90
+ session, then reports `session-unavailable` as a *warning*. A permission denial is never retried —
91
+ retrying one is how a request storm is built. (The incident: a reader mounted before the access
92
+ token existed and the app captured **909 errors in 0.6 seconds**.) Profile and list reads also
93
+ share one in-flight promise per key, so N concurrent readers make one request — and a failure is
94
+ never cached, or one wobble leaves the surface broken until reload.
95
+
96
+ ### 8. Pagination is keyset, terminated by a unique column.
97
+
98
+ `(created_at, id)`, never `created_at` alone — which duplicates or skips rows whenever two share a
99
+ millisecond, i.e. every page in a large org. The package also asks for one row more than the page
100
+ so `hasMore` is a fact rather than a guess from "we got a full page".
101
+
102
+ ### 9. An agent never wears a colleague's face.
103
+
104
+ `sender_id` is the audit principal and never changes. When an agent acted through a human's
105
+ session, the *rendered* author is the agent — and it names the human it acted for. An unnamed
106
+ agent is labeled "Agent"; falling back to the human's name and avatar would put an automated
107
+ message under a real person's byline.
108
+
109
+ ### 10. Everything a message names must OPEN.
110
+
111
+ Matrx references arrive two ways — structured in `metadata.references`, or as a ` ```matrx `
112
+ fence in the content. Both become live cards. A fence **never** reaches the reader as a code
113
+ block of JSON, and never reaches an inbox preview or a desktop notification as raw JSON either;
114
+ one collapse function serves both. A reference with no resolvable identity is dropped rather than
115
+ drawn as a card that cannot open.
116
+
117
+ ### 11. An action this build cannot honor renders NOTHING.
118
+
119
+ Actionable messages go through a registry, not a `switch`. An unknown `kind` — or a known kind at
120
+ an unknown `version` — renders no chips and executes nothing, which is what lets a new sender ship
121
+ before every reader has caught up. Idempotence is enforced *by the registry*: concurrent
122
+ executions of `(kind, messageId, actorId)` share one call, and a settled action returns its stored
123
+ receipt without touching the server. **The payload is not authorization** — a handler must
124
+ re-resolve the durable, caller-authorized row before it writes.
125
+
126
+ ### 12. Nothing structured rides `user_input`.
127
+
128
+ `user_input` is what a human typed. Transcripts, rosters, and cutoffs travel as named
129
+ **variables**. The server stores and frames `user_input` as the turn's human utterance, so
130
+ smuggling a transcript through it silently corrupts every conversation it touches. The one
131
+ `user_input` this package ever sends is the instruction a person typed into "draft a reply".
132
+
133
+ ---
134
+
135
+ ## Using it
136
+
137
+ ### The one-hour integration
138
+
139
+ ```tsx
140
+ import { MessagingProvider, MessagingInbox } from "@ai-matrx/messaging/react";
141
+ import "@ai-matrx/messaging/tokens.css"; // defaults — override with your brand
142
+ import "@ai-matrx/messaging/styles.css"; // structural, required
143
+
144
+ export default function MessagesPage() {
145
+ return (
146
+ <MessagingProvider
147
+ client={supabase}
148
+ userId={user.id}
149
+ organizationId={org.id}
150
+ >
151
+ <MessagingInbox />
152
+ </MessagingProvider>
153
+ );
154
+ }
155
+ ```
156
+
157
+ That is realtime, presence, typing, optimistic send, the offline outbox, references, actionable
158
+ messages, keyset pagination, and the whole polished UI. There is nothing else to wire.
159
+
160
+ ### Adding the AI
161
+
162
+ ```tsx
163
+ <MessagingProvider
164
+ client={supabase}
165
+ userId={user.id}
166
+ organizationId={org.id}
167
+ transport={matrxTransport} // from @ai-matrx/agents/matrx
168
+ agents={{
169
+ catchUp: "…", summarize: "…", actionItems: "…", draftReply: "…",
170
+ }}
171
+ >
172
+ ```
173
+
174
+ Agent definitions live in the **database**, never in this package — the id is the only part a
175
+ host injects. A capability with no agent id **does not render at all**; it is never a dead button
176
+ that fails when pressed.
177
+
178
+ ### Making references open
179
+
180
+ ```tsx
181
+ <MessagingProvider … onOpenReference={(ref) => router.push(`/${ref.entityType}/${ref.entityId}`)}>
182
+ ```
183
+
184
+ Omit it and reference cards render as labeled, non-interactive cards with a title explaining why —
185
+ honest, never a button that does nothing.
186
+
187
+ ### Registering an actionable message kind
188
+
189
+ ```tsx
190
+ <MessagingProvider … actions={[{
191
+ kind: "access_request",
192
+ versions: [1],
193
+ choices: () => [
194
+ { id: "approve", label: "Approve", tone: "primary" },
195
+ { id: "decline", label: "Decline", tone: "danger" },
196
+ ],
197
+ execute: async (payload, ctx) => {
198
+ // Re-resolve the authorized request row here. The payload is the REQUEST,
199
+ // never the permission.
200
+ const outcome = await decideAccessRequest(payload.requestId, ctx.choice);
201
+ return { kind: "access_request", messageId: ctx.messageId, actorId: ctx.actorId,
202
+ outcome: outcome.alreadyAnswered ? "already" : "applied",
203
+ label: outcome.alreadyAnswered ? "Already answered" : "Approved",
204
+ settledAt: new Date().toISOString() };
205
+ },
206
+ }]}>
207
+ ```
208
+
209
+ This is the seam `@ai-matrx/meet` uses: a call invitation is just a message with a structured
210
+ payload.
211
+
212
+ ### Theming
213
+
214
+ Structural CSS ships in the package; the token **contract** is enforced; token **values** are
215
+ yours. Import `tokens.css` for a complete working default, then override the names you care
216
+ about:
217
+
218
+ ```css
219
+ :root {
220
+ --mx-msg-accent: #7c3aed;
221
+ --mx-msg-bubble-mine-bg: var(--mx-msg-accent);
222
+ }
223
+ ```
224
+
225
+ Hardcoded colors inside package components are banned, and the tarball gate fails the build if a
226
+ hex value appears in `styles.css`. Dark values are defined for both `prefers-color-scheme` and an
227
+ explicit `[data-theme="dark"]`, so a host toggle wins in either direction.
228
+
229
+ ### Composing your own layout
230
+
231
+ `<MessagingInbox />` is the product. If you want the list in a sidebar and the thread on a route:
232
+
233
+ ```tsx
234
+ <ConversationList onSelect={(id) => router.push(`/messages/${id}`)} />
235
+ <ConversationView conversationId={id} />
236
+ ```
237
+
238
+ Hooks — `useConversations`, `useConversation`, `useComposer`, `useTypists`, `useOnlineUserIds`,
239
+ `useMessagingAi`, `useMessageAction` — are exported for hosts that compose their own pages. They
240
+ are **not** an invitation to rebuild the product out of parts.
241
+
242
+ ### Outside React
243
+
244
+ The default entry is framework-free — safe from Redux middleware, a service module, a worker, or
245
+ a node test:
246
+
247
+ ```ts
248
+ import { createMessagingEngine, createMessagingRepository } from "@ai-matrx/messaging";
249
+ ```
250
+
251
+ ---
252
+
253
+ ## The database contract
254
+
255
+ One canonical schema, in Matrx Main, under the platform's DB conventions:
256
+
257
+ | | |
258
+ |---|---|
259
+ | Schema | `communication` (never `public`) |
260
+ | Tables | `dm_conversations`, `dm_conversation_participants`, `dm_messages` |
261
+ | RPCs | `dm_get_or_create_direct_conversation`, `get_dm_conversations_with_details`, `get_dm_user_info`, `get_dm_unread_count`, `is_dm_participant` |
262
+
263
+ Every name lives in exactly one file — `src/core/repository.ts` — and the packed-tarball canary
264
+ asserts them, so a silent rename fails the build instead of the app.
265
+
266
+ `matrx-dm` grew a parallel schema in its own project (`public.conversations` / `messages` /
267
+ `message_reactions`). Per the work order's R8 exactly one canonical schema survives, and it is
268
+ this one — the one with explicit org, versioning, soft deletes, and auth-checked RPCs.
269
+
270
+ ---
271
+
272
+ ## Entry points
273
+
274
+ | Entry | Contents | `"use client"` |
275
+ |---|---|---|
276
+ | `@ai-matrx/messaging` | Framework-free core: repository, store, outbox, engine, action registry, references, AI, formatting | **No** — a stamped pure module becomes a client reference inside an RSC |
277
+ | `@ai-matrx/messaging/react` | Provider, hooks, and the complete product UI. Re-exports the core, so a React app needs one specifier | Yes |
278
+ | `@ai-matrx/messaging/styles.css` | Structural CSS. Required | — |
279
+ | `@ai-matrx/messaging/tokens.css` | Default token values. Import, then override | — |
280
+
281
+ There is deliberately **no `/native` entry**. Per C25, platform bindings are built at the first
282
+ real consumer and always inside the package; a React Native UI that no Expo app exercises is
283
+ untested theory that rots. The core is already platform-agnostic (it is proven under a `node`
284
+ test environment with no DOM shim), so `/native` is a UI layer away when the mobile app adopts it.
285
+
286
+ ## License
287
+
288
+ MIT.