@ai-matrx/chat 0.4.7 → 0.4.8

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,11 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.4.8
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/chat/v0.4.7..npm/chat/v0.4.8 -- apps/shared/chat`).
7
+ No source changes intended and no consumer action required.
8
+
3
9
  ## 0.4.7
4
10
 
5
11
  - The host-source column test gets an explicit 120s budget: the column now renders the real text engine
@@ -5,7 +5,11 @@ import {
5
5
  classifyUsageRefusal
6
6
  } from "../../../../host/usage-gate.js";
7
7
  import { parseNdjsonStream } from "../../../../host/server/stream-parser.js";
8
- import { isStreamTransportLost, StreamTransportError } from "@ai-matrx/agents/matrx";
8
+ import {
9
+ isStreamTransportLost,
10
+ readStreamCursor,
11
+ StreamTransportError
12
+ } from "@ai-matrx/agents/matrx";
9
13
  import { monitorStream } from "@ai-matrx/data/net";
10
14
  import { mintClientTempId } from "@ai-matrx/kit/ids";
11
15
  import { withPerformedScript } from "../../../speech-script/types.js";
@@ -139,15 +143,7 @@ import { getCapabilitiesForConversation } from "../../../runtime/get-model-capab
139
143
  import { toast } from "../../../../host/notify.js";
140
144
  import { isDirectiveApplyEvent } from "@ai-matrx/agents/envelope";
141
145
  import { proposeDirective } from "../../proposed-directives/proposedDirectivesSlice.js";
142
- function readTransportCursor(event) {
143
- if (typeof event !== "object" || event === null || !("stream_seq" in event) || typeof event.stream_seq !== "number" || !Number.isSafeInteger(event.stream_seq) || event.stream_seq <= 0) {
144
- return null;
145
- }
146
- return {
147
- streamId: "stream_id" in event && typeof event.stream_id === "string" && event.stream_id.length > 0 ? event.stream_id : null,
148
- streamSeq: event.stream_seq
149
- };
150
- }
146
+ const readTransportCursor = readStreamCursor;
151
147
  function renderBlockToContentType(type) {
152
148
  if (type === "image" || type === "image_output") return "image";
153
149
  if (type === "audio_output") return "audio";
@@ -349,6 +345,7 @@ async function processStream({
349
345
  let pendingTransportCursor = null;
350
346
  const batchedDispatch = (action) => dispatch(autoBatched(action));
351
347
  let reasoningLeakReported = false;
348
+ let serverStructuredData = null;
352
349
  const runJsonExtraction = (isFinal) => {
353
350
  if (!jsonTracker) return;
354
351
  const req = getState().activeRequests.byRequestId[requestId];
@@ -367,10 +364,22 @@ async function processStream({
367
364
  const jsonState = isFinal ? jsonTracker.finalize() : jsonTracker.getState();
368
365
  if (isFinal || jsonState.revision !== lastJsonRevision) {
369
366
  lastJsonRevision = jsonState.revision;
367
+ const clientResults = jsonState.results.map(toSnapshot);
368
+ const results = serverStructuredData ? [
369
+ toSnapshot({
370
+ value: serverStructuredData,
371
+ type: Array.isArray(serverStructuredData) ? "array" : "object",
372
+ source: "whole-string",
373
+ isComplete: true,
374
+ repairApplied: false,
375
+ warnings: ["Server structured_output payload"]
376
+ }),
377
+ ...clientResults
378
+ ] : clientResults;
370
379
  batchedDispatch(
371
380
  updateExtractedJson({
372
381
  requestId,
373
- results: jsonState.results.map(toSnapshot),
382
+ results,
374
383
  revision: jsonState.revision,
375
384
  isComplete: isFinal
376
385
  })
@@ -1920,6 +1929,10 @@ async function processStream({
1920
1929
  );
1921
1930
  } else if (isStructuredOutputEvent(event)) {
1922
1931
  otherEvents++;
1932
+ if (event.data.success && event.data.data != null && typeof event.data.data === "object") {
1933
+ serverStructuredData = event.data.data;
1934
+ runJsonExtraction(false);
1935
+ }
1923
1936
  dispatch(
1924
1937
  appendTimeline({
1925
1938
  requestId,
@@ -276,6 +276,8 @@ export interface HeadlessAgentJsonResult {
276
276
  * result passes through untouched.
277
277
  */
278
278
  export declare function failWarnedOutputMissingKeys(getState: () => ChatRootState, opts: Pick<HeadlessAgentJsonOptions, "mandateKey" | "surfaceKey" | "expect">, result: HeadlessAgentJsonResult): Promise<HeadlessAgentJsonResult>;
279
+ /** The first JSON object in `text` that holds EVERY required key, or null. */
280
+ export declare function recoverByRequiredKeys(text: string | undefined, required: readonly string[]): Record<string, unknown> | null;
279
281
  /**
280
282
  * The plain sentence of a warned-holder failure (`failWarnedOutputMissingKeys`),
281
283
  * or `null` for any other result. A lane that swallows failures to `null`
@@ -1,4 +1,4 @@
1
- import { extractFirstJson } from "@ai-matrx/kit/json-extract";
1
+ import { extractAllJson, extractFirstJson } from "@ai-matrx/kit/json-extract";
2
2
  import { extractErrorMessage } from "@ai-matrx/data/net";
3
3
  import { destroyInstanceIfAllowed } from "../conversations/conversations.thunks.js";
4
4
  import { captureError } from "../../../../host/diagnostics.js";
@@ -129,13 +129,18 @@ async function failWarnedOutputMissingKeys(getState, opts, result) {
129
129
  if (required.length === 0) return result;
130
130
  let missing;
131
131
  if (result.success && isRecord) {
132
- const record = data;
133
- const answers = typeof record.answers === "object" && record.answers !== null && !Array.isArray(record.answers) ? record.answers : null;
134
- missing = required.filter((key) => !(key in record) && !(answers && key in answers));
132
+ missing = missingRequiredKeys(data, required);
135
133
  } else {
136
134
  missing = [...required];
137
135
  }
138
136
  if (missing.length === 0) return result;
137
+ const recovered = recoverByRequiredKeys(result.fullResponse, required);
138
+ if (recovered) {
139
+ console.warn(
140
+ `[runHeadlessAgentJson] ${opts.surfaceKey}: the first JSON read lacked ${missing.join(", ")}; a candidate in the answer holding every required key was used instead.`
141
+ );
142
+ return { ...result, success: true, data: recovered, noResult: false, error: void 0, errorDetail: void 0 };
143
+ }
139
144
  const warning = resolved.outputWarnings[0];
140
145
  const holderId = warning.holderId ?? resolved.agentId;
141
146
  let holderName;
@@ -174,6 +179,21 @@ async function failWarnedOutputMissingKeys(getState, opts, result) {
174
179
  errorDetail: `mandate_output_unusable: missing ${keys}`
175
180
  };
176
181
  }
182
+ function missingRequiredKeys(record, required) {
183
+ const answers = typeof record.answers === "object" && record.answers !== null && !Array.isArray(record.answers) ? record.answers : null;
184
+ return required.filter((key) => !(key in record) && !(answers && key in answers));
185
+ }
186
+ function recoverByRequiredKeys(text, required) {
187
+ if (!text || required.length === 0) return null;
188
+ for (const candidate of extractAllJson(text, { allowFuzzy: true })) {
189
+ const value = candidate.value;
190
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
191
+ if (missingRequiredKeys(value, required).length === 0) {
192
+ return value;
193
+ }
194
+ }
195
+ return null;
196
+ }
177
197
  function mandateOutputUnusableSentence(result) {
178
198
  if (result.success) return null;
179
199
  if (!result.errorDetail?.startsWith("mandate_output_unusable")) return null;
@@ -565,5 +585,6 @@ export {
565
585
  failWarnedOutputMissingKeys,
566
586
  livePosture,
567
587
  mandateOutputUnusableSentence,
588
+ recoverByRequiredKeys,
568
589
  runHeadlessAgentJson
569
590
  };
@@ -1,7 +1,11 @@
1
1
  import { retryReconnect } from "./retry-reconnect.js";
2
2
  import { discardRetainedTransportConsumer } from "../redux/execution-system/thunks/process-stream.js";
3
3
  import { createAsyncThunk } from "@reduxjs/toolkit";
4
- import { runtimeOperationRejoinPath, settleRunPickup } from "@ai-matrx/agents/matrx";
4
+ import {
5
+ pickServerJobOperation,
6
+ runtimeOperationRejoinPath,
7
+ settleRunPickup
8
+ } from "@ai-matrx/agents/matrx";
5
9
  import { toast } from "../../host/notify.js";
6
10
  import { captureError } from "../../host/diagnostics.js";
7
11
  import { setRequestStatus } from "../redux/execution-system/active-requests/active-requests.slice.js";
@@ -77,8 +81,9 @@ const reconnectServerOperation = createAsyncThunk(
77
81
  clearFollower();
78
82
  return noFollow;
79
83
  }
80
- op = byLink?.operations.find((o) => !o.is_terminal) ?? null;
81
- settledWhileDisconnected = !op && Boolean(byLink?.operations.length);
84
+ const picked = pickServerJobOperation(byLink?.operations);
85
+ op = picked && !picked.is_terminal ? picked : null;
86
+ settledWhileDisconnected = Boolean(picked?.is_terminal);
82
87
  } catch (err) {
83
88
  const superseded = ctrl.signal.aborted || !stillOwner();
84
89
  clearFollower();
@@ -263,9 +268,8 @@ const reconnectServerOperation = createAsyncThunk(
263
268
  clearFollower();
264
269
  return noFollow;
265
270
  }
266
- const stillLive = refreshed?.operations.find(
267
- (operation) => !operation.is_terminal
268
- );
271
+ const latest = pickServerJobOperation(refreshed?.operations);
272
+ const stillLive = latest && !latest.is_terminal ? latest : null;
269
273
  if (stillLive) {
270
274
  if (followers.get(conversationId) === ctrl) {
271
275
  followers.delete(conversationId);
@@ -23,14 +23,22 @@ export declare const PREFERENCE_FIELDS: Readonly<Record<string, keyof ChatPrefer
23
23
  export declare function createUnhostedKnobs(): ChatKnobsPort;
24
24
  export declare const CHAT_PREFS_PREFIX = "ai-matrx-chat:";
25
25
  export declare function createWebPrefs(): ChatPrefsPort;
26
+ /** A host-kept prefs port, plus the late arrival of the host's saved values. */
27
+ export interface HostKeptPrefs extends ChatPrefsPort {
28
+ /**
29
+ * The host's saved values arrived after the port was made (read over a socket or IPC). A key
30
+ * already written this session keeps the newer value; every other key is set and announced.
31
+ */
32
+ load(values: Readonly<Record<string, string>>): void;
33
+ }
26
34
  /**
27
35
  * A prefs port the HOST keeps (HOST-OWNED-SOURCE-PLAN §2.2 "Drafts"): a page whose origin
28
36
  * changes between launches (a local host on a new port, a desktop window) cannot keep drafts in
29
- * web storage. The host reads its saved values once (`initial`) and receives every change
30
- * (`persist`, `null` = removed) to write where it keeps them — a file, an IPC store. Reads are
31
- * synchronous from memory, so the first render already sees the saved draft.
37
+ * web storage. The host hands its saved values in (`initial`, or `load` when they arrive later)
38
+ * and receives every change (`persist`, `null` = removed) to write where it keeps them — a file,
39
+ * an IPC store. Reads are synchronous from memory.
32
40
  */
33
41
  export declare function createHostKeptPrefs(options: {
34
- initial: Readonly<Record<string, string>>;
42
+ initial?: Readonly<Record<string, string>>;
35
43
  persist: (key: string, value: string | null) => void;
36
- }): ChatPrefsPort;
44
+ }): HostKeptPrefs;
@@ -169,12 +169,14 @@ function createWebPrefs() {
169
169
  return port;
170
170
  }
171
171
  function createHostKeptPrefs(options) {
172
- const memory = new Map(Object.entries(options.initial));
172
+ const memory = new Map(Object.entries(options.initial ?? {}));
173
+ const written = /* @__PURE__ */ new Set();
173
174
  const listeners = /* @__PURE__ */ new Set();
174
175
  const emit = (key) => {
175
176
  for (const listener of listeners) listener(key);
176
177
  };
177
178
  const write = (key, value) => {
179
+ written.add(key);
178
180
  try {
179
181
  options.persist(key, value);
180
182
  } catch (error) {
@@ -206,7 +208,14 @@ function createHostKeptPrefs(options) {
206
208
  },
207
209
  knob: (key, fallback) => parseKnob(memory.get(key) ?? null, fallback),
208
210
  knobs: createUnhostedKnobs(),
209
- snapshot: () => Object.fromEntries(memory)
211
+ snapshot: () => Object.fromEntries(memory),
212
+ load(values) {
213
+ for (const [key, value] of Object.entries(values)) {
214
+ if (written.has(key) || memory.get(key) === value) continue;
215
+ memory.set(key, value);
216
+ emit(key);
217
+ }
218
+ }
210
219
  };
211
220
  return port;
212
221
  }
@@ -10,7 +10,7 @@ export { createDbIdentity, SIGNED_OUT_IDENTITY } from "./defaults/identity.js";
10
10
  export { createNoPickerOrg, createStoredOrg, CHAT_ACTIVE_ORG_STORAGE_KEY } from "./defaults/org.js";
11
11
  export { createDefaultServer, DEFAULT_CHAT_SERVER_URL, } from "./defaults/server.js";
12
12
  export { createDomNotifier } from "./defaults/notify.js";
13
- export { createWebPrefs, createHostKeptPrefs, CHAT_PREFS_PREFIX } from "./defaults/prefs.js";
13
+ export { createWebPrefs, createHostKeptPrefs, CHAT_PREFS_PREFIX, type HostKeptPrefs } from "./defaults/prefs.js";
14
14
  export { createWindowNavigation } from "./defaults/navigation.js";
15
15
  export { createUnhostedWindows } from "./defaults/windows.js";
16
16
  export { CHAT_WINDOWS, CHAT_WINDOW_IDS, DEFAULT_WINDOW_INSTANCE_ID, isChatWindowId, type ChatWindowId, } from "./windows.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
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 per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -226,17 +226,14 @@
226
226
  "server-only": "^0.0.1",
227
227
  "uuid": "^14.0.2",
228
228
  "zod": "^4.6.5",
229
+ "@ai-matrx/agents": "latest",
229
230
  "@ai-matrx/alchemy": "latest",
230
- "@ai-matrx/associations": "latest",
231
231
  "@ai-matrx/browser-audio": "latest",
232
- "@ai-matrx/canvas": "latest",
233
- "@ai-matrx/agents": "latest",
234
232
  "@ai-matrx/content-ir": "latest",
235
- "@ai-matrx/content-ir-react": "latest",
236
- "@ai-matrx/data": "latest",
233
+ "@ai-matrx/associations": "latest",
234
+ "@ai-matrx/canvas": "latest",
237
235
  "@ai-matrx/design-system": "latest",
238
236
  "@ai-matrx/diff": "latest",
239
- "@ai-matrx/icons": "latest",
240
237
  "@ai-matrx/kit": "latest",
241
238
  "@ai-matrx/media": "latest",
242
239
  "@ai-matrx/print": "latest",
@@ -245,6 +242,9 @@
245
242
  "@ai-matrx/records-ui": "latest",
246
243
  "@ai-matrx/rich-content": "latest",
247
244
  "@ai-matrx/rich-editor": "latest",
245
+ "@ai-matrx/data": "latest",
246
+ "@ai-matrx/content-ir-react": "latest",
247
+ "@ai-matrx/icons": "latest",
248
248
  "@ai-matrx/tap-target": "latest"
249
249
  },
250
250
  "devDependencies": {