@ai-matrx/messaging 0.8.0 → 0.10.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,76 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.10.0 — 2026-09-07
4
+
5
+ **The four AI-native intelligences rendered nothing in the first host that shipped them, and
6
+ would have shown a skeleton instead of an answer if they had.** Three defects, all found by
7
+ wiring the package to a real switching layer rather than by reading it.
8
+
9
+ ### Changed (breaking, one line at the call site)
10
+
11
+ - **`agents` now takes an IDENTITY per capability, not a bare id.**
12
+ `{ summarize: "id" }` becomes `{ summarize: { agentId: "id", configOverrides } }`. A host that
13
+ resolves a capability's agent from a switching layer resolves a PAIR — which agent, and the
14
+ settings that layer decided for it. Taking only the id silently dropped the settings half on
15
+ every call. `configOverrides` rides to the server as the turn's `config_overrides`, verbatim;
16
+ omitted, the field is not sent at all and the agent's own settings stand.
17
+
18
+ ### Added
19
+
20
+ - **AI runs STREAM.** Every capability takes `onText`, and `useMessagingAi` exposes
21
+ `partialText` + `activeCapability`. `ConversationView` renders the answer as it arrives; the
22
+ skeleton survives only for the gap before the first chunk, where it now names the job that is
23
+ running. A placeholder that sat until a run finished was a spinner standing in for an answer.
24
+ - **`<MessagingProvider maxTranscriptMessages>`** — the provider never forwarded the core's
25
+ transcript cap, so the 200 in this package was unreachable taste. A host with an
26
+ organization-configurable knob passes the resolved number.
27
+ - **`unreadCutoff(messages, unreadCount)`**, exported from the framework-free core.
28
+
29
+ ### Fixed
30
+
31
+ - **"Catch me up" was a second "Summarize".** It always passed `since: null`, so it sent the
32
+ whole loaded window and answered the question nobody asked. It now derives the exclusive
33
+ cutoff from the store's own unread count — the message just before the first unread one — and
34
+ passes `null` only when there is genuinely no honest cutoff (nothing unread, or a loaded
35
+ window that does not reach back that far). Never a guessed timestamp.
36
+
37
+ ### Consumer action (C28)
38
+
39
+ Change `agents={{ summarize: id }}` to `agents={{ summarize: { agentId: id } }}` and pass
40
+ `configOverrides` when your resolution layer produces them. If you render the AI output
41
+ yourself, render `partialText` while `isRunning`.
42
+
43
+ ## 0.9.0 — 2026-09-07
44
+
45
+ **`<MessagingProvider client={supabase}>` did not typecheck for a host with generated Supabase
46
+ types** — the one line the README tells every consumer to write.
47
+
48
+ ### Fixed
49
+
50
+ - **The public `client` prop type is now shallow.** Checking a fully-typed
51
+ `SupabaseClient<Database>` against the rich internal shape makes tsc chase PostgREST's schema
52
+ machinery until it bails with `TS2589: Type instantiation is excessively deep and possibly
53
+ infinite`, followed by a page of "not assignable" — so the only way to pass the client a host
54
+ already has was the cast this package's own header forbids. The boundary is method names and
55
+ arity with `unknown` results; the rich contract (`MessagingSupabaseInternal`, formerly the
56
+ public type's body) is still what everything inside works against, and the provider narrows to
57
+ it ONCE, in-package. Same lesson as data 0.2.1: type a public boundary over `unknown`, never
58
+ over a strict recursive type the host's generator also produces.
59
+ - **`createMessagingRepository` takes the same shallow client**, so the framework-free path a
60
+ service module uses (the README's "Outside React" section) compiles for the same host that the
61
+ provider does. Both legs are proven at compile time.
62
+ - **`supabase-shape.test.ts` now proves the consumer's leg**: a `SupabaseClient<Database>` is
63
+ assignable to `MessagingSupabaseClient`. The old proof deliberately checked only the CALLS,
64
+ noting that whole-interface assignability "makes tsc bail with TS2589, which is a compiler
65
+ limit, not a contract failure" — true, and precisely why the consumer could not compile. The
66
+ limit was the contract failure.
67
+
68
+ ### Consumer action (C28)
69
+
70
+ None, and one less cast: pass your `SupabaseClient` straight in. A host that had written
71
+ `client={supabase as never}` can delete the cast. `MessagingSupabaseInternal` is exported for
72
+ anyone building the repository directly.
73
+
3
74
  ## 0.8.0 — 2026-09-07
4
75
 
5
76
  **Every conversation in the first real inbox was titled with a raw UUID.** The second live defect
package/README.md CHANGED
@@ -166,14 +166,33 @@ messages, keyset pagination, and the whole polished UI. There is nothing else to
166
166
  organizationId={org.id}
167
167
  transport={matrxTransport} // from @ai-matrx/agents/matrx
168
168
  agents={{
169
- catchUp: "…", summarize: "…", actionItems: "", draftReply: "…",
169
+ catchUp: { agentId: "…", configOverrides: { } },
170
+ summarize: { agentId: "…" },
171
+ actionItems: { agentId: "…" },
172
+ draftReply: { agentId: "…" },
170
173
  }}
174
+ maxTranscriptMessages={200} // your knob, not our taste
171
175
  >
172
176
  ```
173
177
 
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.
178
+ Agent definitions live in the **database**, never in this package — the injected **identity** is
179
+ the only part a host supplies. A capability with no identity **does not render at all**; it is
180
+ never a dead button that fails when pressed.
181
+
182
+ **An identity is both halves.** If your app resolves a capability's agent from a switching layer
183
+ that also decides settings for the job, pass `configOverrides` with the id — it is sent verbatim
184
+ as the turn's `config_overrides`. Passing the id alone silently drops the settings half. Omit it
185
+ and nothing is sent, so the agent's own settings stand.
186
+
187
+ **A run the user watches streams.** `useMessagingAi` reports `partialText` after every chunk and
188
+ `ConversationView` renders it; the skeleton appears only in the gap before the first chunk, and
189
+ names the job it is waiting on. If you render the output yourself, render `partialText` while
190
+ `isRunning` — a placeholder that sits until the run finishes is a spinner standing in for an
191
+ answer.
192
+
193
+ **"Catch me up" means the unread tail**, derived from the store's own unread count. Nothing
194
+ unread, or a loaded window that does not reach back to the read mark, means there is no honest
195
+ cutoff and the whole window applies — never a guessed timestamp.
177
196
 
178
197
  ### Making references open
179
198
 
package/dist/index.cjs CHANGED
@@ -63,7 +63,8 @@ __export(src_exports, {
63
63
  projectUserSummary: () => projectUserSummary,
64
64
  resolveActor: () => resolveActor,
65
65
  splitText: () => splitText,
66
- summarizeText: () => summarizeText
66
+ summarizeText: () => summarizeText,
67
+ unreadCutoff: () => unreadCutoff
67
68
  });
68
69
  module.exports = __toCommonJS(src_exports);
69
70
 
@@ -357,6 +358,12 @@ ${JSON.stringify(references, null, 2)}
357
358
  }
358
359
 
359
360
  // src/core/ai.ts
361
+ function unreadCutoff(messages, unreadCount) {
362
+ if (unreadCount <= 0) return null;
363
+ const index = messages.length - unreadCount - 1;
364
+ if (index < 0) return null;
365
+ return messages[index]?.createdAt ?? null;
366
+ }
360
367
  function nameOf(participants, senderId) {
361
368
  return participants.find((p) => p.userId === senderId)?.displayName ?? senderId;
362
369
  }
@@ -377,22 +384,23 @@ function buildTranscript(messages, participants, limit, since) {
377
384
  }
378
385
  function createMessagingAi(options) {
379
386
  const limit = options.maxTranscriptMessages ?? 200;
380
- function agentFor(capability) {
381
- const agentId = options.agents[capability];
382
- if (typeof agentId !== "string" || agentId.length === 0) {
387
+ function identityFor(capability) {
388
+ const identity = options.agents[capability];
389
+ if (identity === void 0 || identity.agentId.length === 0) {
383
390
  throw new MessagingError(
384
391
  "misconfigured",
385
392
  `Messaging AI capability "${capability}" has no agent configured`,
386
- `Pass agents={{ ${capability}: "<agent-id>" }} to <MessagingProvider>. Agent definitions live in the database, never in this package \u2014 the id is the only part a host injects. Until then the UI hides this action rather than offering a button that cannot work.`
393
+ `Pass agents={{ ${capability}: { agentId: "<agent-id>" } }} to <MessagingProvider>. Agent definitions live in the database, never in this package \u2014 the identity is the only part a host injects. Until then the UI hides this action rather than offering a button that cannot work.`
387
394
  );
388
395
  }
389
- return agentId;
396
+ return identity;
390
397
  }
391
- async function run(capability, variables, userInput, signal) {
392
- const agentId = agentFor(capability);
398
+ async function run(capability, variables, userInput, signal, onText) {
399
+ const identity = identityFor(capability);
400
+ const overrides = identity.configOverrides;
393
401
  const completed = await (0, import_matrx.runAgentToCompletion)(
394
402
  options.transport,
395
- agentId,
403
+ identity.agentId,
396
404
  {
397
405
  ...(0, import_matrx.newEphemeralConversationStart)(),
398
406
  organization_id: options.organizationId,
@@ -401,9 +409,15 @@ function createMessagingAi(options) {
401
409
  initiation: "user",
402
410
  // THE USER-INPUT LAW: structured content is NEVER here.
403
411
  ...userInput !== null ? { user_input: userInput } : {},
412
+ // The settings half of the injected identity, verbatim. An absent one
413
+ // is OMITTED rather than sent as null: the agent's own settings stand.
414
+ ...overrides != null ? { config_overrides: overrides } : {},
404
415
  variables
405
416
  },
406
- signal !== void 0 ? { signal } : {}
417
+ {
418
+ ...signal !== void 0 ? { signal } : {},
419
+ ...onText !== void 0 ? { onChunk: onText } : {}
420
+ }
407
421
  );
408
422
  return {
409
423
  capability,
@@ -436,27 +450,26 @@ function createMessagingAi(options) {
436
450
  ...args.since != null ? { unread_since: args.since } : {}
437
451
  };
438
452
  }
453
+ function configured(capability) {
454
+ const identity = options.agents[capability];
455
+ return identity !== void 0 && identity.agentId.length > 0;
456
+ }
439
457
  return {
440
458
  available: () => ["catchUp", "summarize", "actionItems", "draftReply"].filter(
441
- (capability) => {
442
- const id = options.agents[capability];
443
- return typeof id === "string" && id.length > 0;
444
- }
459
+ configured
445
460
  ),
446
- isAvailable(capability) {
447
- const id = options.agents[capability];
448
- return typeof id === "string" && id.length > 0;
449
- },
450
- catchMeUp: (args) => run("catchUp", variablesFor(args), null, args.signal),
451
- summarize: (args) => run("summarize", variablesFor(args), null, args.signal),
452
- extractActionItems: (args) => run("actionItems", variablesFor(args), null, args.signal),
461
+ isAvailable: configured,
462
+ catchMeUp: (args) => run("catchUp", variablesFor(args), null, args.signal, args.onText),
463
+ summarize: (args) => run("summarize", variablesFor(args), null, args.signal, args.onText),
464
+ extractActionItems: (args) => run("actionItems", variablesFor(args), null, args.signal, args.onText),
453
465
  draftReply: (args) => run(
454
466
  "draftReply",
455
467
  variablesFor(args),
456
468
  // The ONE genuine human utterance in this module: what the user asked
457
469
  // the drafter for. Everything else rode `variables`.
458
470
  args.instruction !== void 0 && args.instruction.trim().length > 0 ? args.instruction.trim() : null,
459
- args.signal
471
+ args.signal,
472
+ args.onText
460
473
  )
461
474
  };
462
475
  }
@@ -1571,12 +1584,13 @@ function requireOrg(organizationId, operation) {
1571
1584
  return organizationId;
1572
1585
  }
1573
1586
  function createMessagingRepository(options) {
1574
- const { client, identity } = options;
1587
+ const { identity } = options;
1575
1588
  const org = requireOrg(identity.organizationId, "createMessagingRepository");
1576
1589
  const userCache = createReadCache({
1577
1590
  ttlMs: options.userTtlMs ?? 5 * 6e4,
1578
1591
  ...options.now !== void 0 ? { now: options.now } : {}
1579
1592
  });
1593
+ const client = options.client;
1580
1594
  const db = () => client.schema(MESSAGING_SCHEMA);
1581
1595
  const rpcDb = () => client.schema(MESSAGING_RPC_SCHEMA);
1582
1596
  async function withSessionRetry(operation, run) {