@ai-matrx/messaging 0.9.0 → 0.10.1

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,59 @@
1
1
  # Changelog — `@ai-matrx/messaging`
2
2
 
3
+ ## 0.10.1 — 2026-09-07
4
+
5
+ **Every AI call would have been refused by the real server.** `<MessagingProvider>` never forwarded
6
+ `sourceApp` / `sourceFeature` to the core, so every run went out stamped with this package's
7
+ placeholder `ai-matrx` / `messaging.<capability>` — and the AI Matrx server keeps a REGISTER of
8
+ legal producers and answers an unregistered one with a 422. Found by putting a real request on the
9
+ wire, not by reading the code; the same shape as the 0.7.0 wrong-schema defect.
10
+
11
+ ### Added
12
+
13
+ - **`<MessagingProvider sourceApp sourceFeature>`**, forwarded to `createMessagingAi`. Only the
14
+ host knows its registered slugs, so the host passes them; the package's defaults are documented
15
+ as placeholders that will not survive a real server.
16
+
17
+ ## 0.10.0 — 2026-09-07
18
+
19
+ **The four AI-native intelligences rendered nothing in the first host that shipped them, and
20
+ would have shown a skeleton instead of an answer if they had.** Three defects, all found by
21
+ wiring the package to a real switching layer rather than by reading it.
22
+
23
+ ### Changed (breaking, one line at the call site)
24
+
25
+ - **`agents` now takes an IDENTITY per capability, not a bare id.**
26
+ `{ summarize: "id" }` becomes `{ summarize: { agentId: "id", configOverrides } }`. A host that
27
+ resolves a capability's agent from a switching layer resolves a PAIR — which agent, and the
28
+ settings that layer decided for it. Taking only the id silently dropped the settings half on
29
+ every call. `configOverrides` rides to the server as the turn's `config_overrides`, verbatim;
30
+ omitted, the field is not sent at all and the agent's own settings stand.
31
+
32
+ ### Added
33
+
34
+ - **AI runs STREAM.** Every capability takes `onText`, and `useMessagingAi` exposes
35
+ `partialText` + `activeCapability`. `ConversationView` renders the answer as it arrives; the
36
+ skeleton survives only for the gap before the first chunk, where it now names the job that is
37
+ running. A placeholder that sat until a run finished was a spinner standing in for an answer.
38
+ - **`<MessagingProvider maxTranscriptMessages>`** — the provider never forwarded the core's
39
+ transcript cap, so the 200 in this package was unreachable taste. A host with an
40
+ organization-configurable knob passes the resolved number.
41
+ - **`unreadCutoff(messages, unreadCount)`**, exported from the framework-free core.
42
+
43
+ ### Fixed
44
+
45
+ - **"Catch me up" was a second "Summarize".** It always passed `since: null`, so it sent the
46
+ whole loaded window and answered the question nobody asked. It now derives the exclusive
47
+ cutoff from the store's own unread count — the message just before the first unread one — and
48
+ passes `null` only when there is genuinely no honest cutoff (nothing unread, or a loaded
49
+ window that does not reach back that far). Never a guessed timestamp.
50
+
51
+ ### Consumer action (C28)
52
+
53
+ Change `agents={{ summarize: id }}` to `agents={{ summarize: { agentId: id } }}` and pass
54
+ `configOverrides` when your resolution layer produces them. If you render the AI output
55
+ yourself, render `partialText` while `isRunning`.
56
+
3
57
  ## 0.9.0 — 2026-09-07
4
58
 
5
59
  **`<MessagingProvider client={supabase}>` did not typecheck for a host with generated Supabase
package/README.md CHANGED
@@ -166,14 +166,39 @@ 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
175
+ sourceApp="matrx-frontend" // YOUR registered producer slugs
176
+ sourceFeature="messages"
171
177
  >
172
178
  ```
173
179
 
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.
180
+ Agent definitions live in the **database**, never in this package — the injected **identity** is
181
+ the only part a host supplies. A capability with no identity **does not render at all**; it is
182
+ never a dead button that fails when pressed.
183
+
184
+ **An identity is both halves.** If your app resolves a capability's agent from a switching layer
185
+ that also decides settings for the job, pass `configOverrides` with the id — it is sent verbatim
186
+ as the turn's `config_overrides`. Passing the id alone silently drops the settings half. Omit it
187
+ and nothing is sent, so the agent's own settings stand.
188
+
189
+ **A run the user watches streams.** `useMessagingAi` reports `partialText` after every chunk and
190
+ `ConversationView` renders it; the skeleton appears only in the gap before the first chunk, and
191
+ names the job it is waiting on. If you render the output yourself, render `partialText` while
192
+ `isRunning` — a placeholder that sits until the run finishes is a spinner standing in for an
193
+ answer.
194
+
195
+ 🚨 **`sourceApp` / `sourceFeature` are not analytics garnish.** The AI Matrx server keeps a register
196
+ of legal producers and answers an unregistered one with a **422** — so every AI call fails until you
197
+ pass slugs your server knows. This package's defaults are placeholders; only you know yours.
198
+
199
+ **"Catch me up" means the unread tail**, derived from the store's own unread count. Nothing
200
+ unread, or a loaded window that does not reach back to the read mark, means there is no honest
201
+ cutoff and the whole window applies — never a guessed timestamp.
177
202
 
178
203
  ### Making references open
179
204
 
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
  }