@ai-matrx/chat 0.7.9 → 0.7.10

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,14 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.7.10
4
+ ### a refused answer is a failed run (lane M3)
5
+
6
+ - **`useHeadlessAgentJson.run`**: when `coerce` refuses the answer the model gave, the run is recorded as failed
7
+ (`chat.user_request` status `failed`, `error` = the reason, `metadata.output_refused`) through the guarded
8
+ `mergeJsonColumn` write (`recordRunOutputRefused`); a write that does not land is a `console.error`. Before, the run
9
+ stayed `completed` with `error` null forever. The error is re-thrown unchanged for the surface to say plainly.
10
+ - **Consumer action:** none.
11
+
3
12
  ## 0.7.9
4
13
 
5
14
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -15,6 +24,11 @@ No source changes intended and no consumer action required.
15
24
  ## 0.7.7
16
25
  ### Unreleased
17
26
 
27
+ - **No first-load layout shift from assistant messages** (spend register item 32): `AgentAssistantMessage` no longer swaps a fixed-height
28
+ skeleton for the markdown after 140 ms (measured CLS 0.48 on the run page). Persisted answers render in place on first paint.
29
+ `deferColdMarkdown` is removed from `AgentAssistantMessage`, `AssistantTurnGroup` and `AgentConversationDisplay` (the column keeps its
30
+ own history staging). **Consumer action:** none.
31
+
18
32
  - **`continueAgentJson`** (`agents/redux/execution-system/thunks/continue-agent-json`): the NEXT turn of a
19
33
  conversation whose first turn ran headlessly, waited on like the first (`adoptHeadlessAgentJson`). A turn is
20
34
  the person's words (`userInput`) or a turn the HOST writes (`hostTurn` — sent as `host_turn`, never
@@ -8,9 +8,8 @@ interface AgentConversationDisplayProps {
8
8
  */
9
9
  surfaceKey?: string;
10
10
  compact?: boolean;
11
- deferColdMarkdown?: boolean;
12
11
  fallbackVisibleGroupLimit?: number | null;
13
12
  bottomPinned?: boolean;
14
13
  }
15
- export declare function AgentConversationDisplay({ conversationId, surfaceKey, compact, deferColdMarkdown, fallbackVisibleGroupLimit, bottomPinned, }: AgentConversationDisplayProps): import("react").JSX.Element;
14
+ export declare function AgentConversationDisplay({ conversationId, surfaceKey, compact, fallbackVisibleGroupLimit, bottomPinned, }: AgentConversationDisplayProps): import("react").JSX.Element;
16
15
  export {};
@@ -53,7 +53,6 @@ function AgentConversationDisplay({
53
53
  conversationId,
54
54
  surfaceKey,
55
55
  compact = false,
56
- deferColdMarkdown = false,
57
56
  fallbackVisibleGroupLimit = null,
58
57
  bottomPinned = false
59
58
  }) {
@@ -262,8 +261,7 @@ function AgentConversationDisplay({
262
261
  isStreamActive: false,
263
262
  surfaceKey,
264
263
  compact: true,
265
- hideActionBar: true,
266
- deferColdMarkdown
264
+ hideActionBar: true
267
265
  },
268
266
  member.messageId
269
267
  )
@@ -307,8 +305,7 @@ function AgentConversationDisplay({
307
305
  isStreamActive: group.isStreamActive,
308
306
  surfaceKey,
309
307
  compact,
310
- canRetry: group.canRetry,
311
- deferColdMarkdown
308
+ canRetry: group.canRetry
312
309
  }
313
310
  )
314
311
  },
@@ -327,8 +324,7 @@ function AgentConversationDisplay({
327
324
  conversationId,
328
325
  surfaceKey,
329
326
  compact,
330
- members: group.members,
331
- deferColdMarkdown
327
+ members: group.members
332
328
  }
333
329
  )
334
330
  },
@@ -33,8 +33,6 @@ export interface AgentAssistantMessageProps {
33
33
  * button. Retry is non-destructive (the failed turn stays in history).
34
34
  */
35
35
  canRetry?: boolean;
36
- /** Chat cold-load only: show a short text skeleton before DB markdown mounts. */
37
- deferColdMarkdown?: boolean;
38
36
  /**
39
37
  * False for the intermediate iterations of a multi-step agentic turn. Only
40
38
  * the LAST member of a turn carries the answer, so only it may say "this run
@@ -43,4 +41,4 @@ export interface AgentAssistantMessageProps {
43
41
  */
44
42
  isTurnAnswer?: boolean;
45
43
  }
46
- export declare function AgentAssistantMessage({ conversationId, requestId, messageId, isStreamActive, recordMessageIds, streamSlotStart, streamSlotEnd, surfaceKey, hideActionBar, canRetry, deferColdMarkdown, isTurnAnswer, }: AgentAssistantMessageProps): import("react").JSX.Element;
44
+ export declare function AgentAssistantMessage({ conversationId, requestId, messageId, isStreamActive, recordMessageIds, streamSlotStart, streamSlotEnd, surfaceKey, hideActionBar, canRetry, isTurnAnswer, }: AgentAssistantMessageProps): import("react").JSX.Element;
@@ -1,7 +1,6 @@
1
1
  "use client";
2
2
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import {
4
- startTransition,
5
4
  useCallback,
6
5
  useMemo,
7
6
  useState,
@@ -117,7 +116,6 @@ function AgentAssistantMessage({
117
116
  surfaceKey,
118
117
  hideActionBar = false,
119
118
  canRetry = false,
120
- deferColdMarkdown = false,
121
119
  isTurnAnswer = true
122
120
  }) {
123
121
  useDebugContext("AgentAssistantMessage");
@@ -137,17 +135,7 @@ function AgentAssistantMessage({
137
135
  }, [surfaceKey, conversationId, messageId, requestId, isStreamActive]);
138
136
  const dispatch = useAppDispatch();
139
137
  const [retrying, setRetrying] = useState(false);
140
- const shouldDeferColdMarkdown = deferColdMarkdown && !requestId && !!messageId && !isStreamActive;
141
- const [coldMarkdownSettledMessageId, setColdMarkdownSettledMessageId] = useState(null);
142
- const coldMarkdownReady = !shouldDeferColdMarkdown || coldMarkdownSettledMessageId === messageId;
143
138
  const [providerRetryBusyAction, setProviderRetryBusyAction] = useState(null);
144
- useEffect(() => {
145
- if (!shouldDeferColdMarkdown || !messageId) return void 0;
146
- const timer = window.setTimeout(() => {
147
- startTransition(() => setColdMarkdownSettledMessageId(messageId));
148
- }, 140);
149
- return () => window.clearTimeout(timer);
150
- }, [shouldDeferColdMarkdown, messageId]);
151
139
  const { captureRef, isCapturing, captureAsPDF } = useDomCapturePrint();
152
140
  const handleFullPrint = useCallback(() => {
153
141
  captureAsPDF({
@@ -308,7 +296,6 @@ function AgentAssistantMessage({
308
296
  rowParts: extractContentBlocks(record)
309
297
  }),
310
298
  failed,
311
- coldMarkdownReady,
312
299
  messageId,
313
300
  renderedText,
314
301
  attachmentCount: attachmentParts.length,
@@ -394,7 +381,7 @@ function AgentAssistantMessage({
394
381
  "data-message-id": messageId ?? void 0,
395
382
  className: "group/assistant-msg rounded transition-shadow",
396
383
  children: [
397
- !coldMarkdownReady ? /* @__PURE__ */ jsx(AssistantMarkdownSkeleton, {}) : bufferStream && isStreamActive && !failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
384
+ bufferStream && isStreamActive && !failed ? /* @__PURE__ */ jsxs(Fragment, { children: [
398
385
  showProviderRetry && providerRetry && /* @__PURE__ */ jsx(
399
386
  ProviderRetryCard,
400
387
  {
@@ -538,15 +525,6 @@ function readErrorDoor(details) {
538
525
  const { label, href } = door;
539
526
  return typeof label === "string" && typeof href === "string" && href.startsWith("/") ? { label, href } : null;
540
527
  }
541
- function AssistantMarkdownSkeleton() {
542
- return /* @__PURE__ */ jsxs("div", { className: "space-y-2 py-1", "aria-hidden": true, children: [
543
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[96%] animate-pulse rounded bg-muted/55" }),
544
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[88%] animate-pulse rounded bg-muted/50" }),
545
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[72%] animate-pulse rounded bg-muted/45" }),
546
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[94%] animate-pulse rounded bg-muted/40" }),
547
- /* @__PURE__ */ jsx("div", { className: "h-3.5 w-[42%] animate-pulse rounded bg-muted/35" })
548
- ] });
549
- }
550
528
  export {
551
529
  AgentAssistantMessage
552
530
  };
@@ -16,7 +16,6 @@ interface AssistantTurnGroupProps {
16
16
  compact?: boolean;
17
17
  /** Every assistant member of this logical turn, in transcript order. */
18
18
  members: AssistantTurnGroupMember[];
19
- deferColdMarkdown?: boolean;
20
19
  }
21
- export declare function AssistantTurnGroup({ conversationId, surfaceKey, compact, members: rawMembers, deferColdMarkdown, }: AssistantTurnGroupProps): import("react").JSX.Element;
20
+ export declare function AssistantTurnGroup({ conversationId, surfaceKey, compact, members: rawMembers, }: AssistantTurnGroupProps): import("react").JSX.Element;
22
21
  export {};
@@ -26,8 +26,7 @@ function AssistantTurnGroup({
26
26
  conversationId,
27
27
  surfaceKey,
28
28
  compact = false,
29
- members: rawMembers,
30
- deferColdMarkdown = false
29
+ members: rawMembers
31
30
  }) {
32
31
  const { captureRef, isCapturing, captureAsPDF } = useDomCapturePrint();
33
32
  const persistedView = useAppSelector(
@@ -96,8 +95,7 @@ function AssistantTurnGroup({
96
95
  surfaceKey,
97
96
  compact,
98
97
  hideActionBar: true,
99
- isTurnAnswer: idx === members.length - 1 && m.streamSlotEnd === void 0,
100
- deferColdMarkdown: deferColdMarkdown && idx < members.length - 1
98
+ isTurnAnswer: idx === members.length - 1 && m.streamSlotEnd === void 0
101
99
  }
102
100
  ) }, m.key))
103
101
  }
@@ -113,8 +113,6 @@ export interface AnswerlessTurnInput {
113
113
  isStreamActive: boolean;
114
114
  /** True when the turn failed — the error path owns that case. */
115
115
  failed: boolean;
116
- /** False while a cold-load skeleton stands in for the persisted markdown. */
117
- coldMarkdownReady: boolean;
118
116
  /** `cx_message.id`; null/undefined until the row settles. */
119
117
  messageId: string | null | undefined;
120
118
  /** The text this turn renders (citation markers included). */
@@ -31,7 +31,6 @@ function isAnswerlessTurn(input) {
31
31
  if (input.isStreamActive) return false;
32
32
  if (input.awaitingPerson) return false;
33
33
  if (input.failed) return false;
34
- if (!input.coldMarkdownReady) return false;
35
34
  if (!input.messageId) return false;
36
35
  if (input.attachmentCount > 0) return false;
37
36
  if (input.mediaBlockCount > 0) return false;
@@ -299,7 +299,6 @@ function AgentConversationColumn({
299
299
  {
300
300
  conversationId: displayId,
301
301
  surfaceKey,
302
- deferColdMarkdown,
303
302
  bottomPinned: shouldPinColdScroll,
304
303
  fallbackVisibleGroupLimit: deferColdMarkdown ? chatVisibleGroupLimit ?? CHAT_INITIAL_VISIBLE_GROUPS : null
305
304
  }
@@ -3,6 +3,7 @@ import { useCallback, useState } from "react";
3
3
  import { useAppDispatch, useAppSelector, useAppStore } from "../../store/hooks.js";
4
4
  import { selectConversationRequestIds } from "../redux/execution-system/active-requests/active-requests.selectors.js";
5
5
  import { useRetainRequestForViewer } from "../redux/execution-system/active-requests/useRetainRequestForViewer.js";
6
+ import { recordRunOutputRefused } from "../redux/execution-system/thunks/record-run-output-refused.js";
6
7
  import {
7
8
  runHeadlessAgentJson
8
9
  } from "../redux/execution-system/thunks/run-headless-agent-json.js";
@@ -49,7 +50,14 @@ function useHeadlessAgentJson() {
49
50
  ...result.conversationId ? { conversationId: result.conversationId } : {}
50
51
  });
51
52
  }
52
- return opts.coerce ? opts.coerce(result.data, result) : result.data;
53
+ if (!opts.coerce) return result.data;
54
+ try {
55
+ return opts.coerce(result.data, result);
56
+ } catch (refused) {
57
+ const why = refused instanceof Error ? refused.message : String(refused);
58
+ if (result.requestId) void recordRunOutputRefused(result.requestId, why);
59
+ throw refused;
60
+ }
53
61
  } catch (e) {
54
62
  const message = e instanceof Error ? e.message : "Failed to run the agent";
55
63
  setError(message);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * recordRunOutputRefused — a run whose answer the BROWSER refused is a failed run.
3
+ *
4
+ * The server marks a run `completed` the moment the model stops; whether the answer was usable is only
5
+ * known where it is read (the feature's `coerce`, its own check). Before this, such a run stayed
6
+ * `completed` with `error` null forever (Make's describe box, 2026-10-09: "personal" is not one of
7
+ * organization, individual). The refusal is written onto the run's own `chat.user_request` row —
8
+ * status `failed`, `error` = the reason, and `metadata.output_refused` — through the guarded
9
+ * read-modify-write (`mergeJsonColumn`, a version guard, never a blind update).
10
+ *
11
+ * Never silent: a write that does not land is a `console.error` naming the run.
12
+ */
13
+ export declare const OUTPUT_REFUSED_METADATA_KEY = "output_refused";
14
+ export declare function recordRunOutputRefused(requestId: string, reason: string): Promise<boolean>;
@@ -0,0 +1,24 @@
1
+ import { mergeJsonColumn } from "@ai-matrx/data/db";
2
+ import { supabase } from "../../../../host/db.js";
3
+ import { hasBrowserSession } from "../../../../host/identity.js";
4
+ const OUTPUT_REFUSED_METADATA_KEY = "output_refused";
5
+ async function recordRunOutputRefused(requestId, reason) {
6
+ if (!requestId || !await hasBrowserSession()) return false;
7
+ const result = await mergeJsonColumn({
8
+ fetchCurrent: () => supabase.schema("chat").from("user_request").select("id, version, metadata").eq("id", requestId).maybeSingle(),
9
+ readColumn: (row) => row.metadata,
10
+ merge: (current) => ({ ...current, [OUTPUT_REFUSED_METADATA_KEY]: { reason, at: (/* @__PURE__ */ new Date()).toISOString() } }),
11
+ applyUpdate: ({ value, expectedVersion, nextVersion }) => supabase.schema("chat").from("user_request").update({ metadata: value, version: nextVersion, status: "failed", error: reason.slice(0, 500) }).eq("id", requestId).eq("version", expectedVersion).select("id, version, metadata").maybeSingle()
12
+ });
13
+ if (result.status !== "saved") {
14
+ console.error(
15
+ `[run-output-refused] Could not record the refusal on run ${requestId}: ` + (result.status === "error" ? String(result.error) : `write ${result.status}`) + `. The run still reads as completed. Reason: ${reason}`
16
+ );
17
+ return false;
18
+ }
19
+ return true;
20
+ }
21
+ export {
22
+ OUTPUT_REFUSED_METADATA_KEY,
23
+ recordRunOutputRefused
24
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.7.9",
3
+ "version": "0.7.10",
4
4
  "description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as explicit per-module subpaths @ai-matrx/chat/<domain>/<module> (no wildcards: scripts/public-modules.json) over a host contract (/host, /host/react) whose only required value is a db. One chat, one renderer: the old kernel entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) are retired.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -4681,25 +4681,25 @@
4681
4681
  "server-only": "^0.0.1",
4682
4682
  "uuid": "^14.0.2",
4683
4683
  "zod": "^4.6.5",
4684
- "@ai-matrx/agents": "latest",
4685
4684
  "@ai-matrx/alchemy": "latest",
4686
4685
  "@ai-matrx/associations": "latest",
4687
4686
  "@ai-matrx/browser-audio": "latest",
4688
4687
  "@ai-matrx/canvas": "latest",
4689
4688
  "@ai-matrx/content-ir": "latest",
4690
- "@ai-matrx/data": "latest",
4691
- "@ai-matrx/design-system": "latest",
4692
- "@ai-matrx/kit": "latest",
4693
- "@ai-matrx/media": "latest",
4689
+ "@ai-matrx/content-ir-react": "latest",
4690
+ "@ai-matrx/agents": "latest",
4694
4691
  "@ai-matrx/icons": "latest",
4695
- "@ai-matrx/print": "latest",
4696
4692
  "@ai-matrx/diff": "latest",
4693
+ "@ai-matrx/media": "latest",
4694
+ "@ai-matrx/kit": "latest",
4695
+ "@ai-matrx/print": "latest",
4697
4696
  "@ai-matrx/realtime": "latest",
4698
- "@ai-matrx/records": "latest",
4699
4697
  "@ai-matrx/records-ui": "latest",
4698
+ "@ai-matrx/design-system": "latest",
4700
4699
  "@ai-matrx/rich-content": "latest",
4701
- "@ai-matrx/rich-editor": "latest",
4702
- "@ai-matrx/content-ir-react": "latest"
4700
+ "@ai-matrx/records": "latest",
4701
+ "@ai-matrx/data": "latest",
4702
+ "@ai-matrx/rich-editor": "latest"
4703
4703
  },
4704
4704
  "devDependencies": {
4705
4705
  "@arethetypeswrong/cli": "^0.18.5",
@@ -4818,5 +4818,5 @@
4818
4818
  "check:side-effects": "node ../scripts/check-side-effects-listed.mjs . && node ../scripts/check-side-effects-listed.mjs --self-test",
4819
4819
  "check:public-surface": "node ./scripts/sync-public-surface.mjs --check && node ./scripts/sync-public-surface.mjs --self-test"
4820
4820
  },
4821
- "gitHead": "6f80fabd57ab4f61aa950c7e2d9b1f3309797abd"
4821
+ "gitHead": "c1236d2d5f8a42087300bafce53317efb25700af"
4822
4822
  }