@ai-matrx/chat 0.4.12 → 0.4.14

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 (29) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/agents/redux/execution-system/active-requests/active-requests.selectors.js +1 -1
  3. package/dist/agents/redux/execution-system/active-requests/active-requests.slice.d.ts +2 -0
  4. package/dist/agents/redux/execution-system/active-requests/active-requests.slice.js +7 -0
  5. package/dist/agents/redux/execution-system/conversations/conversation-persistence.js +2 -4
  6. package/dist/agents/redux/execution-system/thunks/dispatch-matrx-extend-tool.thunk.js +3 -1
  7. package/dist/agents/redux/execution-system/thunks/process-stream.js +26 -23
  8. package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +1 -1
  9. package/dist/agents/runtime-reconnect/api.d.ts +6 -25
  10. package/dist/agents/runtime-reconnect/api.js +7 -13
  11. package/dist/agents/runtime-reconnect/reconnect-server-operation.thunk.js +167 -159
  12. package/dist/agents/runtime-reconnect/types.d.ts +2 -0
  13. package/dist/agents/services/mcp-connections.service.d.ts +12 -0
  14. package/dist/agents/services/mcp-connections.service.js +4 -7
  15. package/dist/agents/types/request.types.d.ts +6 -0
  16. package/dist/agents/utils/json-extraction.js +2 -2
  17. package/dist/host/configure.js +15 -5
  18. package/dist/host/contract.d.ts +43 -2
  19. package/dist/host/db.js +6 -0
  20. package/dist/host/defaults/diagnostics.d.ts +5 -0
  21. package/dist/host/defaults/diagnostics.js +1 -2
  22. package/dist/host/defaults/identity.d.ts +26 -0
  23. package/dist/host/defaults/identity.js +133 -1
  24. package/dist/host/identity.d.ts +19 -2
  25. package/dist/host/identity.js +18 -10
  26. package/dist/host/session-retry.js +2 -2
  27. package/dist/surfaces/services/bind-agent-to-surface.service.js +2 -4
  28. package/dist/tool-call-visualization/renderers/agent-call/CollabCallCard.js +1 -1
  29. package/package.json +7 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @ai-matrx/chat
2
2
 
3
+ ## 0.4.14 — token-only hosts, on-device tools, and a bundle budget (W0(b), W0(c), W0(e))
4
+
5
+ - **Token-only database clients** (supabase-js `accessToken` option — matrx-local's custodian
6
+ client, where `db.auth` throws): new host field `accessToken: () => Promise<string | null>`; the
7
+ default identity reads the person from that token (admin level through `db`). A token-only `db`
8
+ with no function is signed out and settled, announced once with the remedy. The resolver no
9
+ longer touches `db.auth` on such a client.
10
+ - **One session door:** every package session read goes through the identity port
11
+ (`readSessionToken`, `hasBrowserSession`, `hostSessionSource` in `host/identity`); guard test
12
+ `host/__tests__/session-reads-go-through-the-identity-port.test.ts` fails on a direct auth-js call.
13
+ - **`deviceTools` host port** (`ChatDeviceToolsPort`): a host that IS the device (chat inside Matrx
14
+ Extend, a desktop app) executes delegated device tools; the package keeps the lifecycle, the
15
+ durable result submit and the resume. Absent: unchanged web route through the extension bridge.
16
+ - **Side-panel bundle budget:** `pnpm check:side-panel-bundle` builds the bare Vite canary and fails
17
+ when the first chat screen or the whole output (gzip) passes `side-panel-bundle-budget.json`
18
+ (measured on 0.4.9: 2435 kB / 8197 kB — a ratchet, lower it when it shrinks).
19
+
20
+ Consumer action: none for session (auth-js) hosts. A token-only host passes `accessToken` (the same
21
+ function its Supabase client got). A device host passes `deviceTools`.
22
+
23
+ ## 0.4.14 — reconnect rides the shared follow core
24
+
25
+ - Reconnect (`reconnectServerOperation`) now identifies, rejoins, follows and settles through
26
+ `followServerJob` (@ai-matrx/agents 0.57.0); `processStream` dedupes replay through the shared
27
+ `createStreamReplayCursor` (a high-water mark per `stream_id`, persisted on `ActiveRequest` as
28
+ `transportSeqByStream`). The chat-only follower (`followOperationStream`) is deleted. Fixes
29
+ replayed segment A rendering twice after a live A then B.
30
+
31
+ Consumer action: none.
32
+
33
+ ## 0.4.13 — markdown-source helpers from their one home
34
+
35
+ - `StreamingJsonTracker`, `extractAllJson`, `extractFirstJson`, `findAllFencedBlocks` are read from
36
+ `@ai-matrx/content-ir/json-extract`, and `stripThinkingStreaming` from `@ai-matrx/content-ir/source`
37
+ (kit 0.36.0 no longer carries them).
38
+
39
+ Consumer action: none.
40
+
3
41
  ## Unreleased
4
42
 
5
43
  - 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.
@@ -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
  );
@@ -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
  };