@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
@@ -738,6 +738,34 @@ export interface ChatAppIdentity {
738
738
  * `"local-only"` (confidential) conversation refuses every target but the Matrx Local engine on this
739
739
  * computer, loudly, before a byte leaves the machine. Default: `"any"` for every conversation.
740
740
  */
741
+ /** What a device-tool run answered (the same shape the browser-extension bridge answers). */
742
+ export type ChatDeviceToolInvocation = {
743
+ handled: false;
744
+ reason: string;
745
+ } | {
746
+ handled: true;
747
+ ok: true;
748
+ output: Record<string, unknown>;
749
+ } | {
750
+ handled: true;
751
+ ok: false;
752
+ error: string;
753
+ };
754
+ /**
755
+ * Tools that run ON THIS DEVICE for a cloud agent (lanes/agents-chat-proposal.md W0(c)): the
756
+ * browser tools when the chat runs inside Matrx Extend, local machine tools in a desktop host. The
757
+ * package owns the whole loop around them — the delegated call's lifecycle, the durable result
758
+ * submit with retry, and the resume (`surfaceDelegatedToolCall` → `submitToolResult`); the host
759
+ * only executes. `handled: false` = "not a tool this device owns" (the model is told, never a
760
+ * wedge). Absent: delegated device tools go to the installed browser extension through the server
761
+ * port (`invokeMatrxExtendTool`), as on the web.
762
+ */
763
+ export interface ChatDeviceToolsPort {
764
+ invoke(toolName: string, args: Record<string, unknown>, context: {
765
+ conversationId: string;
766
+ callId: string;
767
+ }): Promise<ChatDeviceToolInvocation>;
768
+ }
741
769
  export interface ChatRoutingPort {
742
770
  policy(conversationId: string): ChatRoutingPolicy;
743
771
  }
@@ -767,7 +795,16 @@ export interface ChatHost {
767
795
  * that is not matrx-frontend never labels its rows as the main app's.
768
796
  */
769
797
  app?: ChatAppIdentity;
770
- /** Default: from db.auth (getSession + onAuthStateChange); admin level from admin.admins. */
798
+ /**
799
+ * The token function a token-only client was built with (supabase-js `accessToken` option, the
800
+ * matrx-local custodian shape): pass the SAME function here and the default identity reads who is
801
+ * signed in from it. A token-only `db` without it is signed out, announced once with this remedy.
802
+ */
803
+ accessToken?: () => Promise<string | null>;
804
+ /**
805
+ * Default: from db.auth (getSession + onAuthStateChange); for a token-only `db`, from
806
+ * `accessToken`. Admin level from admin.admins either way.
807
+ */
771
808
  identity?: ChatIdentityPort;
772
809
  /** Default: no active org; `require` refuses loudly (never auto-picks). */
773
810
  org?: ChatOrgPort;
@@ -803,8 +840,10 @@ export interface ChatHost {
803
840
  canvas?: ChatCanvasPort;
804
841
  /** Default: every conversation may go to any target (`"any"`). */
805
842
  routing?: ChatRoutingPort;
843
+ /** Default: none — delegated device tools go to the browser extension via the server port. */
844
+ deviceTools?: ChatDeviceToolsPort;
806
845
  }
807
- export type ChatPortName = "identity" | "org" | "server" | "notify" | "diagnostics" | "prefs" | "navigation" | "windows" | "catalog" | "registry" | "chrome" | "feedback" | "routes" | "canvas" | "routing";
846
+ export type ChatPortName = "identity" | "org" | "server" | "notify" | "diagnostics" | "prefs" | "navigation" | "windows" | "catalog" | "registry" | "chrome" | "feedback" | "routes" | "canvas" | "routing" | "deviceTools";
808
847
  /** Every port present. `overridden` names the ports the host supplied itself. */
809
848
  export interface ResolvedChatHost {
810
849
  /** The host's client, or the no-database stand-in when it gave only a `source`. */
@@ -831,5 +870,7 @@ export interface ResolvedChatHost {
831
870
  routes: ChatRoutes;
832
871
  canvas: ChatCanvasPort;
833
872
  routing: ChatRoutingPort;
873
+ /** The host's on-device tool executor, or null. */
874
+ deviceTools: ChatDeviceToolsPort | null;
834
875
  overridden: ReadonlySet<ChatPortName>;
835
876
  }
package/dist/host/db.js CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  AuthSessionMissingError
4
4
  } from "@supabase/supabase-js";
5
5
  import { getChatHost } from "./configure.js";
6
+ import { decodeTokenClaims, isTokenOnlyClient } from "./defaults/identity.js";
6
7
  function createClient() {
7
8
  return getChatHost().db;
8
9
  }
@@ -35,6 +36,11 @@ function userFromClaims(claims) {
35
36
  };
36
37
  }
37
38
  async function getClaimsUser(client, jwt) {
39
+ if (isTokenOnlyClient(client)) {
40
+ const token = jwt ?? await getChatHost().identity.getAccessToken().catch(() => null);
41
+ const claims = token ? decodeTokenClaims(token) : null;
42
+ return { data: { user: userFromClaims(claims ?? void 0) }, error: null };
43
+ }
38
44
  const { data, error } = await client.auth.getClaims(jwt);
39
45
  if (error) {
40
46
  if (isSignedOutError(error)) return { data: { user: null }, error: null };
@@ -27,6 +27,11 @@ export interface LogClientErrorDiagnosticsOptions {
27
27
  /** Default: `process.env.NODE_ENV === "production"`. */
28
28
  persist?: boolean;
29
29
  flushDelayMs?: number;
30
+ /**
31
+ * Is someone signed in? The resolver passes the identity port's answer, so a token-only client
32
+ * works. Default (a db with an auth-js session): `db.auth.getSession()`.
33
+ */
34
+ signedIn?: () => Promise<boolean>;
30
35
  }
31
36
  export declare function isChatSourceApp(value: unknown): value is ChatSourceApp;
32
37
  export declare function createLogClientErrorDiagnostics(db: ChatDb, options: LogClientErrorDiagnosticsOptions): ChatDiagnosticsPort;
@@ -59,8 +59,7 @@ function createLogClientErrorDiagnostics(db, options) {
59
59
  if (queue.length === 0) return;
60
60
  let signedIn = false;
61
61
  try {
62
- const { data } = await db.auth.getSession();
63
- signedIn = Boolean(data.session?.user);
62
+ signedIn = options.signedIn ? await options.signedIn() : Boolean((await db.auth.getSession()).data.session?.user);
64
63
  } catch {
65
64
  signedIn = false;
66
65
  }
@@ -3,6 +3,32 @@
3
3
  * admin level from `admin.admins` (RLS: a person reads their own row). Starts
4
4
  * on first use, never at import, so SSR and tests that never ask pay nothing.
5
5
  */
6
+ import type { User } from "@supabase/supabase-js";
6
7
  import type { ChatDb, ChatDiagnosticsPort, ChatIdentity, ChatIdentityPort } from "../contract.js";
7
8
  export declare const SIGNED_OUT_IDENTITY: ChatIdentity;
8
9
  export declare function createDbIdentity(db: ChatDb, diagnostics: () => ChatDiagnosticsPort): ChatIdentityPort;
10
+ /**
11
+ * Is this a supabase-js client built with the `accessToken` option? Such a client has no auth-js
12
+ * session: touching `db.auth` throws by construction (matrx-local's custodian client is one). The
13
+ * probe reads the property inside a try, so it never throws itself.
14
+ */
15
+ export declare function isTokenOnlyClient(db: unknown): boolean;
16
+ /** A JWT's payload as an object (no verification — routing metadata only). */
17
+ export declare function decodeTokenClaims(token: string): Record<string, unknown> | null;
18
+ /**
19
+ * The person a token names, read from its claims — routing metadata, not verification (the
20
+ * server and RLS verify every call that token makes). `null` when the token names nobody.
21
+ */
22
+ export declare function userFromAccessToken(token: string | null): User | null;
23
+ /**
24
+ * Identity for a token-only client: the host hands the same `accessToken` function it gave
25
+ * supabase-js, and the package reads who is signed in from it. Every `getAccessToken()` re-reads
26
+ * the token, so a refresh or an account switch reaches the snapshot (and its subscribers) the next
27
+ * time anything asks; admin level is read through `db` exactly like the session default.
28
+ */
29
+ export declare function createTokenIdentity(db: ChatDb, getToken: () => Promise<string | null>, diagnostics: () => ChatDiagnosticsPort): ChatIdentityPort;
30
+ /**
31
+ * A token-only client with no `accessToken` function: nobody can be read. Signed out and settled
32
+ * (never a forever-loading screen), announced once with the remedy.
33
+ */
34
+ export declare function createUnreadableTokenIdentity(announce: (message: string) => void): ChatIdentityPort;
@@ -113,7 +113,139 @@ function createDbIdentity(db, diagnostics) {
113
113
  }
114
114
  };
115
115
  }
116
+ function isTokenOnlyClient(db) {
117
+ if (db === null || typeof db !== "object") return false;
118
+ try {
119
+ const auth = db.auth;
120
+ if (!auth) return true;
121
+ void auth.getSession;
122
+ return false;
123
+ } catch {
124
+ return true;
125
+ }
126
+ }
127
+ function decodeTokenClaims(token) {
128
+ const payload = token.split(".")[1];
129
+ if (!payload) return null;
130
+ try {
131
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
132
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
133
+ const parsed = JSON.parse(
134
+ typeof atob === "function" ? atob(padded) : Buffer.from(padded, "base64").toString("utf8")
135
+ );
136
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ function userFromAccessToken(token) {
142
+ if (!token) return null;
143
+ const claims = decodeTokenClaims(token);
144
+ if (!claims || typeof claims.sub !== "string" || !claims.sub) return null;
145
+ const meta = claims.user_metadata;
146
+ const appMeta = claims.app_metadata;
147
+ return {
148
+ id: claims.sub,
149
+ email: typeof claims.email === "string" ? claims.email : void 0,
150
+ user_metadata: meta !== null && typeof meta === "object" ? meta : {},
151
+ app_metadata: appMeta !== null && typeof appMeta === "object" ? appMeta : {},
152
+ aud: typeof claims.aud === "string" ? claims.aud : "authenticated",
153
+ created_at: ""
154
+ };
155
+ }
156
+ function createTokenIdentity(db, getToken, diagnostics) {
157
+ let snapshot = SIGNED_OUT_IDENTITY;
158
+ let started = false;
159
+ let adminFor = null;
160
+ const listeners = /* @__PURE__ */ new Set();
161
+ function publish(next) {
162
+ if (snapshot.userId === next.userId && snapshot.accessToken === next.accessToken && snapshot.adminLevel === next.adminLevel && snapshot.authReady === next.authReady) {
163
+ return;
164
+ }
165
+ snapshot = next;
166
+ for (const listener of listeners) listener();
167
+ }
168
+ async function readAdminLevel(userId) {
169
+ try {
170
+ const { data, error } = await db.schema("admin").from("admins").select("level").eq("user_id", userId).maybeSingle();
171
+ if (error) throw error;
172
+ const level = data?.level;
173
+ return ADMIN_LEVELS.includes(level) ? level : null;
174
+ } catch (error) {
175
+ diagnostics().capture(error, { area: "identity", code: "admin-level-unreadable" });
176
+ return null;
177
+ }
178
+ }
179
+ async function refresh() {
180
+ let token;
181
+ try {
182
+ token = await getToken();
183
+ } catch (error) {
184
+ diagnostics().capture(error, { area: "identity", code: "access-token-unreadable" });
185
+ token = null;
186
+ }
187
+ const user = userFromAccessToken(token);
188
+ const level = user && user.id === snapshot.userId ? snapshot.adminLevel : null;
189
+ publish(identityFromUser(user, level, user ? token : null));
190
+ if (user && adminFor !== user.id) {
191
+ adminFor = user.id;
192
+ const read = await readAdminLevel(user.id);
193
+ if (snapshot.userId === user.id) publish(identityFromUser(user, read, snapshot.accessToken));
194
+ }
195
+ return user ? token : null;
196
+ }
197
+ function start() {
198
+ if (started) return;
199
+ started = true;
200
+ void refresh();
201
+ }
202
+ return {
203
+ current() {
204
+ start();
205
+ return snapshot;
206
+ },
207
+ subscribe(listener) {
208
+ start();
209
+ listeners.add(listener);
210
+ return () => {
211
+ listeners.delete(listener);
212
+ };
213
+ },
214
+ getAccessToken: refresh
215
+ };
216
+ }
217
+ function createUnreadableTokenIdentity(announce) {
218
+ const value = Object.freeze({ ...SIGNED_OUT_IDENTITY, authReady: true });
219
+ let said = false;
220
+ const say = () => {
221
+ if (said) return;
222
+ said = true;
223
+ announce(
224
+ "This chat's database client was built with an access-token function, so the chat cannot read who is signed in. Pass the same function to the chat host as `accessToken`."
225
+ );
226
+ };
227
+ return {
228
+ current() {
229
+ say();
230
+ return value;
231
+ },
232
+ subscribe() {
233
+ say();
234
+ return () => {
235
+ };
236
+ },
237
+ async getAccessToken() {
238
+ say();
239
+ return null;
240
+ }
241
+ };
242
+ }
116
243
  export {
117
244
  SIGNED_OUT_IDENTITY,
118
- createDbIdentity
245
+ createDbIdentity,
246
+ createTokenIdentity,
247
+ createUnreadableTokenIdentity,
248
+ decodeTokenClaims,
249
+ isTokenOnlyClient,
250
+ userFromAccessToken
119
251
  };
@@ -50,8 +50,25 @@ export declare function isNotAuthenticatedError(error: unknown): error is NotAut
50
50
  /** The signed-in person's id; throws `NotAuthenticatedError` when nobody is. */
51
51
  export declare function requireUserId(): string;
52
52
  /**
53
- * Does this client carry a signed-in session? A local read of the host's
54
- * `db.auth` (no network). For skipping reads only a signed-in person can make;
53
+ * The signed-in person's access token, or null — THE one session read in the package. It goes
54
+ * through the host's identity port, so a session client (auth-js) and a token-only client
55
+ * (`ChatHost.accessToken`) answer the same way; nothing else in the package touches `db.auth`
56
+ * (guard: `host/__tests__/session-reads-go-through-the-identity-port.test.ts`).
57
+ */
58
+ export declare function readSessionToken(): Promise<string | null>;
59
+ /** `readSessionToken` in the structural shape `@ai-matrx/data`'s session retry reads. */
60
+ export declare const hostSessionSource: {
61
+ getSession(): Promise<{
62
+ data: {
63
+ session: {
64
+ access_token: string;
65
+ } | null;
66
+ };
67
+ error: null;
68
+ }>;
69
+ };
70
+ /**
71
+ * Does this client carry a signed-in session? For skipping reads only a signed-in person can make;
55
72
  * "no session" means an empty result, never a way around a reader a guest needs.
56
73
  */
57
74
  export declare function hasBrowserSession(): Promise<boolean>;
@@ -40,11 +40,11 @@ function requireUserId() {
40
40
  if (!id) throw new NotAuthenticatedError();
41
41
  return id;
42
42
  }
43
- async function hasBrowserSession() {
44
- if (!chatHostHasDatabase()) return false;
45
- let db;
43
+ async function readSessionToken() {
44
+ if (!chatHostHasDatabase()) return null;
45
+ let host;
46
46
  try {
47
- db = getChatHost().db;
47
+ host = getChatHost();
48
48
  } catch (error) {
49
49
  announceOnce(
50
50
  "identity:no-host-session",
@@ -52,16 +52,22 @@ async function hasBrowserSession() {
52
52
  error?.message ?? error
53
53
  )}).`
54
54
  );
55
- return false;
55
+ return null;
56
56
  }
57
57
  try {
58
- const {
59
- data: { session }
60
- } = await db.auth.getSession();
61
- return Boolean(session);
58
+ return await host.identity.getAccessToken();
62
59
  } catch {
63
- return false;
60
+ return null;
61
+ }
62
+ }
63
+ const hostSessionSource = {
64
+ async getSession() {
65
+ const token = await readSessionToken();
66
+ return { data: { session: token ? { access_token: token } : null }, error: null };
64
67
  }
68
+ };
69
+ async function hasBrowserSession() {
70
+ return await readSessionToken() !== null;
65
71
  }
66
72
  async function isSignedOutVisitor() {
67
73
  return !getUserId() && !await hasBrowserSession();
@@ -70,8 +76,10 @@ export {
70
76
  NotAuthenticatedError,
71
77
  getUserId,
72
78
  hasBrowserSession,
79
+ hostSessionSource,
73
80
  isNotAuthenticatedError,
74
81
  isSignedOutVisitor,
82
+ readSessionToken,
75
83
  requireUserId,
76
84
  selectAccessToken,
77
85
  selectActiveUserName,
@@ -1,11 +1,11 @@
1
1
  import { createSessionRetry } from "@ai-matrx/data/db";
2
- import { supabase } from "./db.js";
2
+ import { hostSessionSource } from "./identity.js";
3
3
  import {
4
4
  isMissingSessionError,
5
5
  SessionUnavailableError
6
6
  } from "@ai-matrx/data/db";
7
7
  const runWithSessionRetry = createSessionRetry({
8
- auth: { getSession: () => supabase.auth.getSession() }
8
+ auth: hostSessionSource
9
9
  });
10
10
  export {
11
11
  SessionUnavailableError,
@@ -16,6 +16,7 @@ import { ensureOrgAvailability } from "../../host/ui-slots.js";
16
16
  import { assertMappingsAreAnswerable } from "../../ui/mandates-storage/provision-shapes.js";
17
17
  import { ensureOrgId } from "../../host/org.js";
18
18
  import { readProjectScopeOrganizationId } from "../../host/ui-slots.js";
19
+ import { hasBrowserSession } from "../../host/identity.js";
19
20
  function tierFromScope(scope) {
20
21
  if (scope.userId) return "user";
21
22
  if (scope.organizationId) return "org";
@@ -359,10 +360,7 @@ async function listAgentSurfaceBindings(agentId) {
359
360
  async function fetchSurfaceBindingLayers(agentId, surfaceName) {
360
361
  const chain = [...getSurfaceAncestry(surfaceName), surfaceName];
361
362
  const sb = createClient();
362
- const {
363
- data: { session }
364
- } = await sb.auth.getSession();
365
- if (!session) return [];
363
+ if (!await hasBrowserSession()) return [];
366
364
  const { data, error } = await sb.schema("agent").from("menu_surface").select(MENU_SURFACE_COLUMNS).eq("agent_id", agentId).in("surface_name", chain);
367
365
  if (error) throw error;
368
366
  const rows = (data ?? []).map(fromRow).filter(
@@ -9,7 +9,7 @@ import { useConversationTitle } from "../../../agents/hooks/useConversationTitle
9
9
  import { EntityRef } from "../../../host/ui-slots.js";
10
10
  import MarkdownStream from "../../../ui/markdown-stream/MarkdownStream.js";
11
11
  import { AnswerValueView } from "../../../host/ui-slots.js";
12
- import { stripThinkingStreaming } from "@ai-matrx/kit/text";
12
+ import { stripThinkingStreaming } from "@ai-matrx/content-ir/source";
13
13
  import { cn } from "@ai-matrx/design-system";
14
14
  import { getCollabCallInfo } from "./collab.js";
15
15
  import { ErrorAlchemyMenu } from "../../../host/ui-slots.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/chat",
3
- "version": "0.4.12",
3
+ "version": "0.4.14",
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",
@@ -230,8 +230,8 @@
230
230
  "@ai-matrx/alchemy": "latest",
231
231
  "@ai-matrx/associations": "latest",
232
232
  "@ai-matrx/browser-audio": "latest",
233
- "@ai-matrx/content-ir": "latest",
234
233
  "@ai-matrx/canvas": "latest",
234
+ "@ai-matrx/content-ir": "latest",
235
235
  "@ai-matrx/data": "latest",
236
236
  "@ai-matrx/content-ir-react": "latest",
237
237
  "@ai-matrx/design-system": "latest",
@@ -240,12 +240,12 @@
240
240
  "@ai-matrx/kit": "latest",
241
241
  "@ai-matrx/media": "latest",
242
242
  "@ai-matrx/realtime": "latest",
243
+ "@ai-matrx/records": "latest",
243
244
  "@ai-matrx/records-ui": "latest",
244
- "@ai-matrx/rich-content": "latest",
245
- "@ai-matrx/print": "latest",
246
245
  "@ai-matrx/rich-editor": "latest",
246
+ "@ai-matrx/rich-content": "latest",
247
247
  "@ai-matrx/tap-target": "latest",
248
- "@ai-matrx/records": "latest"
248
+ "@ai-matrx/print": "latest"
249
249
  },
250
250
  "devDependencies": {
251
251
  "@arethetypeswrong/cli": "^0.18.5",
@@ -342,6 +342,7 @@
342
342
  "check:front-door": "node ./scripts/check-front-door.mjs",
343
343
  "check:front-door:self-test": "node ./scripts/check-front-door.mjs --self-test",
344
344
  "check:front-door:canary": "node ./scripts/check-front-door.mjs --root ./canary/src",
345
- "deps:write": "node ./scripts/finish-build.mjs --write-deps"
345
+ "deps:write": "node ./scripts/finish-build.mjs --write-deps",
346
+ "check:side-panel-bundle": "node ./scripts/check-side-panel-bundle.mjs"
346
347
  }
347
348
  }