@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
@@ -2,9 +2,7 @@ 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
4
  import {
5
- pickServerJobOperation,
6
- runtimeOperationRejoinPath,
7
- settleRunPickup
5
+ runtimeOperationRejoinPath
8
6
  } from "@ai-matrx/agents/matrx";
9
7
  import { toast } from "../../host/notify.js";
10
8
  import { captureError } from "../../host/diagnostics.js";
@@ -26,7 +24,7 @@ import { resolveBackendForConversation } from "../redux/execution-system/thunks/
26
24
  import { surfaceColdPendingCalls } from "../redux/execution-system/thunks/surface-cold-pending-calls.thunk.js";
27
25
  import { resumeInstance } from "../redux/execution-system/thunks/resume-instance.thunk.js";
28
26
  import { selectActivePendingAsksForConversation } from "../ui-first-tools/redux/pending-asks.slice.js";
29
- import { fetchOperationsByLink, followOperationStream } from "./api.js";
27
+ import { followConversationServerJob } from "./api.js";
30
28
  import { decideAfterContinueRefused, decideWaitingInputRecovery } from "./waiting-input-recovery.js";
31
29
  import { countOpenAsksForConversation } from "./parked-on-person.js";
32
30
  const RUNNING_KINDS = /* @__PURE__ */ new Set(["started", "resumed"]);
@@ -69,56 +67,25 @@ const reconnectServerOperation = createAsyncThunk(
69
67
  followers.delete(conversationId);
70
68
  };
71
69
  let op = null;
72
- let settledWhileDisconnected = false;
73
- try {
74
- const byLink = await retryReconnect(
75
- () => fetchOperationsByLink(backend, conversationId, ctrl.signal),
76
- // org-filter: server-call the conversation's own organization rides the backend headers; the read is by conversation id
77
- ctrl.signal,
78
- stillOwner
79
- );
80
- if (!stillOwner()) {
81
- clearFollower();
82
- return noFollow;
83
- }
84
- const picked = pickServerJobOperation(byLink?.operations);
85
- op = picked && !picked.is_terminal ? picked : null;
86
- settledWhileDisconnected = Boolean(picked?.is_terminal);
87
- } catch (err) {
88
- const superseded = ctrl.signal.aborted || !stillOwner();
89
- clearFollower();
90
- if (superseded) return noFollow;
91
- console.warn(
92
- "[runtime-reconnect] operations-by-link fetch failed \u2014 falling back to legacy recovery.",
93
- { conversationId, err }
94
- );
95
- if (source === "stream-loss") fallbackToLegacyRecovery();
96
- return noFollow;
97
- }
98
- if (!op) {
99
- clearFollower();
100
- if (source === "stream-loss") fallbackToLegacyRecovery();
101
- else if (settledWhileDisconnected)
102
- await dispatch(loadConversation({ conversationId })).unwrap();
103
- return noFollow;
104
- }
105
- console.warn(
106
- "[runtime-reconnect] non-terminal server operation found \u2014 following its event stream.",
107
- {
108
- conversationId,
109
- executionId: op.execution_id,
110
- status: op.status,
111
- source
112
- }
113
- );
70
+ const liveOp = () => {
71
+ if (!op) throw new Error("reconnect: no server operation identified yet");
72
+ return op;
73
+ };
74
+ let identified = false;
75
+ let superseded = false;
76
+ let sawLive = false;
77
+ let rejoinRan = false;
78
+ let standDown = false;
79
+ let waitingRecoveryInFlight = false;
114
80
  const stampOperation = (status, waitingInput, recoveryState) => {
81
+ const current = liveOp();
115
82
  const serverOperation = {
116
- executionId: op.execution_id,
117
- userRequestId: op.request_id,
83
+ executionId: current.execution_id,
84
+ userRequestId: current.request_id,
118
85
  status,
119
86
  waitingInput,
120
87
  ...recoveryState ? { recoveryState } : {},
121
- startedAt: op.started_at,
88
+ startedAt: current.started_at,
122
89
  checkedAt: (/* @__PURE__ */ new Date()).toISOString()
123
90
  };
124
91
  dispatch(patchConversation({ conversationId, serverOperation }));
@@ -131,10 +98,10 @@ const reconnectServerOperation = createAsyncThunk(
131
98
  }
132
99
  dispatch(setInstanceStatus({ conversationId, status: "paused" }));
133
100
  };
134
- let waitingRecoveryInFlight = false;
135
101
  const recoverWaitingInput = async () => {
136
102
  if (waitingRecoveryInFlight) return;
137
103
  waitingRecoveryInFlight = true;
104
+ const waiting = liveOp();
138
105
  stampOperation("waiting_input", true, "checking_for_prompt");
139
106
  try {
140
107
  const surfaced = await dispatch(
@@ -148,7 +115,7 @@ const reconnectServerOperation = createAsyncThunk(
148
115
  } catch (err) {
149
116
  console.error(
150
117
  "[runtime-reconnect] could not read the person's open asks; not resuming a turn that may be waiting on one.",
151
- { conversationId, executionId: op.execution_id, err }
118
+ { conversationId, executionId: waiting.execution_id, err }
152
119
  );
153
120
  stampOperation("waiting_input", true, "checking_for_prompt");
154
121
  return;
@@ -158,7 +125,7 @@ const reconnectServerOperation = createAsyncThunk(
158
125
  pendingCallCount: surfaced,
159
126
  pendingAskCount: asks.length,
160
127
  parkedOnPersonCount,
161
- userRequestId: op.request_id
128
+ userRequestId: waiting.request_id
162
129
  });
163
130
  if (decision === "prompt_visible") {
164
131
  stampOperation("waiting_input", true, "prompt_visible");
@@ -175,12 +142,12 @@ const reconnectServerOperation = createAsyncThunk(
175
142
  if (decision === "needs_action") {
176
143
  console.error(
177
144
  "[runtime-reconnect] waiting operation has no pending prompt and no request_id; user action is required.",
178
- { conversationId, executionId: op.execution_id }
145
+ { conversationId, executionId: waiting.execution_id }
179
146
  );
180
147
  stampOperation("waiting_input", true, "needs_action");
181
148
  return;
182
149
  }
183
- const userRequestId = op.request_id;
150
+ const userRequestId = waiting.request_id;
184
151
  if (!userRequestId) {
185
152
  stampOperation("waiting_input", true, "needs_action");
186
153
  return;
@@ -189,7 +156,7 @@ const reconnectServerOperation = createAsyncThunk(
189
156
  "[runtime-reconnect] waiting operation has zero pending calls \u2014 continuing the resolved request.",
190
157
  {
191
158
  conversationId,
192
- executionId: op.execution_id,
159
+ executionId: waiting.execution_id,
193
160
  userRequestId
194
161
  }
195
162
  );
@@ -202,7 +169,7 @@ const reconnectServerOperation = createAsyncThunk(
202
169
  ).then((action) => {
203
170
  if (!resumeInstance.rejected.match(action)) return;
204
171
  const current = getState().conversations.byConversationId[conversationId]?.serverOperation;
205
- if (current?.executionId !== op.execution_id) return;
172
+ if (current?.executionId !== waiting.execution_id) return;
206
173
  const next = decideAfterContinueRefused({
207
174
  originalErrorName: action.meta.originalErrorName,
208
175
  liveStream: hasAbortController(conversationId)
@@ -216,25 +183,24 @@ const reconnectServerOperation = createAsyncThunk(
216
183
  } catch (err) {
217
184
  console.error(
218
185
  "[runtime-reconnect] pending-call recovery check failed; preserving an explicit retry affordance.",
219
- { conversationId, executionId: op.execution_id, err }
186
+ { conversationId, executionId: waiting.execution_id, err }
220
187
  );
221
188
  stampOperation("waiting_input", true, "needs_action");
222
189
  } finally {
223
190
  waitingRecoveryInFlight = false;
224
191
  }
225
192
  };
226
- stampOperation(
227
- op.status,
228
- op.waiting_input,
229
- op.waiting_input ? "checking_for_prompt" : void 0
230
- );
231
- if (!op.waiting_input && op.request_id) {
193
+ const consumeRejoin = async ({
194
+ operation,
195
+ requestId: rejoinId
196
+ }) => {
197
+ rejoinRan = true;
232
198
  const activeRequests = getState().activeRequests.byRequestId;
233
199
  const hintedRequest = requestId ? activeRequests[requestId] : void 0;
234
200
  const matchingRequest = Object.values(activeRequests).find(
235
- (candidate) => candidate.conversationId === conversationId && (candidate.requestId === op.request_id || candidate.serverRequestId === op.request_id)
201
+ (candidate) => candidate.conversationId === conversationId && (candidate.requestId === rejoinId || candidate.serverRequestId === rejoinId || candidate.requestId === operation.request_id || candidate.serverRequestId === operation.request_id)
236
202
  );
237
- const localRequestId = hintedRequest?.conversationId === conversationId ? hintedRequest.requestId : matchingRequest?.requestId ?? op.request_id;
203
+ const localRequestId = hintedRequest?.conversationId === conversationId ? hintedRequest.requestId : matchingRequest?.requestId ?? rejoinId;
238
204
  dispatch(createRequest({ requestId: localRequestId, conversationId }));
239
205
  dispatch(
240
206
  setRequestStatus({ requestId: localRequestId, status: "connecting" })
@@ -243,7 +209,7 @@ const reconnectServerOperation = createAsyncThunk(
243
209
  await runAiStream({
244
210
  requestId: localRequestId,
245
211
  conversationId,
246
- url: `${backend.baseUrl}${runtimeOperationRejoinPath(op.request_id)}`,
212
+ url: `${backend.baseUrl}${runtimeOperationRejoinPath(rejoinId)}`,
247
213
  headers: backend.headers,
248
214
  body: {},
249
215
  channel: backend.channel,
@@ -258,117 +224,159 @@ const reconnectServerOperation = createAsyncThunk(
258
224
  );
259
225
  }
260
226
  });
261
- const refreshed = await retryReconnect(
262
- () => fetchOperationsByLink(backend, conversationId, ctrl.signal),
263
- // org-filter: server-call the conversation's own organization rides the backend headers; the read is by conversation id
264
- ctrl.signal,
265
- stillOwner
266
- );
267
- if (!stillOwner()) {
268
- clearFollower();
269
- return noFollow;
270
- }
271
- const latest = pickServerJobOperation(refreshed?.operations);
272
- const stillLive = latest && !latest.is_terminal ? latest : null;
273
- if (stillLive) {
274
- if (followers.get(conversationId) === ctrl) {
275
- followers.delete(conversationId);
227
+ } catch (error) {
228
+ if (error instanceof StreamRejoinUnavailableError) throw error;
229
+ standDown = true;
230
+ return "stand-down";
231
+ }
232
+ };
233
+ let outcome;
234
+ try {
235
+ outcome = await followConversationServerJob(backend, conversationId, {
236
+ signal: ctrl.signal,
237
+ // org-filter: server-call the conversation's own organization rides the backend headers; the read is by conversation id
238
+ retry: (attempt) => retryReconnect(attempt, ctrl.signal, stillOwner),
239
+ onStatus: (status, operation) => {
240
+ if (status === "connecting") return;
241
+ if (!identified) {
242
+ identified = true;
243
+ if (!stillOwner()) {
244
+ superseded = true;
245
+ ctrl.abort();
246
+ return;
247
+ }
276
248
  }
249
+ if (!operation) return;
250
+ op = operation;
251
+ if (!sawLive && !operation.is_terminal) {
252
+ sawLive = true;
253
+ console.warn(
254
+ "[runtime-reconnect] non-terminal server operation found \u2014 following its event stream.",
255
+ {
256
+ conversationId,
257
+ executionId: operation.execution_id,
258
+ status: operation.status,
259
+ source
260
+ }
261
+ );
262
+ stampOperation(
263
+ operation.status,
264
+ operation.waiting_input,
265
+ operation.waiting_input ? "checking_for_prompt" : void 0
266
+ );
267
+ if (operation.waiting_input) void recoverWaitingInput();
268
+ }
269
+ },
270
+ rejoinConsumer: consumeRejoin,
271
+ onRejoinUnavailable: () => {
272
+ const current = liveOp();
273
+ console.warn(
274
+ "[runtime-reconnect] live response replay unavailable \u2014 following durable lifecycle instead.",
275
+ { conversationId, executionId: current.execution_id }
276
+ );
277
+ captureError({
278
+ source: "agent-stream-warning",
279
+ code: "live_replay_unavailable",
280
+ message: "A running response could not replay its live NDJSON; following durable lifecycle until the saved result is available.",
281
+ requestId: current.request_id ?? void 0,
282
+ conversationId,
283
+ raw: { executionId: current.execution_id, source }
284
+ });
285
+ stampOperation(current.status, false, "live_replay_unavailable");
286
+ },
287
+ afterRejoin: () => {
288
+ if (!stillOwner()) {
289
+ superseded = true;
290
+ return "handoff";
291
+ }
292
+ clearFollower();
277
293
  void dispatch(
278
294
  reconnectServerOperation({ conversationId, source: "cold-load" })
279
295
  );
280
- return { followed: true, finalStatus: null };
281
- }
282
- await dispatch(loadConversation({ conversationId })).unwrap();
283
- if (followers.get(conversationId) === ctrl) {
284
- followers.delete(conversationId);
285
- }
286
- dispatch(patchConversation({ conversationId, serverOperation: null }));
287
- return { followed: true, finalStatus: "completed" };
288
- } catch (error) {
289
- if (!(error instanceof StreamRejoinUnavailableError)) {
290
- if (followers.get(conversationId) === ctrl) {
291
- followers.delete(conversationId);
296
+ return "handoff";
297
+ },
298
+ onOperationEvent: (event) => {
299
+ if (hasAbortController(conversationId)) {
300
+ ctrl.abort();
301
+ return;
302
+ }
303
+ if (event.kind === "waiting_input") {
304
+ void recoverWaitingInput();
305
+ } else if (RUNNING_KINDS.has(event.kind)) {
306
+ stampOperation("running", false);
307
+ } else if (event.kind === "paused") {
308
+ stampOperation("paused", false);
292
309
  }
293
- return noFollow;
310
+ },
311
+ reloadSavedResult: async ({ status }) => {
312
+ if (status === "none") return;
313
+ if (rejoinRan && !stillOwner()) {
314
+ superseded = true;
315
+ return;
316
+ }
317
+ if (!sawLive && source === "stream-loss") {
318
+ fallbackToLegacyRecovery();
319
+ return;
320
+ }
321
+ await dispatch(loadConversation({ conversationId })).unwrap();
294
322
  }
295
- console.warn(
296
- "[runtime-reconnect] live response replay unavailable \u2014 following durable lifecycle instead.",
297
- { conversationId, executionId: op.execution_id }
298
- );
299
- captureError({
300
- source: "agent-stream-warning",
301
- code: "live_replay_unavailable",
302
- message: "A running response could not replay its live NDJSON; following durable lifecycle until the saved result is available.",
303
- requestId: op.request_id ?? void 0,
304
- conversationId,
305
- raw: { executionId: op.execution_id, source }
306
- });
307
- stampOperation(op.status, false, "live_replay_unavailable");
308
- }
323
+ });
324
+ } catch (err) {
325
+ const stale = ctrl.signal.aborted || !stillOwner();
326
+ clearFollower();
327
+ if (stale) return noFollow;
328
+ console.warn(
329
+ "[runtime-reconnect] operations-by-link fetch failed \u2014 falling back to legacy recovery.",
330
+ { conversationId, err }
331
+ );
332
+ if (source === "stream-loss") fallbackToLegacyRecovery();
333
+ return noFollow;
309
334
  }
310
- if (op.waiting_input) {
311
- void recoverWaitingInput();
335
+ if (superseded) {
336
+ clearFollower();
337
+ return noFollow;
312
338
  }
313
- const result = await followOperationStream({
314
- backend,
315
- executionId: op.execution_id,
316
- lastEventSeq: op.last_event_seq,
317
- signal: ctrl.signal,
318
- onEvent: (event) => {
319
- if (hasAbortController(conversationId)) {
320
- ctrl.abort();
321
- return;
322
- }
323
- if (event.kind === "waiting_input") {
324
- void recoverWaitingInput();
325
- } else if (RUNNING_KINDS.has(event.kind)) {
326
- stampOperation("running", false);
327
- } else if (event.kind === "paused") {
328
- stampOperation("paused", false);
329
- }
330
- }
331
- });
332
- if (followers.get(conversationId) === ctrl) {
333
- followers.delete(conversationId);
339
+ if (standDown) {
340
+ clearFollower();
341
+ return noFollow;
342
+ }
343
+ if (outcome.path === "rejoin" && !outcome.settled && !outcome.aborted) {
344
+ clearFollower();
345
+ return { followed: true, finalStatus: null };
346
+ }
347
+ clearFollower();
348
+ if (outcome.status === "none") {
349
+ if (source === "stream-loss") fallbackToLegacyRecovery();
350
+ return noFollow;
351
+ }
352
+ if (outcome.path === "already_settled" && !sawLive) {
353
+ if (outcome.reloadError !== void 0) throw outcome.reloadError;
354
+ return noFollow;
334
355
  }
335
- if (ctrl.signal.aborted) {
356
+ if (outcome.aborted) {
336
357
  if (followers.has(conversationId)) return noFollow;
337
358
  dispatch(patchConversation({ conversationId, serverOperation: null }));
338
359
  return noFollow;
339
360
  }
340
- const settlement = await settleRunPickup(
341
- {
342
- kind: "followed",
343
- executionId: op.execution_id,
344
- ended: result.ended,
345
- status: result.status
346
- },
347
- {
348
- reloadSavedTurn: async () => {
349
- await dispatch(loadConversation({ conversationId })).unwrap();
350
- },
351
- onStillRunning: () => {
352
- console.warn(
353
- "[runtime-reconnect] event stream unavailable after retries \u2014 falling back to legacy recovery.",
354
- { conversationId, executionId: op.execution_id }
355
- );
356
- dispatch(patchConversation({ conversationId, serverOperation: null }));
357
- fallbackToLegacyRecovery();
358
- }
359
- }
360
- );
361
- if (settlement.state !== "settled") return noFollow;
362
- if (!settlement.reloaded) {
361
+ if (!outcome.settled) {
362
+ console.warn(
363
+ "[runtime-reconnect] event stream unavailable after retries \u2014 falling back to legacy recovery.",
364
+ { conversationId, executionId: outcome.executionId }
365
+ );
366
+ dispatch(patchConversation({ conversationId, serverOperation: null }));
367
+ fallbackToLegacyRecovery();
368
+ return noFollow;
369
+ }
370
+ if (!outcome.reloaded) {
363
371
  console.warn(
364
372
  "[runtime-reconnect] operation settled but the conversation refetch failed.",
365
- { conversationId, err: settlement.error }
373
+ { conversationId, err: outcome.reloadError }
366
374
  );
367
375
  dispatch(patchConversation({ conversationId, serverOperation: null }));
368
- return { followed: true, finalStatus: result.status };
376
+ return { followed: true, finalStatus: outcome.status };
369
377
  }
370
378
  dispatch(patchConversation({ conversationId, serverOperation: null }));
371
- const finalStatus = settlement.status;
379
+ const finalStatus = outcome.status;
372
380
  if (finalStatus === "completed") {
373
381
  const rid = requestId ?? selectLatestRequestId(conversationId)(getState());
374
382
  if (rid)
@@ -386,7 +394,7 @@ const reconnectServerOperation = createAsyncThunk(
386
394
  }
387
395
  console.warn("[runtime-reconnect] server operation settled \u2014 rehydrated.", {
388
396
  conversationId,
389
- executionId: op.execution_id,
397
+ executionId: outcome.executionId,
390
398
  finalStatus
391
399
  });
392
400
  return { followed: true, finalStatus };
@@ -20,6 +20,8 @@ export interface RuntimeOperationView {
20
20
  execution_id: string;
21
21
  /** Durable runtime/chat request identity used by the resume endpoint. */
22
22
  request_id: string | null;
23
+ /** Newer by-link shape: the root run to rejoin by (preferred over `request_id`). */
24
+ root_request_id?: string | null;
23
25
  type: string;
24
26
  status: RuntimeExecutionStatus;
25
27
  is_terminal: boolean;
@@ -1,3 +1,15 @@
1
+ /**
2
+ * MCP connections client — aidream `/api/mcp-connections/*`.
3
+ *
4
+ * Phase 4 of the Unified Credential Vault cutover: MCP tokens/credentials
5
+ * live ONLY in vault items on the server. The browser never sees a token —
6
+ * discovery, invocation, refresh, credential persistence, and disconnect all
7
+ * run in aidream with vault-resolved auth. The connection row
8
+ * (tool.mcp_user_conn) is non-secret metadata the FE may read directly.
9
+ *
10
+ * (Replaces the deleted Next.js /api/mcp/servers/[serverId]/* routes and the
11
+ * deleted mcp-client/token-refresh.ts.)
12
+ */
1
13
  import type { McpToolSchema } from "./mcp-client/tool-discovery.js";
2
14
  import type { components } from "@ai-matrx/agents/generated/api-types";
3
15
  import type { AttachableAvailability } from "../connectors/attachable-resources.js";
@@ -1,4 +1,3 @@
1
- import { createClient } from "../../host/db.js";
2
1
  import { productionUrl } from "../../host/server/endpoints.js";
3
2
  import {
4
3
  buildMatrxRequestUrl,
@@ -7,22 +6,20 @@ import {
7
6
  } from "@ai-matrx/agents/matrx";
8
7
  import { applyOrganizationContextHeader } from "../../host/server/organization-context.js";
9
8
  import { ensureOrganizationForRequest } from "../../host/org.js";
9
+ import { readSessionToken } from "../../host/identity.js";
10
10
  function backendBase() {
11
11
  return productionUrl();
12
12
  }
13
13
  async function authHeaders(method, explicitOrganizationId) {
14
- const supabase = createClient();
15
- const {
16
- data: { session }
17
- } = await supabase.auth.getSession();
18
- if (!session?.access_token) throw new Error("Not signed in");
14
+ const accessToken = await readSessionToken();
15
+ if (!accessToken) throw new Error("Not signed in");
19
16
  const organizationId = await ensureOrganizationForRequest({
20
17
  method,
21
18
  organizationId: explicitOrganizationId
22
19
  });
23
20
  return applyOrganizationContextHeader(
24
21
  {
25
- Authorization: `Bearer ${session.access_token}`,
22
+ Authorization: `Bearer ${accessToken}`,
26
23
  "Content-Type": "application/json"
27
24
  },
28
25
  organizationId
@@ -137,6 +137,12 @@ export interface ActiveRequest {
137
137
  * Absent only for older servers that did not stamp a segment id.
138
138
  */
139
139
  transportStreamId?: string | null;
140
+ /**
141
+ * High-water `stream_seq` per emitter segment seen so far. A rejoin replays
142
+ * every segment from frame one; this is what drops segment A again after the
143
+ * page live-followed A then B.
144
+ */
145
+ transportSeqByStream?: Record<string, number>;
140
146
  /**
141
147
  * User-applied override for the rendered text. When non-null, this
142
148
  * supersedes the server-derived text built from `renderBlocks` for the
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  extractFirstJson
3
- } from "@ai-matrx/kit/json-extract";
4
- import { findAllFencedBlocks } from "@ai-matrx/kit/json-extract";
3
+ } from "@ai-matrx/content-ir/json-extract";
4
+ import { findAllFencedBlocks } from "@ai-matrx/content-ir/json-extract";
5
5
  function extractJsonFromText(text) {
6
6
  if (!text || typeof text !== "string") {
7
7
  return { success: false, error: "No text provided", rawText: text || "" };
@@ -7,7 +7,12 @@ import {
7
7
  createLogClientErrorDiagnostics,
8
8
  isChatSourceApp
9
9
  } from "./defaults/diagnostics.js";
10
- import { createDbIdentity } from "./defaults/identity.js";
10
+ import {
11
+ createDbIdentity,
12
+ createTokenIdentity,
13
+ createUnreadableTokenIdentity,
14
+ isTokenOnlyClient
15
+ } from "./defaults/identity.js";
11
16
  import { createStoredOrg } from "./defaults/org.js";
12
17
  import { createDefaultServer } from "./defaults/server.js";
13
18
  import { createDefaultServerApi } from "./defaults/server-api.js";
@@ -45,7 +50,8 @@ const PORTS = [
45
50
  "feedback",
46
51
  "routes",
47
52
  "canvas",
48
- "routing"
53
+ "routing",
54
+ "deviceTools"
49
55
  ];
50
56
  const ANY_ROUTING = Object.freeze({ policy: () => "any" });
51
57
  const EMPTY_REGISTRATIONS = Object.freeze({});
@@ -55,7 +61,7 @@ function assertDb(host) {
55
61
  if (host?.source) return;
56
62
  throw new ChatHostInvalidError("`db` is missing (pass `db`, or a message `source`)");
57
63
  }
58
- if (typeof db.rpc !== "function" || typeof db.from !== "function" || !db.auth) {
64
+ if (typeof db.rpc !== "function" || typeof db.from !== "function" || !isTokenOnlyClient(db) && !db.auth) {
59
65
  throw new ChatHostInvalidError(
60
66
  "`db` is not a Supabase client (no auth/rpc/from)"
61
67
  );
@@ -82,9 +88,12 @@ function resolveChatHost(host) {
82
88
  PORTS.filter((name) => host[name] !== void 0)
83
89
  );
84
90
  const ref = {};
85
- const diagnostics = host.diagnostics ?? (hasDatabase ? createLogClientErrorDiagnostics(db, { sourceApp: host.sourceApp }) : createConsoleDiagnostics());
91
+ const diagnostics = host.diagnostics ?? (hasDatabase ? createLogClientErrorDiagnostics(db, {
92
+ sourceApp: host.sourceApp,
93
+ signedIn: async () => await ref.identity.getAccessToken() !== null
94
+ }) : createConsoleDiagnostics());
86
95
  const notify = host.notify ?? createDomNotifier();
87
- const identity = host.identity ?? (hasDatabase ? createDbIdentity(db, () => ref.diagnostics) : SOURCE_ONLY_IDENTITY);
96
+ const identity = host.identity ?? (!hasDatabase ? SOURCE_ONLY_IDENTITY : host.accessToken ? createTokenIdentity(db, host.accessToken, () => ref.diagnostics) : isTokenOnlyClient(db) ? createUnreadableTokenIdentity((message) => announceOnce("identity:token-only-no-function", message)) : createDbIdentity(db, () => ref.diagnostics));
88
97
  const org = host.org ?? createStoredOrg(
89
98
  () => ref.notify,
90
99
  () => ref.diagnostics
@@ -130,6 +139,7 @@ function resolveChatHost(host) {
130
139
  routes: { ...DEFAULT_CHAT_ROUTES, ...host.routes },
131
140
  canvas: host.canvas ?? createUnhostedCanvas(() => ref.diagnostics),
132
141
  routing: host.routing ?? ANY_ROUTING,
142
+ deviceTools: host.deviceTools ?? null,
133
143
  overridden
134
144
  });
135
145
  return ref;