@ai-matrx/messaging 0.11.1 → 0.11.3

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,50 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.11.3 — 2026-09-12
4
+
5
+ ### Fixed
6
+
7
+ - **"listConversations: deadlock detected" on the inbox's first load.** An independent
8
+ reviewer opening `/chat` on 2026-09-12 got that banner from the messaging surface; a reload
9
+ worked. The read writes nothing — `get_dm_conversations_with_details` is `STABLE` on the live
10
+ database and so is `get_dm_unread_count`. A pure read still joins a deadlock cycle whenever a
11
+ concurrent DDL session (migrations apply against live traffic here all day; four applied in the
12
+ ten minutes around the sighting) wants `AccessExclusiveLock` on a table the read already holds
13
+ `AccessShareLock` on, and Postgres kills whichever party's `deadlock_timeout` expires first —
14
+ often the reader. Reproduced on a throwaway Postgres 17: a two-statement read-only transaction
15
+ against one `lock table … access exclusive` + `alter table` session dies `40P01` with
16
+ `waits for AccessShareLock … blocked by …` — the same DETAIL shape the platform logged for
17
+ `chat.conversation` the same day.
18
+ `normalizeMessagingError` now classifies `40P01`, `40001` and `55P03` as a new `contention`
19
+ code, and the repository's one retry became two: the cancelled call is re-issued up to three
20
+ times with a short jittered backoff (the victim is rolled back in full, so re-running is safe —
21
+ for writes too), and **every heal announces itself** through the new `onDiagnostic` option,
22
+ which the React provider wires to the host's diagnostics as a warning. Only an exhausted retry
23
+ budget reaches a person, and it no longer speaks Postgres at them. `core/repository.test.ts`
24
+ pins all four behaviours, including that a `42501` is never retried as contention.
25
+
26
+ ### Consumer action (C28)
27
+
28
+ None required. A host that builds a repository directly may pass `onDiagnostic` to see the heals;
29
+ `<MessagingProvider>` already does.
30
+
31
+ ## 0.11.2 — 2026-09-11
32
+
33
+ ### Fixed
34
+
35
+ - **The AI answer was squeezed to a sliver.** `.mx-msg__ai-output` capped itself at `30dvh`
36
+ with `overflow-y: auto`, but declared no `flex-shrink: 0` inside `.mx-msg__thread`'s flex
37
+ column — whose scroll region is `flex: 1 1 auto` with a content-sized basis. In the first
38
+ live "Catch me up" run in matrx-frontend (2026-09-11) the answer rendered as a 45px box holding
39
+ 270px of text and no visible scrollbar: a complete answer the reader could not see. The same
40
+ hole was open on `.mx-msg__ai-bar`. Both now declare `flex: 0 0 auto`, and
41
+ `src/react/thread-rows.test.ts` pins every fixed row of the thread column (header, AI bar, AI
42
+ output, composer) so the next fixed row cannot re-open the class.
43
+
44
+ ### Consumer action (C28)
45
+
46
+ None. Upgrade to receive the stylesheet fix; no API changed.
47
+
3
48
  ## 0.11.1
4
49
 
5
50
  Automatic changed-only republish (docs/metadata drift since the last tag — see
package/dist/index.cjs CHANGED
@@ -229,14 +229,15 @@ var MessagingError = class extends Error {
229
229
  this.remedy = remedy;
230
230
  if (cause !== void 0) this.cause = cause;
231
231
  }
232
- /** True when retrying after the host re-establishes a session is the fix. */
232
+ /** True when re-running the same call is the fix. */
233
233
  get isRetryable() {
234
- return this.code === "session-unavailable" || this.code === "transport";
234
+ return this.code === "session-unavailable" || this.code === "contention" || this.code === "transport";
235
235
  }
236
236
  };
237
237
  var FORBIDDEN_CODES = /* @__PURE__ */ new Set(["42501", "PGRST301", "PGRST302"]);
238
238
  var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["PGRST116", "PGRST205"]);
239
239
  var CONFLICT_CODES = /* @__PURE__ */ new Set(["23505"]);
240
+ var CONTENTION_CODES = /* @__PURE__ */ new Set(["40P01", "40001", "55P03"]);
240
241
  var SESSION_MARKERS = [
241
242
  "auth session missing",
242
243
  "jwt expired",
@@ -258,6 +259,14 @@ function normalizeMessagingError(error, operation) {
258
259
  error
259
260
  );
260
261
  }
262
+ if (code !== void 0 && CONTENTION_CODES.has(code)) {
263
+ return new MessagingError(
264
+ "contention",
265
+ `${operation}: the database resolved a lock collision by cancelling this call (${code}: ${message})`,
266
+ "Nothing of this call committed, so it is safe to re-run \u2014 the package re-runs it a bounded number of times and announces each heal. Seeing it repeatedly means real contention on the messaging tables (usually DDL applied against live traffic); look at what else held a lock at that moment, never at the caller.",
267
+ error
268
+ );
269
+ }
261
270
  if (code !== void 0 && FORBIDDEN_CODES.has(code)) {
262
271
  return new MessagingError(
263
272
  "forbidden",
@@ -285,7 +294,7 @@ function normalizeMessagingError(error, operation) {
285
294
  return new MessagingError(
286
295
  "transport",
287
296
  `${operation}: ${message}`,
288
- "Transient transport failure. The package retries reads; a write is surfaced to the outbox so the typed message is never lost.",
297
+ "Transient transport failure. A typed message is surfaced to the outbox so it is never lost; a read is the caller's to re-issue (only `contention` and `session-unavailable` are re-run inside the package).",
289
298
  error
290
299
  );
291
300
  }
@@ -1681,6 +1690,12 @@ function requireOrg(organizationId, operation) {
1681
1690
  }
1682
1691
  return organizationId;
1683
1692
  }
1693
+ var CONTENTION_ATTEMPTS = 3;
1694
+ var CONTENTION_BACKOFF_MS = [60, 180];
1695
+ function sleep(ms) {
1696
+ const jittered = ms + Math.random() * ms * 0.5;
1697
+ return new Promise((resolve) => setTimeout(resolve, jittered));
1698
+ }
1684
1699
  function createMessagingRepository(options) {
1685
1700
  const { identity } = options;
1686
1701
  const org = requireOrg(identity.organizationId, "createMessagingRepository");
@@ -1692,18 +1707,31 @@ function createMessagingRepository(options) {
1692
1707
  const db = () => client.schema(MESSAGING_SCHEMA);
1693
1708
  const rpcDb = () => client.schema(MESSAGING_RPC_SCHEMA);
1694
1709
  async function withSessionRetry(operation, run) {
1695
- try {
1696
- return await run();
1697
- } catch (error) {
1698
- const normalized = normalizeMessagingError(error, operation);
1699
- if (normalized.code !== "session-unavailable" || options.resolveSession === void 0) {
1700
- throw normalized;
1701
- }
1702
- await options.resolveSession();
1710
+ let sessionRetried = false;
1711
+ for (let attempt = 1; ; attempt += 1) {
1703
1712
  try {
1704
- return await run();
1705
- } catch (retryError) {
1706
- throw normalizeMessagingError(retryError, operation);
1713
+ const value = await run();
1714
+ if (attempt > 1) {
1715
+ options.onDiagnostic?.(
1716
+ `${operation}: healed on attempt ${attempt} after a database lock collision (rolled back, safe to re-run).`
1717
+ );
1718
+ }
1719
+ return value;
1720
+ } catch (error) {
1721
+ const normalized = normalizeMessagingError(error, operation);
1722
+ if (normalized.code === "session-unavailable" && options.resolveSession !== void 0 && !sessionRetried) {
1723
+ sessionRetried = true;
1724
+ await options.resolveSession();
1725
+ continue;
1726
+ }
1727
+ if (normalized.code === "contention" && attempt < CONTENTION_ATTEMPTS) {
1728
+ options.onDiagnostic?.(
1729
+ `${operation}: attempt ${attempt}/${CONTENTION_ATTEMPTS} hit a database lock collision (${normalized.message}) \u2014 re-running in ${CONTENTION_BACKOFF_MS[attempt - 1] ?? 0}ms.`
1730
+ );
1731
+ await sleep(CONTENTION_BACKOFF_MS[attempt - 1] ?? 0);
1732
+ continue;
1733
+ }
1734
+ throw normalized;
1707
1735
  }
1708
1736
  }
1709
1737
  }