@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 +45 -0
- package/dist/index.cjs +42 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -2
- package/dist/index.d.ts +8 -2
- package/dist/index.js +42 -14
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +46 -14
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +8 -2
- package/dist/react.d.ts +8 -2
- package/dist/react.js +46 -14
- package/dist/react.js.map +1 -1
- package/dist/styles.css +9 -0
- package/package.json +3 -3
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
|
|
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.
|
|
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
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
}
|