@ai-matrx/chat 0.4.12 → 0.4.15

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/dist/agents/components/shared/AgentModeController.d.ts +1 -1
  3. package/dist/agents/components/shared/AgentModeController.js +4 -0
  4. package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.js +1 -1
  5. package/dist/agents/redux/execution-system/active-requests/active-requests.slice.d.ts +2 -0
  6. package/dist/agents/redux/execution-system/active-requests/active-requests.slice.js +7 -0
  7. package/dist/agents/redux/execution-system/conversations/conversation-persistence.js +2 -4
  8. package/dist/agents/redux/execution-system/instance-context/instance-context.slice.js +1 -1
  9. package/dist/agents/redux/execution-system/thunks/dispatch-matrx-extend-tool.thunk.js +3 -1
  10. package/dist/agents/redux/execution-system/thunks/process-stream.js +26 -23
  11. package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +1 -1
  12. package/dist/agents/runtime-reconnect/api.d.ts +6 -25
  13. package/dist/agents/runtime-reconnect/api.js +7 -13
  14. package/dist/agents/runtime-reconnect/reconnect-server-operation.thunk.js +167 -159
  15. package/dist/agents/runtime-reconnect/types.d.ts +2 -0
  16. package/dist/agents/services/mcp-connections.service.d.ts +12 -0
  17. package/dist/agents/services/mcp-connections.service.js +4 -7
  18. package/dist/agents/types/request.types.d.ts +6 -0
  19. package/dist/agents/utils/json-extraction.js +2 -2
  20. package/dist/cx-chat/utils/settings-diff.js +1 -21
  21. package/dist/host/configure.js +15 -5
  22. package/dist/host/contract.d.ts +43 -2
  23. package/dist/host/db-types.d.ts +112 -0
  24. package/dist/host/db.js +6 -0
  25. package/dist/host/defaults/diagnostics.d.ts +5 -0
  26. package/dist/host/defaults/diagnostics.js +1 -2
  27. package/dist/host/defaults/identity.d.ts +26 -0
  28. package/dist/host/defaults/identity.js +133 -1
  29. package/dist/host/identity.d.ts +19 -2
  30. package/dist/host/identity.js +18 -10
  31. package/dist/host/session-retry.js +2 -2
  32. package/dist/host/ui-slots.js +27 -20
  33. package/dist/surfaces/services/bind-agent-to-surface.service.js +2 -4
  34. package/dist/tool-call-visualization/renderers/agent-call/CollabCallCard.js +1 -1
  35. package/dist/ui/mandates-storage/mandateStorage.d.ts +60 -0
  36. package/dist/ui/mandates-storage/shortcutStorage.d.ts +15 -0
  37. package/dist/utils/deep-equal.d.ts +5 -0
  38. package/dist/utils/deep-equal.js +24 -0
  39. package/package.json +7 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.4.15
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/chat/v0.4.14..npm/chat/v0.4.15 -- apps/shared/chat`).
7
+ No source changes intended and no consumer action required.
8
+
9
+ ## 0.4.14 — token-only hosts, on-device tools, and a bundle budget (W0(b), W0(c), W0(e))
10
+
11
+ - **Token-only database clients** (supabase-js `accessToken` option — matrx-local's custodian
12
+ client, where `db.auth` throws): new host field `accessToken: () => Promise<string | null>`; the
13
+ default identity reads the person from that token (admin level through `db`). A token-only `db`
14
+ with no function is signed out and settled, announced once with the remedy. The resolver no
15
+ longer touches `db.auth` on such a client.
16
+ - **One session door:** every package session read goes through the identity port
17
+ (`readSessionToken`, `hasBrowserSession`, `hostSessionSource` in `host/identity`); guard test
18
+ `host/__tests__/session-reads-go-through-the-identity-port.test.ts` fails on a direct auth-js call.
19
+ - **`deviceTools` host port** (`ChatDeviceToolsPort`): a host that IS the device (chat inside Matrx
20
+ Extend, a desktop app) executes delegated device tools; the package keeps the lifecycle, the
21
+ durable result submit and the resume. Absent: unchanged web route through the extension bridge.
22
+ - **Side-panel bundle budget:** `pnpm check:side-panel-bundle` builds the bare Vite canary and fails
23
+ when the first chat screen or the whole output (gzip) passes `side-panel-bundle-budget.json`
24
+ (measured on 0.4.9: 2435 kB / 8197 kB — a ratchet, lower it when it shrinks).
25
+
26
+ Consumer action: none for session (auth-js) hosts. A token-only host passes `accessToken` (the same
27
+ function its Supabase client got). A device host passes `deviceTools`.
28
+
29
+ ## 0.4.14 — reconnect rides the shared follow core
30
+
31
+ - Reconnect (`reconnectServerOperation`) now identifies, rejoins, follows and settles through
32
+ `followServerJob` (@ai-matrx/agents 0.57.0); `processStream` dedupes replay through the shared
33
+ `createStreamReplayCursor` (a high-water mark per `stream_id`, persisted on `ActiveRequest` as
34
+ `transportSeqByStream`). The chat-only follower (`followOperationStream`) is deleted. Fixes
35
+ replayed segment A rendering twice after a live A then B.
36
+
37
+ Consumer action: none.
38
+
39
+ ## 0.4.13 — markdown-source helpers from their one home
40
+
41
+ - `StreamingJsonTracker`, `extractAllJson`, `extractFirstJson`, `findAllFencedBlocks` are read from
42
+ `@ai-matrx/content-ir/json-extract`, and `stripThinkingStreaming` from `@ai-matrx/content-ir/source`
43
+ (kit 0.36.0 no longer carries them).
44
+
45
+ Consumer action: none.
46
+
3
47
  ## Unreleased
4
48
 
5
49
  - Chat's own message surfaces import `ui/markdown-stream/MarkdownStream` by its default export, so a host test that mocks that module (default only) replaces it everywhere, as it did when it was a slot.
@@ -1,5 +1,5 @@
1
1
  import { Eye } from "lucide-react";
2
- export type AgentPageMode = "view" | "edit" | "run" | "shortcuts" | "apps" | "surfaces" | "hindsight" | "versions" | "widgets";
2
+ export type AgentPageMode = "view" | "edit" | "run" | "shortcuts" | "apps" | "surfaces" | "hindsight" | "runs" | "versions" | "widgets";
3
3
  export type ModeOption = AgentPageMode | "new";
4
4
  export declare const MODES: {
5
5
  id: ModeOption;
@@ -3,6 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useState, useTransition } from "react";
4
4
  import { useRouter, usePathname, Link } from "../../../host/navigation.js";
5
5
  import {
6
+ Activity,
6
7
  AppWindow,
7
8
  Eye,
8
9
  History,
@@ -40,6 +41,7 @@ const MODES = [
40
41
  { id: "apps", label: "Apps", icon: AppWindow },
41
42
  { id: "surfaces", label: "Surfaces", icon: Layers },
42
43
  { id: "hindsight", label: "Review", icon: Telescope },
44
+ { id: "runs", label: "Runs", icon: Activity },
43
45
  { id: "versions", label: "Versions", icon: History },
44
46
  { id: "widgets", label: "Widgets", icon: LayoutGrid },
45
47
  { id: "new", label: "New", icon: Plus }
@@ -54,6 +56,7 @@ function getAgentModeHref(mode, agentId, basePath) {
54
56
  apps: `${basePath}/${agentId}/apps`,
55
57
  surfaces: `${basePath}/${agentId}/surfaces`,
56
58
  hindsight: `${basePath}/${agentId}/hindsight`,
59
+ runs: `${basePath}/${agentId}/runs`,
57
60
  versions: `${basePath}/${agentId}/latest`,
58
61
  widgets: `${basePath}/${agentId}/widgets`
59
62
  };
@@ -67,6 +70,7 @@ function deriveAgentMode(pathname, agentId, basePath) {
67
70
  if (pathname.startsWith(`${base}/apps`)) return "apps";
68
71
  if (pathname.startsWith(`${base}/surfaces`)) return "surfaces";
69
72
  if (pathname.startsWith(`${base}/hindsight`)) return "hindsight";
73
+ if (pathname.startsWith(`${base}/runs`)) return "runs";
70
74
  if (pathname.startsWith(`${base}/widgets`)) return "widgets";
71
75
  const versionPattern = new RegExp(
72
76
  `^${basePath.replace(/\//g, "\\/")}\\/[^/]+\\/v\\/\\d+$`
@@ -17,7 +17,7 @@ import {
17
17
  insertCitationMarkers
18
18
  } from "../messages/message-citations.js";
19
19
  import { soleFence } from "@ai-matrx/content-ir/source";
20
- import { stripThinkingStreaming } from "@ai-matrx/kit/text";
20
+ import { stripThinkingStreaming } from "@ai-matrx/content-ir/source";
21
21
  const EMPTY_REQUEST_IDS = [];
22
22
  const EMPTY_ACTIVE_REQUESTS = [];
23
23
  const selectRequest = (requestId) => (state) => state.activeRequests.byRequestId[requestId];
@@ -66,6 +66,8 @@ export declare const createRequest: import("@reduxjs/toolkit").ActionCreatorWith
66
66
  requestId: string;
67
67
  streamSeq: number;
68
68
  streamId?: string | null;
69
+ /** The cursor's per-segment high-water marks (replay dedup survives A→B). */
70
+ marks?: Record<string, number>;
69
71
  }, "activeRequests/recordTransportSeq">, setRequestStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
70
72
  requestId: string;
71
73
  status: RequestStatus;
@@ -269,6 +269,13 @@ const activeRequestsSlice = createSlice({
269
269
  recordTransportSeq(state, action) {
270
270
  const request = state.byRequestId[action.payload.requestId];
271
271
  if (!request) return;
272
+ if (action.payload.marks) {
273
+ const merged = { ...request.transportSeqByStream ?? {} };
274
+ for (const [id, seq] of Object.entries(action.payload.marks)) {
275
+ merged[id] = Math.max(merged[id] ?? 0, seq);
276
+ }
277
+ request.transportSeqByStream = merged;
278
+ }
272
279
  const streamId = action.payload.streamId ?? null;
273
280
  if (streamId && streamId !== request.transportStreamId) {
274
281
  request.transportStreamId = streamId;
@@ -1,14 +1,12 @@
1
1
  import { supabase } from "../../../../host/db.js";
2
+ import { hasBrowserSession } from "../../../../host/identity.js";
2
3
  async function waitForConversationPersisted(conversationId, {
3
4
  signal,
4
5
  timeoutMs = 18e4,
5
6
  intervalMs = 250,
6
7
  maxIntervalMs = 1500
7
8
  } = {}) {
8
- const {
9
- data: { session }
10
- } = await supabase.auth.getSession();
11
- if (!session) {
9
+ if (!await hasBrowserSession()) {
12
10
  console.info(
13
11
  `[conversation-persistence] Guest session \u2014 skipping persistence poll for ${conversationId} (URL stays on /new; server persists under the guest's anonymous user).`
14
12
  );
@@ -1,5 +1,5 @@
1
1
  import { createSlice } from "@reduxjs/toolkit";
2
- import isEqual from "lodash/isEqual";
2
+ import { deepEqual as isEqual } from "../../../../utils/deep-equal.js";
3
3
  import { destroyInstance } from "../conversations/conversations.slice.js";
4
4
  import { createInstanceFull } from "../create-instance-full.js";
5
5
  import { humanizeIdentifier } from "@ai-matrx/kit/text-case";
@@ -2,13 +2,15 @@ import { createAsyncThunk } from "@reduxjs/toolkit";
2
2
  import { submitToolResult } from "../../../api/submit-tool-results.js";
3
3
  import { upsertToolLifecycle } from "../active-requests/active-requests.slice.js";
4
4
  import { invokeMatrxExtendTool } from "../../../../host/server/extension-bridge.js";
5
+ import { getChatHost, isChatHostConfigured } from "../../../../host/configure.js";
5
6
  import { extractErrorMessage } from "@ai-matrx/data/net";
6
7
  const dispatchMatrxExtendTool = createAsyncThunk(
7
8
  "matrxExtendTools/dispatch",
8
9
  async ({ conversationId, requestId, callId, toolName, args }, { dispatch }) => {
9
10
  const startedAt = performance.now();
10
11
  try {
11
- const invocation = await invokeMatrxExtendTool(toolName, args);
12
+ const deviceTools = isChatHostConfigured() ? getChatHost().deviceTools : null;
13
+ const invocation = deviceTools ? await deviceTools.invoke(toolName, args, { conversationId, callId }) : await invokeMatrxExtendTool(toolName, args);
12
14
  if (!invocation.handled) {
13
15
  const message = invocation.reason === "matrx_extend_unavailable" ? `'${toolName}' runs inside the Matrx Extend browser extension, and no Matrx Extend is installed or reachable in this browser right now. Nothing ran and nothing was changed. Tell the user plainly, and ask them to install or enable Matrx Extend and reload this page \u2014 or take a route that does not need the extension.` : `Matrx Extend is installed here, but it does not own a tool named '${toolName}', so nothing ran and nothing was changed. Do not retry the same name: use a tool that is actually offered this turn, or tell the user this capability is not available on this browser.`;
14
16
  dispatch(
@@ -6,6 +6,7 @@ import {
6
6
  } from "../../../../host/usage-gate.js";
7
7
  import { parseNdjsonStream } from "../../../../host/server/stream-parser.js";
8
8
  import {
9
+ createStreamReplayCursor,
9
10
  isStreamTransportLost,
10
11
  readStreamCursor,
11
12
  StreamTransportError
@@ -114,7 +115,7 @@ import {
114
115
  } from "../instance-context/sync-canvas-item-context.thunk.js";
115
116
  import { docKindForContextKey } from "../../../utils/workingDocumentContext.js";
116
117
  import { isCanvasItemContextKey } from "../../../utils/canvasItemContext.js";
117
- import { StreamingJsonTracker } from "@ai-matrx/kit/json-extract";
118
+ import { StreamingJsonTracker } from "@ai-matrx/content-ir/json-extract";
118
119
  import { StreamBlockAccumulator } from "../utils/stream-block-accumulator.js";
119
120
  import { deriveAnswerDataText } from "../active-requests/active-requests.selectors.js";
120
121
  import { captureError } from "../../../../host/diagnostics.js";
@@ -143,7 +144,6 @@ import { getCapabilitiesForConversation } from "../../../runtime/get-model-capab
143
144
  import { toast } from "../../../../host/notify.js";
144
145
  import { isDirectiveApplyEvent } from "@ai-matrx/agents/envelope";
145
146
  import { proposeDirective } from "../../proposed-directives/proposedDirectivesSlice.js";
146
- const readTransportCursor = readStreamCursor;
147
147
  function renderBlockToContentType(type) {
148
148
  if (type === "image" || type === "image_output") return "image";
149
149
  if (type === "audio_output") return "audio";
@@ -395,7 +395,8 @@ async function processStream({
395
395
  recordTransportSeq({
396
396
  requestId,
397
397
  streamSeq: cursor.streamSeq,
398
- streamId: cursor.streamId
398
+ streamId: cursor.streamId,
399
+ marks: cursor.marks
399
400
  })
400
401
  );
401
402
  }
@@ -455,8 +456,22 @@ async function processStream({
455
456
  dispatch(setRequestStatus({ requestId, status: "error", error }));
456
457
  dispatch(setInstanceStatus({ conversationId, status: "error" }));
457
458
  };
458
- let lastTransportSeq = getState().activeRequests.byRequestId[requestId]?.lastTransportSeq ?? 0;
459
- let transportStreamId = getState().activeRequests.byRequestId[requestId]?.transportStreamId ?? null;
459
+ const seededRequest = getState().activeRequests.byRequestId[requestId];
460
+ const transportCursor = createStreamReplayCursor({
461
+ seed: {
462
+ streamId: seededRequest?.transportStreamId ?? null,
463
+ lastSeq: seededRequest?.lastTransportSeq ?? 0,
464
+ marks: seededRequest?.transportSeqByStream ?? {}
465
+ },
466
+ // New emitter segment: retain prior rendered content, but make the next
467
+ // client block begin after it. This mirrors a fresh local resume request
468
+ // without blanking the retained viewer.
469
+ onSegmentChange: () => {
470
+ blockAccumulator.rewindToBlockCount(
471
+ getState().activeRequests.byRequestId[requestId]?.renderBlockOrder.length ?? 0
472
+ );
473
+ }
474
+ });
460
475
  let retainedEntry;
461
476
  const consume = async (nextResponse, nextAbortController, nextOnEvent) => {
462
477
  if (retainedEntry.busy) {
@@ -480,25 +495,13 @@ async function processStream({
480
495
  let sawTerminalEvent = false;
481
496
  try {
482
497
  for await (const event of events) {
483
- const transportCursor = readTransportCursor(event);
484
- if (transportCursor !== null) {
485
- const segmentChanged = transportCursor.streamId !== null && transportStreamId !== null && transportCursor.streamId !== transportStreamId;
486
- if (segmentChanged) {
487
- blockAccumulator.rewindToBlockCount(
488
- getState().activeRequests.byRequestId[requestId]?.renderBlockOrder.length ?? 0
489
- );
490
- lastTransportSeq = 0;
491
- }
492
- if (transportCursor.streamId !== null) {
493
- transportStreamId = transportCursor.streamId;
494
- }
495
- if (transportCursor.streamSeq <= lastTransportSeq) {
496
- continue;
497
- }
498
- lastTransportSeq = transportCursor.streamSeq;
498
+ const stamp = readStreamCursor(event);
499
+ if (stamp !== null) {
500
+ if (!transportCursor.admit(event)) continue;
499
501
  pendingTransportCursor = {
500
- streamSeq: transportCursor.streamSeq,
501
- streamId: transportCursor.streamId
502
+ streamSeq: stamp.streamSeq,
503
+ streamId: stamp.streamId,
504
+ marks: transportCursor.marks
502
505
  };
503
506
  scheduleBatchEvent();
504
507
  }
@@ -1,4 +1,4 @@
1
- import { extractAllJson, extractFirstJson } from "@ai-matrx/kit/json-extract";
1
+ import { extractAllJson, extractFirstJson } from "@ai-matrx/content-ir/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";
@@ -18,8 +18,9 @@
18
18
  * follower may retry for minutes across a deploy drain by design), and no
19
19
  * org header (org rides the conversation body only, like `runAiStream`).
20
20
  */
21
+ import { type FollowServerJobOptions, type ServerJobOutcome } from "@ai-matrx/agents/matrx";
21
22
  import type { ResolvedBackend } from "../redux/execution-system/thunks/resolve-base-url.js";
22
- import type { RuntimeExecutionStatus, RuntimeOperationEvent, RuntimeOperationsByLinkResponse } from "./types.js";
23
+ import type { RuntimeOperationsByLinkResponse } from "./types.js";
23
24
  /**
24
25
  * `GET /runtime/operations/by-link/conversation/{conversationId}` — the
25
26
  * identify step. Returns `null` when the surface is absent or the caller owns
@@ -27,29 +28,9 @@ import type { RuntimeExecutionStatus, RuntimeOperationEvent, RuntimeOperationsBy
27
28
  * shape by design); throws on other failures.
28
29
  */
29
30
  export declare function fetchOperationsByLink(backend: ResolvedBackend, conversationId: string, signal?: AbortSignal): Promise<RuntimeOperationsByLinkResponse | null>;
30
- export interface FollowOperationResult {
31
- /** True when the server sent the terminal `end` frame. */
32
- ended: boolean;
33
- /** The root status carried on the `end` frame (when `ended`). */
34
- status: RuntimeExecutionStatus | null;
35
- }
36
- export interface FollowOperationOptions {
37
- backend: ResolvedBackend;
38
- executionId: string;
39
- /** Resume cursor — the operation view's `last_event_seq` (0 = from start). */
40
- lastEventSeq: number;
41
- /** Caller teardown — aborting resolves the promise with `ended: false`. */
42
- signal: AbortSignal;
43
- /** Fired per durable spine event (lifecycle transitions + notes). */
44
- onEvent: (event: RuntimeOperationEvent, seq: number | null) => void;
45
- }
46
31
  /**
47
- * `GET /runtime/executions/{id}/events/stream` — SSE replay-then-follow with
48
- * bounded reconnects, via the package's `followRuntimeOperationToEnd`
49
- * (production defaults). Resolves `{ended: true, status}` on the server's
50
- * `end` frame (the operation settled); `{ended: false}` when the caller
51
- * aborted or every reconnect attempt failed. A WAITING_INPUT park keeps the
52
- * stream open by design — a resume re-attaches to the same execution and its
53
- * events continue arriving here on the same cursor.
32
+ * Identify → rejoin → follow → settle for one conversation, on the shared
33
+ * `followServerJob` core over this backend's channel. Chat supplies only its
34
+ * wiring (the Redux rejoin consumer, banner callbacks, `loadConversation`).
54
35
  */
55
- export declare function followOperationStream(opts: FollowOperationOptions): Promise<FollowOperationResult>;
36
+ export declare function followConversationServerJob(backend: ResolvedBackend, conversationId: string, options: Omit<FollowServerJobOptions, "target">): Promise<ServerJobOutcome>;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createMatrxTransport,
3
- followRuntimeOperationToEnd,
3
+ followServerJob,
4
4
  getRuntimeOperationsByLink
5
5
  } from "@ai-matrx/agents/matrx";
6
6
  function transportFor(backend) {
@@ -21,19 +21,13 @@ async function fetchOperationsByLink(backend, conversationId, signal) {
21
21
  { limit: 5, ...signal ? { signal } : {} }
22
22
  );
23
23
  }
24
- async function followOperationStream(opts) {
25
- const result = await followRuntimeOperationToEnd(
26
- transportFor(opts.backend),
27
- opts.executionId,
28
- {
29
- lastEventSeq: opts.lastEventSeq,
30
- signal: opts.signal,
31
- onEvent: (event, seq) => opts.onEvent(event, seq)
32
- }
33
- );
34
- return { ended: result.ended, status: result.status };
24
+ function followConversationServerJob(backend, conversationId, options) {
25
+ return followServerJob(transportFor(backend), {
26
+ ...options,
27
+ target: { linkKind: "conversation", linkId: conversationId }
28
+ });
35
29
  }
36
30
  export {
37
31
  fetchOperationsByLink,
38
- followOperationStream
32
+ followConversationServerJob
39
33
  };