@ai-matrx/messaging 0.13.1 → 0.15.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 CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Fixed
6
+
7
+ - **A failed inbox read is never a skeleton forever, and never "empty"** (RC-B12
8
+ round 11). When the first conversation-list read failed,
9
+ `hasLoadedConversations` stayed false and the list showed its skeleton for as
10
+ long as it was open. The store now carries `conversationsLoadError`; the list
11
+ resolves to "Couldn't load your conversations" with a **Try again**
12
+ (`useConversations().retry`, `engine.refreshInbox()`), and "No conversations
13
+ yet" only follows a read that succeeded.
14
+ - **Read failures render through the design-system error slot.** "Couldn't load
15
+ this conversation" and the inbox failure are drawn with `ErrorBox` from
16
+ `@ai-matrx/design-system` (new dependency), so the host fills them with its
17
+ error actions — in AI Matrx, the Alchemy Menu. They were `EmptyState`s before,
18
+ in the same grey as "No messages yet".
19
+
20
+ ### Added
21
+
22
+ - `MessagingSnapshot.conversationsLoadError`, `MessagingStore.setConversationsLoadError`,
23
+ `MessagingEngine.refreshInbox()`, `useConversations().loadError` / `.retry`.
24
+
25
+ ## 0.14.1
26
+
27
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
28
+ `git diff npm/messaging/v0.14.0..npm/messaging/v0.14.1 -- apps/shared/messaging`).
29
+ No source changes intended and no consumer action required.
30
+
31
+ ## 0.14.0
32
+
33
+ ### Changed
34
+
35
+ - **Messaging runs with no organization selected** (access belongs to the
36
+ person, 2026-09-23). `<MessagingProvider organizationId={null}>` used to stay
37
+ inert — every inbox and conversation spun forever — and
38
+ `createMessagingRepository` threw at construction without an org, although
39
+ no read here filters by organization. Now the provider starts and every read
40
+ works as the signed-in person. The organization is only a write destination:
41
+ - starting a NEW conversation (`getOrCreateDirectConversation`,
42
+ `createGroupConversation`, `useConversations().startDirect`) without one is
43
+ refused with the new error code **`organization-required`**, before
44
+ anything reaches the database;
45
+ - a message or member added to an EXISTING conversation with no active
46
+ organization is stamped with that conversation's own organization;
47
+ - AI actions do not render without an organization (a run is filed under one).
48
+ - `MessagingIdentity.organizationId` and `MessagingRepository.organizationId`
49
+ are now `OrganizationId | null`; `RepositoryOptions.organizationId` is
50
+ optional. `MessagingErrorCode` gains `"organization-required"`.
51
+
52
+ ### Consumer action
53
+
54
+ - Pass the active organization as before, including `null` when none is
55
+ selected — do not gate `<MessagingProvider>` on having one.
56
+ - Where the host starts a conversation, ask the person to choose an
57
+ organization when none is selected (or handle `error.code ===
58
+ "organization-required"`).
59
+ - Code that read `host.identity.organizationId` as non-null must handle `null`.
60
+
3
61
  ## 0.13.1
4
62
 
5
63
  ### Fixed
@@ -781,3 +839,4 @@ consumer):
781
839
  sign-in reports `session-unavailable` instead of recovering.
782
840
  - This package supersedes host code that talks to `communication.dm_*` directly. Anything still
783
841
  reading those tables outside the package should be moved onto it or deleted.
842
+
package/README.md CHANGED
@@ -82,7 +82,11 @@ notifications race it into two "direct" conversations for one pair, forever.
82
82
  RLS plus auth-checked SECURITY DEFINER RPCs. There is not one permission branch in this package —
83
83
  an RPC that says no becomes a `forbidden` error, never a client-side re-decision. And every
84
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.
85
+ write rather than letting an unscoped row reach the database. The organization is a write
86
+ DESTINATION only: `organizationId={null}` is a valid state in which every read works (the inbox,
87
+ a deep-linked conversation, replying — a reply is stamped with its conversation's organization),
88
+ and starting a NEW conversation is refused as `organization-required` so the host can ask the
89
+ person to choose one.
86
90
 
87
91
  ### 7. A missing session is a lifecycle moment, not an error.
88
92
 
package/dist/index.cjs CHANGED
@@ -931,7 +931,7 @@ function projectMessage(row, fallbackOrganizationId) {
931
931
  id: requiredStr(row, "id", operation),
932
932
  conversationId: requiredStr(row, "conversation_id", operation),
933
933
  senderId: requiredStr(row, "sender_id", operation),
934
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
934
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
935
935
  content: typeof row["content"] === "string" ? row["content"] : "",
936
936
  kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
937
937
  // A row read from the DB is at least `sent`. `sending`/`failed` are outbox
@@ -979,7 +979,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
979
979
  groupName,
980
980
  groupImageUrl,
981
981
  createdBy: createdBy === null ? null : createdBy,
982
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
982
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
983
983
  createdAt,
984
984
  updatedAt,
985
985
  metadata: jsonObject(row["metadata"])
@@ -1045,6 +1045,7 @@ function createMessagingStore() {
1045
1045
  let threads = /* @__PURE__ */ new Map();
1046
1046
  let activeConversationId = null;
1047
1047
  let conversationReadFailures = /* @__PURE__ */ new Map();
1048
+ let conversationsLoadError = null;
1048
1049
  const listeners = /* @__PURE__ */ new Set();
1049
1050
  let cached = null;
1050
1051
  function snapshot() {
@@ -1058,7 +1059,8 @@ function createMessagingStore() {
1058
1059
  totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
1059
1060
  archiveFilter,
1060
1061
  archivedCount,
1061
- conversationReadFailures
1062
+ conversationReadFailures,
1063
+ conversationsLoadError
1062
1064
  };
1063
1065
  return cached;
1064
1066
  }
@@ -1103,6 +1105,12 @@ function createMessagingStore() {
1103
1105
  conversations = normalizeConversations(items);
1104
1106
  hasMoreConversations = hasMore;
1105
1107
  hasLoadedConversations = true;
1108
+ conversationsLoadError = null;
1109
+ emit();
1110
+ },
1111
+ setConversationsLoadError(message) {
1112
+ if (message === conversationsLoadError) return;
1113
+ conversationsLoadError = message;
1106
1114
  emit();
1107
1115
  },
1108
1116
  setArchiveFilter(next) {
@@ -1312,7 +1320,11 @@ function createMessagingEngine(options) {
1312
1320
  optimisticMessage({
1313
1321
  conversationId: entry.draft.conversationId,
1314
1322
  senderId: identity.userId,
1315
- organizationId: identity.organizationId,
1323
+ // Client-only until the confirmed row replaces it: the active
1324
+ // organization, else the conversation's own.
1325
+ organizationId: identity.organizationId ?? store.snapshot().conversations.find(
1326
+ (item) => item.conversation.id === entry.draft.conversationId
1327
+ )?.conversation.organizationId ?? "",
1316
1328
  content: entry.draft.content,
1317
1329
  clientMessageId: entry.clientMessageId,
1318
1330
  ...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
@@ -1331,10 +1343,20 @@ function createMessagingEngine(options) {
1331
1343
  }
1332
1344
  async function reloadInbox() {
1333
1345
  if (disposed) return;
1334
- const page = await repository.listConversations({
1335
- limit: conversationPageSize,
1336
- archived: store.snapshot().archiveFilter
1337
- });
1346
+ let page;
1347
+ try {
1348
+ page = await repository.listConversations({
1349
+ limit: conversationPageSize,
1350
+ archived: store.snapshot().archiveFilter
1351
+ });
1352
+ } catch (error) {
1353
+ if (!disposed) {
1354
+ store.setConversationsLoadError(
1355
+ error instanceof Error ? error.message : typeof error === "string" ? error : "The conversation list could not be read."
1356
+ );
1357
+ }
1358
+ throw error;
1359
+ }
1338
1360
  if (disposed) return;
1339
1361
  conversationCursor = page.nextCursor;
1340
1362
  store.setConversations(page.items, page.hasMore);
@@ -1419,6 +1441,13 @@ function createMessagingEngine(options) {
1419
1441
  }
1420
1442
  });
1421
1443
  },
1444
+ async refreshInbox() {
1445
+ try {
1446
+ await reloadInbox();
1447
+ } catch (error) {
1448
+ reportError(error, "inboxRefresh");
1449
+ }
1450
+ },
1422
1451
  async setArchiveFilter(next) {
1423
1452
  if (next === store.snapshot().archiveFilter) return;
1424
1453
  store.setArchiveFilter(next);
@@ -1844,12 +1873,15 @@ var RPCS = {
1844
1873
  };
1845
1874
  var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
1846
1875
  var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
1876
+ function optionalOrg(organizationId) {
1877
+ return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
1878
+ }
1847
1879
  function requireOrg(organizationId, operation) {
1848
- if (typeof organizationId !== "string" || organizationId.length === 0) {
1880
+ if (organizationId === null) {
1849
1881
  throw new MessagingError(
1850
- "misconfigured",
1851
- `${operation}: no organization_id`,
1852
- "Every conversation and message write carries an explicit organization_id (R5). Pass a real org on <MessagingProvider>; the package refuses the write rather than letting an unscoped row reach the database."
1882
+ "organization-required",
1883
+ `${operation}: no organization selected`,
1884
+ "A new conversation is filed under one organization. Ask the person to choose one, then pass it as organizationId on <MessagingProvider>. Reading and replying to existing conversations never needs it."
1853
1885
  );
1854
1886
  }
1855
1887
  return organizationId;
@@ -1861,7 +1893,7 @@ function sleep(ms) {
1861
1893
  return new Promise((resolve) => setTimeout(resolve, jittered));
1862
1894
  }
1863
1895
  function createMessagingRepository(options) {
1864
- const org = requireOrg(options.organizationId, "createMessagingRepository");
1896
+ const org = optionalOrg(options.organizationId);
1865
1897
  const handedIn = options.identity;
1866
1898
  if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
1867
1899
  options.onDiagnostic?.(
@@ -1931,6 +1963,11 @@ function createMessagingRepository(options) {
1931
1963
  });
1932
1964
  return found;
1933
1965
  }
1966
+ async function organizationOfExisting(conversationId) {
1967
+ if (org !== null) return org;
1968
+ const conversation = await repository.getConversation(conversationId);
1969
+ return conversation.organizationId;
1970
+ }
1934
1971
  const repository = {
1935
1972
  organizationId: org,
1936
1973
  actingUserId,
@@ -2063,6 +2100,7 @@ function createMessagingRepository(options) {
2063
2100
  },
2064
2101
  async getOrCreateDirectConversation(otherUserId) {
2065
2102
  const operation = "getOrCreateDirectConversation";
2103
+ const destination = requireOrg(org, operation);
2066
2104
  const me = await actingUser(operation);
2067
2105
  const id = await withSessionRetry(
2068
2106
  operation,
@@ -2074,7 +2112,7 @@ function createMessagingRepository(options) {
2074
2112
  // not a preference.
2075
2113
  p_user1_id: me,
2076
2114
  p_user2_id: otherUserId,
2077
- p_organization_id: org
2115
+ p_organization_id: destination
2078
2116
  },
2079
2117
  operation
2080
2118
  )
@@ -2086,13 +2124,14 @@ function createMessagingRepository(options) {
2086
2124
  },
2087
2125
  async createGroupConversation({ name, memberIds }) {
2088
2126
  const operation = "createGroupConversation";
2127
+ const destination = requireOrg(org, operation);
2089
2128
  const me = await actingUser(operation);
2090
2129
  const { data, error } = await withSessionRetry(
2091
2130
  operation,
2092
2131
  async () => db().from(TABLES.conversations).insert({
2093
2132
  type: "group",
2094
2133
  group_name: name,
2095
- organization_id: org,
2134
+ organization_id: destination,
2096
2135
  created_by: me
2097
2136
  }).select("id").single()
2098
2137
  );
@@ -2107,7 +2146,7 @@ function createMessagingRepository(options) {
2107
2146
  conversation_id: conversationId,
2108
2147
  user_id: userId,
2109
2148
  role: userId === me ? "owner" : "member",
2110
- organization_id: org,
2149
+ organization_id: destination,
2111
2150
  created_by: me
2112
2151
  }))
2113
2152
  );
@@ -2117,10 +2156,11 @@ function createMessagingRepository(options) {
2117
2156
  async insertMessage(draft, clientMessageId) {
2118
2157
  const operation = "insertMessage";
2119
2158
  const me = await actingUser(operation);
2159
+ const destination = await organizationOfExisting(draft.conversationId);
2120
2160
  const { data, error } = await db().from(TABLES.messages).insert({
2121
2161
  conversation_id: draft.conversationId,
2122
2162
  sender_id: me,
2123
- organization_id: org,
2163
+ organization_id: destination,
2124
2164
  created_by: me,
2125
2165
  content: draft.content,
2126
2166
  message_type: draft.kind ?? "text",
@@ -2181,12 +2221,13 @@ function createMessagingRepository(options) {
2181
2221
  async addMembers(conversationId, memberIds) {
2182
2222
  const operation = "addMembers";
2183
2223
  const me = await actingUser(operation);
2224
+ const destination = await organizationOfExisting(conversationId);
2184
2225
  const { error } = await db().from(TABLES.participants).insert(
2185
2226
  memberIds.map((userId) => ({
2186
2227
  conversation_id: conversationId,
2187
2228
  user_id: userId,
2188
2229
  role: "member",
2189
- organization_id: org,
2230
+ organization_id: destination,
2190
2231
  created_by: me
2191
2232
  }))
2192
2233
  );