@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/dist/react.d.cts CHANGED
@@ -208,7 +208,11 @@ interface Page<TItem, TCursor> {
208
208
  /** What the host injects. Identity ONLY — every hard part is in the package. */
209
209
  interface MessagingIdentity {
210
210
  readonly userId: UserId;
211
- readonly organizationId: OrganizationId;
211
+ /**
212
+ * The active organization, or `null` when none is selected. Reads never
213
+ * depend on it; it is only where a NEW conversation lands.
214
+ */
215
+ readonly organizationId: OrganizationId | null;
212
216
  }
213
217
  interface DraftMessage {
214
218
  readonly conversationId: ConversationId;
@@ -525,7 +529,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
525
529
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
526
530
  * warns and retries; it is never a red error.
527
531
  */
528
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
532
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
533
+ /** A NEW conversation needs an organization to land in and none is selected. */
534
+ | "organization-required" | "unknown";
529
535
  declare class MessagingError extends Error {
530
536
  readonly code: MessagingErrorCode;
531
537
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -817,15 +823,21 @@ interface RepositoryOptions {
817
823
  */
818
824
  client: MessagingSupabaseClient;
819
825
  /**
820
- * The active organization. Every conversation and message write carries it
821
- * explicitly (R5).
826
+ * The active organization, or `null`/omitted when none is selected.
827
+ *
828
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
829
+ * read here is answered by RLS as the session's user; the organization is
830
+ * only the DESTINATION of a NEW conversation. Without one, starting a
831
+ * conversation is refused with `organization-required` (the host asks the
832
+ * person to choose), and a message or member added to an EXISTING
833
+ * conversation is stamped with that conversation's own organization.
822
834
  *
823
835
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
824
836
  * acting is read from `client`'s own session — see `session-identity.ts` for
825
837
  * the class that closed and why a handed-in id could not be made safe by
826
838
  * convention. A caller that still passes `identity` is IGNORED and told so.
827
839
  */
828
- organizationId: string;
840
+ organizationId?: string | null | undefined;
829
841
  /**
830
842
  * INTERNAL. The React provider already runs one session reader for its own
831
843
  * render loop and hands it here so the two cannot answer differently and so
@@ -846,8 +858,11 @@ interface RepositoryOptions {
846
858
  now?: (() => number) | undefined;
847
859
  }
848
860
  interface MessagingRepository {
849
- /** The organization every write here is stamped with. */
850
- readonly organizationId: OrganizationId;
861
+ /**
862
+ * The active organization new conversations land in, or `null` when none is
863
+ * selected — reads work either way.
864
+ */
865
+ readonly organizationId: OrganizationId | null;
851
866
  /**
852
867
  * Who the session says is acting — awaited, because the first `getSession()`
853
868
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1004,11 +1019,18 @@ interface MessagingSnapshot {
1004
1019
  } | null;
1005
1020
  /** Conversations whose last open failed, keyed by id. Cleared on the next open. */
1006
1021
  readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
1022
+ /**
1023
+ * The inbox read FAILED (and no later read succeeded). Without it a failed
1024
+ * first read left `hasLoadedConversations` false forever — a skeleton that
1025
+ * never resolves is a screen telling a lie (RC-B12 round 11).
1026
+ */
1027
+ readonly conversationsLoadError: string | null;
1007
1028
  }
1008
1029
  interface MessagingStore {
1009
1030
  snapshot(): MessagingSnapshot;
1010
1031
  subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
1011
1032
  setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
1033
+ setConversationsLoadError(message: string | null): void;
1012
1034
  /**
1013
1035
  * Switch the archive axis. Clears the list and marks it UNLOADED: the rows
1014
1036
  * on screen belong to the old filter, and leaving them there while the new
@@ -1118,6 +1140,8 @@ interface MessagingEngine {
1118
1140
  * refreshes the archived count the reveal label prints.
1119
1141
  */
1120
1142
  setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
1143
+ /** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
1144
+ refreshInbox(): Promise<void>;
1121
1145
  /** Re-read how many archived conversations exist, for the reveal's label. */
1122
1146
  refreshArchivedCount(): Promise<void>;
1123
1147
  /** Open a conversation: load its thread and subscribe to its channel. */
@@ -1269,7 +1293,12 @@ interface MessagingProviderProps {
1269
1293
  * (DD-241). There is no `userId` prop, on purpose.
1270
1294
  */
1271
1295
  client: MessagingSupabaseClient | null | undefined;
1272
- /** The active organization. Every write carries it (R5). */
1296
+ /**
1297
+ * The active organization, or `null` when none is selected. Messaging runs
1298
+ * without one: every read is the person's own. It is the destination of a
1299
+ * NEW conversation (refused as `organization-required` without it) and of
1300
+ * AI runs (the AI actions do not render without it).
1301
+ */
1273
1302
  organizationId: string | null | undefined;
1274
1303
  /**
1275
1304
  * The Matrx API transport for AI features. Omit to run messaging without AI;
@@ -1356,6 +1385,10 @@ interface UseConversationsResult {
1356
1385
  hasMore: boolean;
1357
1386
  /** True only on the FIRST load; a refresh must not blank the list. */
1358
1387
  isInitialLoading: boolean;
1388
+ /** The inbox read failed; the list shows the failure (never a skeleton forever, never "empty"). */
1389
+ loadError: string | null;
1390
+ /** Read the inbox again. */
1391
+ retry: () => void;
1359
1392
  totalUnreadConversations: number;
1360
1393
  loadMore: () => void;
1361
1394
  select: (id: ConversationId) => void;
@@ -1778,9 +1811,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1778
1811
 
1779
1812
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1780
1813
  declare function projectMessageAction(value: unknown): MessageAction | null;
1781
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1814
+ declare function projectMessage(row: Record<string, unknown>,
1815
+ /** The active organization, or `null` when none is selected (reads never need one). */
1816
+ fallbackOrganizationId: OrganizationId | null): Message;
1782
1817
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1783
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1818
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1819
+ /** The active organization, or `null` when none is selected (reads never need one). */
1820
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1784
1821
 
1785
1822
  /**
1786
1823
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/react.d.ts CHANGED
@@ -208,7 +208,11 @@ interface Page<TItem, TCursor> {
208
208
  /** What the host injects. Identity ONLY — every hard part is in the package. */
209
209
  interface MessagingIdentity {
210
210
  readonly userId: UserId;
211
- readonly organizationId: OrganizationId;
211
+ /**
212
+ * The active organization, or `null` when none is selected. Reads never
213
+ * depend on it; it is only where a NEW conversation lands.
214
+ */
215
+ readonly organizationId: OrganizationId | null;
212
216
  }
213
217
  interface DraftMessage {
214
218
  readonly conversationId: ConversationId;
@@ -525,7 +529,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
525
529
  * errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
526
530
  * warns and retries; it is never a red error.
527
531
  */
528
- type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured" | "unknown";
532
+ type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
533
+ /** A NEW conversation needs an organization to land in and none is selected. */
534
+ | "organization-required" | "unknown";
529
535
  declare class MessagingError extends Error {
530
536
  readonly code: MessagingErrorCode;
531
537
  /** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
@@ -817,15 +823,21 @@ interface RepositoryOptions {
817
823
  */
818
824
  client: MessagingSupabaseClient;
819
825
  /**
820
- * The active organization. Every conversation and message write carries it
821
- * explicitly (R5).
826
+ * The active organization, or `null`/omitted when none is selected.
827
+ *
828
+ * 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
829
+ * read here is answered by RLS as the session's user; the organization is
830
+ * only the DESTINATION of a NEW conversation. Without one, starting a
831
+ * conversation is refused with `organization-required` (the host asks the
832
+ * person to choose), and a message or member added to an EXISTING
833
+ * conversation is stamped with that conversation's own organization.
822
834
  *
823
835
  * 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
824
836
  * acting is read from `client`'s own session — see `session-identity.ts` for
825
837
  * the class that closed and why a handed-in id could not be made safe by
826
838
  * convention. A caller that still passes `identity` is IGNORED and told so.
827
839
  */
828
- organizationId: string;
840
+ organizationId?: string | null | undefined;
829
841
  /**
830
842
  * INTERNAL. The React provider already runs one session reader for its own
831
843
  * render loop and hands it here so the two cannot answer differently and so
@@ -846,8 +858,11 @@ interface RepositoryOptions {
846
858
  now?: (() => number) | undefined;
847
859
  }
848
860
  interface MessagingRepository {
849
- /** The organization every write here is stamped with. */
850
- readonly organizationId: OrganizationId;
861
+ /**
862
+ * The active organization new conversations land in, or `null` when none is
863
+ * selected — reads work either way.
864
+ */
865
+ readonly organizationId: OrganizationId | null;
851
866
  /**
852
867
  * Who the session says is acting — awaited, because the first `getSession()`
853
868
  * is a real read. Rejects with `session-unavailable` when nobody is signed
@@ -1004,11 +1019,18 @@ interface MessagingSnapshot {
1004
1019
  } | null;
1005
1020
  /** Conversations whose last open failed, keyed by id. Cleared on the next open. */
1006
1021
  readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
1022
+ /**
1023
+ * The inbox read FAILED (and no later read succeeded). Without it a failed
1024
+ * first read left `hasLoadedConversations` false forever — a skeleton that
1025
+ * never resolves is a screen telling a lie (RC-B12 round 11).
1026
+ */
1027
+ readonly conversationsLoadError: string | null;
1007
1028
  }
1008
1029
  interface MessagingStore {
1009
1030
  snapshot(): MessagingSnapshot;
1010
1031
  subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
1011
1032
  setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
1033
+ setConversationsLoadError(message: string | null): void;
1012
1034
  /**
1013
1035
  * Switch the archive axis. Clears the list and marks it UNLOADED: the rows
1014
1036
  * on screen belong to the old filter, and leaving them there while the new
@@ -1118,6 +1140,8 @@ interface MessagingEngine {
1118
1140
  * refreshes the archived count the reveal label prints.
1119
1141
  */
1120
1142
  setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
1143
+ /** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
1144
+ refreshInbox(): Promise<void>;
1121
1145
  /** Re-read how many archived conversations exist, for the reveal's label. */
1122
1146
  refreshArchivedCount(): Promise<void>;
1123
1147
  /** Open a conversation: load its thread and subscribe to its channel. */
@@ -1269,7 +1293,12 @@ interface MessagingProviderProps {
1269
1293
  * (DD-241). There is no `userId` prop, on purpose.
1270
1294
  */
1271
1295
  client: MessagingSupabaseClient | null | undefined;
1272
- /** The active organization. Every write carries it (R5). */
1296
+ /**
1297
+ * The active organization, or `null` when none is selected. Messaging runs
1298
+ * without one: every read is the person's own. It is the destination of a
1299
+ * NEW conversation (refused as `organization-required` without it) and of
1300
+ * AI runs (the AI actions do not render without it).
1301
+ */
1273
1302
  organizationId: string | null | undefined;
1274
1303
  /**
1275
1304
  * The Matrx API transport for AI features. Omit to run messaging without AI;
@@ -1356,6 +1385,10 @@ interface UseConversationsResult {
1356
1385
  hasMore: boolean;
1357
1386
  /** True only on the FIRST load; a refresh must not blank the list. */
1358
1387
  isInitialLoading: boolean;
1388
+ /** The inbox read failed; the list shows the failure (never a skeleton forever, never "empty"). */
1389
+ loadError: string | null;
1390
+ /** Read the inbox again. */
1391
+ retry: () => void;
1359
1392
  totalUnreadConversations: number;
1360
1393
  loadMore: () => void;
1361
1394
  select: (id: ConversationId) => void;
@@ -1778,9 +1811,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
1778
1811
 
1779
1812
  declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
1780
1813
  declare function projectMessageAction(value: unknown): MessageAction | null;
1781
- declare function projectMessage(row: Record<string, unknown>, fallbackOrganizationId: OrganizationId): Message;
1814
+ declare function projectMessage(row: Record<string, unknown>,
1815
+ /** The active organization, or `null` when none is selected (reads never need one). */
1816
+ fallbackOrganizationId: OrganizationId | null): Message;
1782
1817
  declare function projectParticipantRole(value: unknown): ParticipantRole;
1783
- declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId, fallbackOrganizationId: OrganizationId): ConversationSummary;
1818
+ declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
1819
+ /** The active organization, or `null` when none is selected (reads never need one). */
1820
+ fallbackOrganizationId: OrganizationId | null): ConversationSummary;
1784
1821
 
1785
1822
  /**
1786
1823
  * MATRX REFERENCES — a message can name a platform entity, and everything it
package/dist/react.js CHANGED
@@ -762,7 +762,7 @@ function projectMessage(row, fallbackOrganizationId) {
762
762
  id: requiredStr(row, "id", operation),
763
763
  conversationId: requiredStr(row, "conversation_id", operation),
764
764
  senderId: requiredStr(row, "sender_id", operation),
765
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
765
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
766
766
  content: typeof row["content"] === "string" ? row["content"] : "",
767
767
  kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
768
768
  // A row read from the DB is at least `sent`. `sending`/`failed` are outbox
@@ -810,7 +810,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
810
810
  groupName,
811
811
  groupImageUrl,
812
812
  createdBy: createdBy === null ? null : createdBy,
813
- organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
813
+ organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
814
814
  createdAt,
815
815
  updatedAt,
816
816
  metadata: jsonObject(row["metadata"])
@@ -876,6 +876,7 @@ function createMessagingStore() {
876
876
  let threads = /* @__PURE__ */ new Map();
877
877
  let activeConversationId = null;
878
878
  let conversationReadFailures = /* @__PURE__ */ new Map();
879
+ let conversationsLoadError = null;
879
880
  const listeners = /* @__PURE__ */ new Set();
880
881
  let cached = null;
881
882
  function snapshot() {
@@ -889,7 +890,8 @@ function createMessagingStore() {
889
890
  totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
890
891
  archiveFilter,
891
892
  archivedCount,
892
- conversationReadFailures
893
+ conversationReadFailures,
894
+ conversationsLoadError
893
895
  };
894
896
  return cached;
895
897
  }
@@ -934,6 +936,12 @@ function createMessagingStore() {
934
936
  conversations = normalizeConversations(items);
935
937
  hasMoreConversations = hasMore;
936
938
  hasLoadedConversations = true;
939
+ conversationsLoadError = null;
940
+ emit();
941
+ },
942
+ setConversationsLoadError(message) {
943
+ if (message === conversationsLoadError) return;
944
+ conversationsLoadError = message;
937
945
  emit();
938
946
  },
939
947
  setArchiveFilter(next) {
@@ -1143,7 +1151,11 @@ function createMessagingEngine(options) {
1143
1151
  optimisticMessage({
1144
1152
  conversationId: entry.draft.conversationId,
1145
1153
  senderId: identity.userId,
1146
- organizationId: identity.organizationId,
1154
+ // Client-only until the confirmed row replaces it: the active
1155
+ // organization, else the conversation's own.
1156
+ organizationId: identity.organizationId ?? store.snapshot().conversations.find(
1157
+ (item) => item.conversation.id === entry.draft.conversationId
1158
+ )?.conversation.organizationId ?? "",
1147
1159
  content: entry.draft.content,
1148
1160
  clientMessageId: entry.clientMessageId,
1149
1161
  ...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
@@ -1162,10 +1174,20 @@ function createMessagingEngine(options) {
1162
1174
  }
1163
1175
  async function reloadInbox() {
1164
1176
  if (disposed) return;
1165
- const page = await repository.listConversations({
1166
- limit: conversationPageSize,
1167
- archived: store.snapshot().archiveFilter
1168
- });
1177
+ let page;
1178
+ try {
1179
+ page = await repository.listConversations({
1180
+ limit: conversationPageSize,
1181
+ archived: store.snapshot().archiveFilter
1182
+ });
1183
+ } catch (error) {
1184
+ if (!disposed) {
1185
+ store.setConversationsLoadError(
1186
+ error instanceof Error ? error.message : typeof error === "string" ? error : "The conversation list could not be read."
1187
+ );
1188
+ }
1189
+ throw error;
1190
+ }
1169
1191
  if (disposed) return;
1170
1192
  conversationCursor = page.nextCursor;
1171
1193
  store.setConversations(page.items, page.hasMore);
@@ -1250,6 +1272,13 @@ function createMessagingEngine(options) {
1250
1272
  }
1251
1273
  });
1252
1274
  },
1275
+ async refreshInbox() {
1276
+ try {
1277
+ await reloadInbox();
1278
+ } catch (error) {
1279
+ reportError(error, "inboxRefresh");
1280
+ }
1281
+ },
1253
1282
  async setArchiveFilter(next) {
1254
1283
  if (next === store.snapshot().archiveFilter) return;
1255
1284
  store.setArchiveFilter(next);
@@ -1632,12 +1661,15 @@ var RPCS = {
1632
1661
  };
1633
1662
  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";
1634
1663
  var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
1664
+ function optionalOrg(organizationId) {
1665
+ return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
1666
+ }
1635
1667
  function requireOrg(organizationId, operation) {
1636
- if (typeof organizationId !== "string" || organizationId.length === 0) {
1668
+ if (organizationId === null) {
1637
1669
  throw new MessagingError(
1638
- "misconfigured",
1639
- `${operation}: no organization_id`,
1640
- "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."
1670
+ "organization-required",
1671
+ `${operation}: no organization selected`,
1672
+ "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."
1641
1673
  );
1642
1674
  }
1643
1675
  return organizationId;
@@ -1649,7 +1681,7 @@ function sleep(ms) {
1649
1681
  return new Promise((resolve) => setTimeout(resolve, jittered));
1650
1682
  }
1651
1683
  function createMessagingRepository(options) {
1652
- const org = requireOrg(options.organizationId, "createMessagingRepository");
1684
+ const org = optionalOrg(options.organizationId);
1653
1685
  const handedIn = options.identity;
1654
1686
  if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
1655
1687
  options.onDiagnostic?.(
@@ -1719,6 +1751,11 @@ function createMessagingRepository(options) {
1719
1751
  });
1720
1752
  return found;
1721
1753
  }
1754
+ async function organizationOfExisting(conversationId) {
1755
+ if (org !== null) return org;
1756
+ const conversation = await repository.getConversation(conversationId);
1757
+ return conversation.organizationId;
1758
+ }
1722
1759
  const repository = {
1723
1760
  organizationId: org,
1724
1761
  actingUserId,
@@ -1851,6 +1888,7 @@ function createMessagingRepository(options) {
1851
1888
  },
1852
1889
  async getOrCreateDirectConversation(otherUserId) {
1853
1890
  const operation = "getOrCreateDirectConversation";
1891
+ const destination = requireOrg(org, operation);
1854
1892
  const me = await actingUser(operation);
1855
1893
  const id = await withSessionRetry(
1856
1894
  operation,
@@ -1862,7 +1900,7 @@ function createMessagingRepository(options) {
1862
1900
  // not a preference.
1863
1901
  p_user1_id: me,
1864
1902
  p_user2_id: otherUserId,
1865
- p_organization_id: org
1903
+ p_organization_id: destination
1866
1904
  },
1867
1905
  operation
1868
1906
  )
@@ -1874,13 +1912,14 @@ function createMessagingRepository(options) {
1874
1912
  },
1875
1913
  async createGroupConversation({ name, memberIds }) {
1876
1914
  const operation = "createGroupConversation";
1915
+ const destination = requireOrg(org, operation);
1877
1916
  const me = await actingUser(operation);
1878
1917
  const { data, error } = await withSessionRetry(
1879
1918
  operation,
1880
1919
  async () => db().from(TABLES.conversations).insert({
1881
1920
  type: "group",
1882
1921
  group_name: name,
1883
- organization_id: org,
1922
+ organization_id: destination,
1884
1923
  created_by: me
1885
1924
  }).select("id").single()
1886
1925
  );
@@ -1895,7 +1934,7 @@ function createMessagingRepository(options) {
1895
1934
  conversation_id: conversationId,
1896
1935
  user_id: userId,
1897
1936
  role: userId === me ? "owner" : "member",
1898
- organization_id: org,
1937
+ organization_id: destination,
1899
1938
  created_by: me
1900
1939
  }))
1901
1940
  );
@@ -1905,10 +1944,11 @@ function createMessagingRepository(options) {
1905
1944
  async insertMessage(draft, clientMessageId) {
1906
1945
  const operation = "insertMessage";
1907
1946
  const me = await actingUser(operation);
1947
+ const destination = await organizationOfExisting(draft.conversationId);
1908
1948
  const { data, error } = await db().from(TABLES.messages).insert({
1909
1949
  conversation_id: draft.conversationId,
1910
1950
  sender_id: me,
1911
- organization_id: org,
1951
+ organization_id: destination,
1912
1952
  created_by: me,
1913
1953
  content: draft.content,
1914
1954
  message_type: draft.kind ?? "text",
@@ -1969,12 +2009,13 @@ function createMessagingRepository(options) {
1969
2009
  async addMembers(conversationId, memberIds) {
1970
2010
  const operation = "addMembers";
1971
2011
  const me = await actingUser(operation);
2012
+ const destination = await organizationOfExisting(conversationId);
1972
2013
  const { error } = await db().from(TABLES.participants).insert(
1973
2014
  memberIds.map((userId) => ({
1974
2015
  conversation_id: conversationId,
1975
2016
  user_id: userId,
1976
2017
  role: "member",
1977
- organization_id: org,
2018
+ organization_id: destination,
1978
2019
  created_by: me
1979
2020
  }))
1980
2021
  );
@@ -2085,7 +2126,7 @@ function MessagingProvider(props) {
2085
2126
  }, [strayUserId]);
2086
2127
  const session = useSessionUserId(client, props.onDiagnostic);
2087
2128
  const userId = session.snapshot.userId;
2088
- const ready = client != null && userId !== null && typeof organizationId === "string" && organizationId.length > 0;
2129
+ const ready = client != null && userId !== null;
2089
2130
  const hostProvidesRealtime = useIsRealtimeProviderMounted();
2090
2131
  const runtime = /* @__PURE__ */ jsx(
2091
2132
  MessagingRuntime,
@@ -2121,12 +2162,13 @@ function MessagingRuntime(props) {
2121
2162
  actionRegistry.register(handler);
2122
2163
  });
2123
2164
  }, [actionRegistry, handlers]);
2124
- const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null && typeof organizationId === "string" && organizationId.length > 0;
2165
+ const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null;
2166
+ const activeOrganizationId = typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
2125
2167
  const engine = useMemo(() => {
2126
2168
  if (!ready || manager === null || sessionIdentity === null || userId === null) return null;
2127
2169
  const identity = {
2128
2170
  userId,
2129
- organizationId
2171
+ organizationId: activeOrganizationId
2130
2172
  };
2131
2173
  const report = (event) => {
2132
2174
  (diagnosticRef.current ?? defaultDiagnostics)(event);
@@ -2137,7 +2179,7 @@ function MessagingRuntime(props) {
2137
2179
  // supabase-shape.ts); everything below this line works against the rich
2138
2180
  // contract, which `supabase-shape.test.ts` proves a real client honors.
2139
2181
  client,
2140
- organizationId,
2182
+ organizationId: activeOrganizationId,
2141
2183
  // THE SAME READER the render loop above uses. Two readers would be two
2142
2184
  // answers during the tick an account switch takes.
2143
2185
  sessionIdentity,
@@ -2170,14 +2212,14 @@ function MessagingRuntime(props) {
2170
2212
  }
2171
2213
  });
2172
2214
  return built;
2173
- }, [ready, manager, sessionIdentity, userId, organizationId]);
2215
+ }, [ready, manager, sessionIdentity, userId, activeOrganizationId]);
2174
2216
  useEffect(() => {
2175
2217
  if (engine === null) return void 0;
2176
2218
  void engine.start().catch((error) => {
2177
2219
  (diagnosticRef.current ?? defaultDiagnostics)({
2178
2220
  level: "error",
2179
2221
  message: `Messaging failed to start: ${String(error?.message ?? error)}`,
2180
- remedy: "Check the signed-in session and organization, then reload the surface."
2222
+ remedy: "Check the signed-in session, then reload the surface."
2181
2223
  });
2182
2224
  });
2183
2225
  return () => {
@@ -2191,9 +2233,10 @@ function MessagingRuntime(props) {
2191
2233
  const sourceFeature = props.sourceFeature;
2192
2234
  const ai = useMemo(() => {
2193
2235
  if (transport === void 0 || agents === void 0 || !ready) return null;
2236
+ if (activeOrganizationId === null) return null;
2194
2237
  return createMessagingAi({
2195
2238
  transport,
2196
- organizationId,
2239
+ organizationId: activeOrganizationId,
2197
2240
  agents,
2198
2241
  ...maxTranscriptMessages !== void 0 ? { maxTranscriptMessages } : {},
2199
2242
  ...sourceApp !== void 0 ? { sourceApp } : {},
@@ -2203,7 +2246,7 @@ function MessagingRuntime(props) {
2203
2246
  transport,
2204
2247
  agents,
2205
2248
  ready,
2206
- organizationId,
2249
+ activeOrganizationId,
2207
2250
  maxTranscriptMessages,
2208
2251
  sourceApp,
2209
2252
  sourceFeature
@@ -2254,7 +2297,7 @@ function useRequiredMessagingHost() {
2254
2297
  const host = useMessagingHost();
2255
2298
  if (host === null) {
2256
2299
  throw new Error(
2257
- "[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} userId={userId} organizationId={orgId}> and render it only once auth has resolved."
2300
+ "[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} organizationId={orgId}> and render it only once auth has resolved."
2258
2301
  );
2259
2302
  }
2260
2303
  return host;
@@ -2379,7 +2422,11 @@ function useConversations() {
2379
2422
  hasMore: snapshot?.hasMoreConversations ?? false,
2380
2423
  // "Not loaded yet" is a FACT from the store, never a guess. Showing
2381
2424
  // "No conversations yet" before the first read is a screen telling a lie.
2382
- isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations,
2425
+ isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations && snapshot.conversationsLoadError === null,
2426
+ loadError: snapshot?.conversationsLoadError ?? null,
2427
+ retry: () => {
2428
+ void host?.engine.refreshInbox();
2429
+ },
2383
2430
  totalUnreadConversations: snapshot?.totalUnreadConversations ?? 0,
2384
2431
  loadMore: () => {
2385
2432
  void host?.engine.loadMoreConversations();
@@ -2644,7 +2691,8 @@ function useMessageAction(message) {
2644
2691
  const context = {
2645
2692
  messageId: message.id,
2646
2693
  actorId: host.identity.userId,
2647
- organizationId: host.identity.organizationId,
2694
+ // The active organization, else the organization the message lives in.
2695
+ organizationId: host.identity.organizationId ?? message.organizationId,
2648
2696
  choice
2649
2697
  };
2650
2698
  host.actions.execute(action, context).then(setReceipt).catch((cause) => {
@@ -2965,6 +3013,7 @@ function EmptyState(props) {
2965
3013
  }
2966
3014
 
2967
3015
  // src/react/components.tsx
3016
+ import { ErrorBox } from "@ai-matrx/design-system";
2968
3017
  import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
2969
3018
  var AI_LABELS = {
2970
3019
  catchUp: "Catch me up",
@@ -2982,7 +3031,9 @@ function ConversationList(props) {
2982
3031
  activeConversationId,
2983
3032
  archiveFilter,
2984
3033
  setArchiveFilter,
2985
- archivedCount
3034
+ archivedCount,
3035
+ loadError,
3036
+ retry
2986
3037
  } = useConversations();
2987
3038
  const RowChrome = useMessagingHost()?.wrapConversationRow ?? null;
2988
3039
  const [query, setQuery] = useState2("");
@@ -3047,7 +3098,20 @@ function ConversationList(props) {
3047
3098
  }
3048
3099
  ) }) : null,
3049
3100
  /* @__PURE__ */ jsxs2("div", { className: "mx-msg__scroll", children: [
3050
- isInitialLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, {}) : visible.length === 0 ? /* @__PURE__ */ jsx4(
3101
+ isInitialLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, {}) : loadError !== null && visible.length === 0 ? (
3102
+ // A failed read resolves to the failure — never a skeleton forever,
3103
+ // never "No conversations yet" (RC-B12 round 11). ErrorBox is the
3104
+ // design-system error slot: the host fills it with its menu.
3105
+ /* @__PURE__ */ jsx4(
3106
+ ReadFailureBox,
3107
+ {
3108
+ title: "Couldn't load your conversations",
3109
+ message: loadError,
3110
+ operation: "List your conversations",
3111
+ onRetry: retry
3112
+ }
3113
+ )
3114
+ ) : visible.length === 0 ? /* @__PURE__ */ jsx4(
3051
3115
  EmptyState,
3052
3116
  {
3053
3117
  title: query.length > 0 ? "No matches" : showingArchive ? "No archived conversations" : "No conversations yet",
@@ -3224,10 +3288,11 @@ function ConversationView(props) {
3224
3288
  body: "It doesn't exist, or you aren't a member of it."
3225
3289
  }
3226
3290
  ) : conversation.error !== null && messages.length === 0 ? /* @__PURE__ */ jsx4(
3227
- EmptyState,
3291
+ ReadFailureBox,
3228
3292
  {
3229
3293
  title: "Couldn't load this conversation",
3230
- body: "Check your connection, then open it again."
3294
+ message: conversation.error.message || "Check your connection, then open it again.",
3295
+ operation: "Open this conversation"
3231
3296
  }
3232
3297
  ) : messages.length === 0 ? /* @__PURE__ */ jsx4(EmptyState, { title: "No messages yet", body: "Say something to get started." }) : /* @__PURE__ */ jsxs2("div", { className: "mx-msg__messages", children: [
3233
3298
  conversation.hasMoreOlder ? /* @__PURE__ */ jsx4(
@@ -3558,6 +3623,29 @@ function MessagingInbox(props) {
3558
3623
  }
3559
3624
  );
3560
3625
  }
3626
+ function ReadFailureBox({
3627
+ title,
3628
+ message,
3629
+ operation,
3630
+ onRetry
3631
+ }) {
3632
+ return /* @__PURE__ */ jsxs2("div", { className: "mx-msg__empty", role: "alert", children: [
3633
+ /* @__PURE__ */ jsx4(
3634
+ ErrorBox,
3635
+ {
3636
+ as: "div",
3637
+ title,
3638
+ message,
3639
+ operation,
3640
+ origin: "messaging",
3641
+ style: { color: "var(--mx-msg-danger)", fontWeight: 600 },
3642
+ children: title
3643
+ }
3644
+ ),
3645
+ /* @__PURE__ */ jsx4("p", { children: message }),
3646
+ onRetry ? /* @__PURE__ */ jsx4("button", { type: "button", className: "mx-msg__button mx-msg__button--ghost", onClick: onRetry, children: "Try again" }) : null
3647
+ ] });
3648
+ }
3561
3649
  export {
3562
3650
  AgentTag,
3563
3651
  AlertIcon,