@voxevolv/client 0.3.15 → 0.3.17

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/dist/index.js CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  Room,
8
8
  RoomEvent,
9
9
  RpcError,
10
- Track
10
+ Track,
11
+ version as LIVEKIT_VERSION
11
12
  } from "./vendor/livekit-client.esm.mjs";
12
13
  import {
13
14
  createClientMessage,
@@ -23,8 +24,12 @@ import {
23
24
  import { ContextDeliveryError, contextRetryDelay, ReplacementContextQueue } from "./context-queue.js";
24
25
  import {
25
26
  ClientDiagnosticBuffer,
27
+ diagnosticId,
26
28
  DiagnosticDeliveryError
27
29
  } from "./diagnostics.js";
30
+ import { observeConnectionTransport } from "./connection-diagnostics.js";
31
+ import { observeRpcTransport } from "./rpc-transport-diagnostics.js";
32
+ import { Presentations } from "./presentations.js";
28
33
  import { TranscriptStore } from "./transcripts.js";
29
34
  import { TranscriptStore as TranscriptStore2 } from "./transcripts.js";
30
35
  import {
@@ -49,7 +54,7 @@ import {
49
54
  decodeContextUpdatedPayload,
50
55
  decodeRealtimeEvent
51
56
  } from "./generated/protocol-codecs.js";
52
- var SDK_VERSION = "0.3.15";
57
+ var SDK_VERSION = "0.3.17";
53
58
  var CLIENT_HELLO_TIMEOUT_MS = 3e4;
54
59
  var RECOVERY_BUFFER_STALL_MS = 5e3;
55
60
  var RECOVERY_BUFFER_POLL_MS = 500;
@@ -172,9 +177,33 @@ var RealtimeSession = class _RealtimeSession {
172
177
  diagnosticLoopTimer;
173
178
  diagnosticLoopExpectedAt = 0;
174
179
  diagnosticLoopDelayMs;
180
+ diagnosticLoopHistory = [];
181
+ diagnosticLastSampleAt = 0;
182
+ stopRpcDiagnostics;
183
+ diagnosticEngine;
184
+ connectionDiagnostics;
185
+ diagnosticRecoveryId;
186
+ presentations = new Presentations(
187
+ (request) => this.room.localParticipant.performRpc(request),
188
+ () => this.sessionEnded || ["disconnected", "failed"].includes(this.stateValue),
189
+ () => {
190
+ this.recordDiagnostic({ operation: "tool_rpc", status: "failed", reason: "presentation_receipt_failed" });
191
+ this.flushDiagnostics("presentation_receipt_failed");
192
+ }
193
+ );
175
194
  diagnosticReportTimeoutMs = DIAGNOSTIC_REPORT_TIMEOUT_MS;
176
195
  onDiagnosticVisibilityChange = () => {
177
- if (globalThis.document?.visibilityState === "hidden") this.flushDiagnostics("page_hidden");
196
+ const reason = globalThis.document?.visibilityState === "hidden" ? "page_hidden" : "page_visible";
197
+ this.recordDiagnostic({ operation: "browser_runtime", status: "succeeded", reason });
198
+ this.flushDiagnostics(reason);
199
+ };
200
+ onDiagnosticNetworkChange = (event) => {
201
+ this.recordDiagnostic({ operation: "browser_runtime", status: "succeeded", reason: `network_${event.type}` });
202
+ this.flushDiagnostics(`network_${event.type}`);
203
+ };
204
+ onDiagnosticLifecycle = (event) => {
205
+ this.recordDiagnostic({ operation: "browser_runtime", status: "succeeded", reason: event.type });
206
+ this.flushDiagnostics(`page_${event.type}`);
178
207
  };
179
208
  constructor(credentials, options = {}) {
180
209
  validateSessionCredentials(credentials);
@@ -194,6 +223,9 @@ var RealtimeSession = class _RealtimeSession {
194
223
  this.startupRetryOptions = resolveStartupRetryOptions(options.startupRetry);
195
224
  this.lifecycleCredentials = options.lifecycleCredentials ?? "same-origin";
196
225
  this.room = new Room({ adaptiveStream: true, dynacast: true });
226
+ this.room.voxevolvObserveEngine = () => {
227
+ this.startRpcDiagnostics();
228
+ };
197
229
  for (const [name, handler] of Object.entries(options.tools ?? {})) {
198
230
  this.registerTool(name, handler);
199
231
  }
@@ -213,6 +245,7 @@ var RealtimeSession = class _RealtimeSession {
213
245
  this.flushDiagnostics("tool_rpc_worker_request");
214
246
  return "{}";
215
247
  });
248
+ this.startRpcDiagnostics();
216
249
  this.bindRoomEvents();
217
250
  }
218
251
  async invokeBrowserTool(invocation) {
@@ -248,17 +281,29 @@ var RealtimeSession = class _RealtimeSession {
248
281
  throw new RpcError(1500, "Tool invocation arguments must be an object");
249
282
  }
250
283
  const arguments_ = payload.arguments;
251
- this.notifyListeners(this.toolCallListeners, { name: toolName, callId, arguments: arguments_ });
252
- record({ stage: "handler_started" });
253
- this.flushDiagnostics("tool_rpc_received");
254
- slowTimer = globalThis.setTimeout(() => {
255
- record({ stage: "handler_started", reason: "handler_slow" });
256
- this.flushDiagnostics("tool_rpc_slow");
257
- }, 2e3);
284
+ const context = { callId };
285
+ const toolCall = { name: toolName, callId, arguments: arguments_ };
286
+ const execute = async () => {
287
+ this.notifyListeners(this.toolCallListeners, toolCall);
288
+ record({ stage: "handler_started" });
289
+ this.flushDiagnostics("tool_rpc_received");
290
+ slowTimer = globalThis.setTimeout(() => {
291
+ record({ stage: "handler_started", reason: "handler_slow" });
292
+ this.flushDiagnostics("tool_rpc_slow");
293
+ }, 2e3);
294
+ const result = await handler(arguments_, context);
295
+ return JSON.stringify(result ?? null);
296
+ };
258
297
  let response;
259
298
  try {
260
- const result = await handler(arguments_, { callId });
261
- response = JSON.stringify(result ?? null);
299
+ if (payload.presentation_id !== void 0) {
300
+ if (toolName !== "present_attachment" || typeof payload.presentation_id !== "string" || !payload.presentation_id || payload.presentation_id.length > 200) {
301
+ throw new RpcError(1500, "Invalid presentation identity");
302
+ }
303
+ response = await this.presentations.invoke(payload.presentation_id, invocation.callerIdentity, execute);
304
+ } else {
305
+ response = await execute();
306
+ }
262
307
  } catch (error) {
263
308
  const message = error instanceof Error ? error.message : "Browser tool handler failed";
264
309
  throw new RpcError(1500, message, void 0, { cause: error });
@@ -671,6 +716,15 @@ var RealtimeSession = class _RealtimeSession {
671
716
  } catch (error) {
672
717
  if (signal?.aborted) this.rejectPendingCommands(toError(abortError(signal)));
673
718
  this.disconnectMode = recovering ? "rejoin" : "connect_failure";
719
+ if (!recovering) {
720
+ this.livekitResumeController?.abort(toError(error));
721
+ this.livekitResumeController = null;
722
+ this.failRecovery(toError(error));
723
+ this.recoveryStartedAt = null;
724
+ this.diagnosticRecoveryId = void 0;
725
+ this.reconnectStartedAt = null;
726
+ this.pendingReconnect = null;
727
+ }
674
728
  try {
675
729
  await this.room.disconnect();
676
730
  } catch {
@@ -1072,15 +1126,32 @@ var RealtimeSession = class _RealtimeSession {
1072
1126
  setState(state) {
1073
1127
  if (this.stateValue === state) return;
1074
1128
  this.stateValue = state;
1129
+ this.recordDiagnostic({ operation: "browser_runtime", status: "succeeded", reason: `state_${state}` });
1075
1130
  if (state === "connecting" || state === "resuming") {
1131
+ this.startRpcDiagnostics();
1076
1132
  this.startDiagnosticLoopMonitor();
1133
+ globalThis.addEventListener?.("online", this.onDiagnosticNetworkChange);
1134
+ globalThis.addEventListener?.("offline", this.onDiagnosticNetworkChange);
1077
1135
  globalThis.document?.addEventListener?.("visibilitychange", this.onDiagnosticVisibilityChange);
1136
+ globalThis.document?.addEventListener?.("freeze", this.onDiagnosticLifecycle);
1137
+ globalThis.document?.addEventListener?.("resume", this.onDiagnosticLifecycle);
1078
1138
  }
1079
1139
  if (state === "disconnected" || state === "failed") {
1080
1140
  globalThis.clearInterval(this.diagnosticLoopTimer);
1081
1141
  this.diagnosticLoopTimer = void 0;
1082
1142
  this.diagnosticLoopDelayMs = void 0;
1143
+ this.diagnosticLoopHistory = [];
1144
+ this.connectionDiagnostics?.stop();
1145
+ this.connectionDiagnostics = void 0;
1146
+ this.diagnosticRecoveryId = void 0;
1147
+ globalThis.removeEventListener?.("online", this.onDiagnosticNetworkChange);
1148
+ globalThis.removeEventListener?.("offline", this.onDiagnosticNetworkChange);
1149
+ this.stopRpcDiagnostics?.();
1150
+ this.stopRpcDiagnostics = void 0;
1151
+ this.diagnosticEngine = void 0;
1083
1152
  globalThis.document?.removeEventListener?.("visibilitychange", this.onDiagnosticVisibilityChange);
1153
+ globalThis.document?.removeEventListener?.("freeze", this.onDiagnosticLifecycle);
1154
+ globalThis.document?.removeEventListener?.("resume", this.onDiagnosticLifecycle);
1084
1155
  this.flushDiagnostics("session_finished");
1085
1156
  const error = new Error(`Session ${state}`);
1086
1157
  this.contextGeneration += 1;
@@ -1114,12 +1185,23 @@ var RealtimeSession = class _RealtimeSession {
1114
1185
  });
1115
1186
  }
1116
1187
  });
1117
- this.room.on(RoomEvent.SignalReconnecting, () => this.handleLiveKitReconnecting());
1118
- this.room.on(RoomEvent.Reconnecting, () => this.handleLiveKitReconnecting());
1188
+ this.room.on(RoomEvent.SignalConnected, () => this.startRpcDiagnostics());
1189
+ this.room.on(RoomEvent.SignalReconnecting, () => this.handleLiveKitReconnecting("signal_reconnecting"));
1190
+ this.room.on(RoomEvent.Reconnecting, () => this.handleLiveKitReconnecting("media_reconnecting"));
1119
1191
  this.room.on(RoomEvent.Reconnected, () => {
1192
+ this.startRpcDiagnostics();
1120
1193
  void this.handleLiveKitReconnected();
1121
1194
  });
1122
1195
  this.room.on(RoomEvent.Disconnected, (reason) => {
1196
+ if (!this.disconnectMode && !this.sessionEnded) {
1197
+ this.recordDiagnostic({
1198
+ operation: "connection_transport",
1199
+ status: "failed",
1200
+ reason: "room_disconnected",
1201
+ disconnect_reason: reason
1202
+ });
1203
+ this.flushDiagnostics("room_disconnected");
1204
+ }
1123
1205
  this.handleLiveKitDisconnected(reason);
1124
1206
  });
1125
1207
  this.room.on(RoomEvent.ConnectionStateChanged, (state) => {
@@ -1199,8 +1281,9 @@ var RealtimeSession = class _RealtimeSession {
1199
1281
  this.notifyListeners(this.listeners, value);
1200
1282
  });
1201
1283
  }
1202
- handleLiveKitReconnecting() {
1284
+ handleLiveKitReconnecting(trigger = "livekit_reconnecting") {
1203
1285
  if (this.sessionEnded || this.disconnectMode === "intentional" || this.terminalReplayController || ["disconnected", "failed"].includes(this.stateValue)) return;
1286
+ this.recordDiagnostic({ operation: "connection_transport", status: "started", reason: trigger });
1204
1287
  this.agentReady = false;
1205
1288
  if (this.autoRejoinPromise !== null || this.activeRecovery?.strategy === "rejoin" || this.disconnectMode === "rejoin") {
1206
1289
  this.setState("resuming");
@@ -1380,6 +1463,7 @@ var RealtimeSession = class _RealtimeSession {
1380
1463
  this.notifyListeners(this.endedListeners, event);
1381
1464
  }
1382
1465
  beginRecovery(strategy, attempt, details) {
1466
+ this.diagnosticRecoveryId ??= diagnosticId();
1383
1467
  this.recoveryStartedAt ??= Date.now();
1384
1468
  this.activeRecovery = { strategy, attempt, details };
1385
1469
  this.recordDiagnostic({
@@ -1412,6 +1496,7 @@ var RealtimeSession = class _RealtimeSession {
1412
1496
  });
1413
1497
  this.flushDiagnostics("recovery_completed");
1414
1498
  this.recoveryStartedAt = null;
1499
+ this.diagnosticRecoveryId = void 0;
1415
1500
  this.activeRecovery = null;
1416
1501
  this.notifyListeners(this.recoveryListeners, {
1417
1502
  status: "succeeded",
@@ -1519,6 +1604,7 @@ var RealtimeSession = class _RealtimeSession {
1519
1604
  this.livekitResumeController?.abort();
1520
1605
  this.livekitResumeController = null;
1521
1606
  this.recoveryStartedAt = null;
1607
+ this.diagnosticRecoveryId = void 0;
1522
1608
  this.autoRejoinController?.abort();
1523
1609
  this.autoRejoinController = null;
1524
1610
  this.manualRejoinController?.abort();
@@ -1967,30 +2053,68 @@ var RealtimeSession = class _RealtimeSession {
1967
2053
  options
1968
2054
  );
1969
2055
  }
2056
+ startRpcDiagnostics() {
2057
+ const engine = this.room.engine;
2058
+ if (!engine || this.stopRpcDiagnostics && this.diagnosticEngine === engine) return;
2059
+ this.stopRpcDiagnostics?.();
2060
+ this.diagnosticEngine = engine;
2061
+ this.connectionDiagnostics?.stop();
2062
+ this.connectionDiagnostics = observeConnectionTransport(engine, (entry) => {
2063
+ if (this.disconnectMode || this.sessionEnded || ["disconnected", "failed"].includes(this.stateValue)) return;
2064
+ this.recordDiagnostic(entry);
2065
+ this.flushDiagnostics("connection_transport");
2066
+ });
2067
+ this.stopRpcDiagnostics = observeRpcTransport(engine, (entry) => {
2068
+ this.recordDiagnostic(entry);
2069
+ this.flushDiagnostics("tool_rpc_transport");
2070
+ });
2071
+ }
1970
2072
  startDiagnosticLoopMonitor() {
1971
2073
  if (this.diagnosticLoopTimer !== void 0) return;
1972
2074
  this.diagnosticLoopExpectedAt = performance.now() + 1e3;
1973
- this.diagnosticLoopTimer = globalThis.setInterval(() => {
1974
- const now = performance.now();
1975
- this.diagnosticLoopDelayMs = Math.max(0, now - this.diagnosticLoopExpectedAt);
1976
- this.diagnosticLoopExpectedAt = now + 1e3;
1977
- }, 1e3);
2075
+ this.diagnosticLastSampleAt = performance.now();
2076
+ this.diagnosticLoopTimer = globalThis.setInterval(() => this.sampleDiagnosticLoop(performance.now()), 1e3);
1978
2077
  this.diagnosticLoopTimer.unref?.();
1979
2078
  }
2079
+ sampleDiagnosticLoop(now) {
2080
+ const delay = Math.max(0, now - this.diagnosticLoopExpectedAt);
2081
+ this.diagnosticLoopDelayMs = delay;
2082
+ this.diagnosticLoopExpectedAt = now + 1e3;
2083
+ this.diagnosticLoopHistory.push({ at: now, delay });
2084
+ this.diagnosticLoopHistory = this.diagnosticLoopHistory.filter((sample) => now - sample.at < 3e4).slice(-30);
2085
+ if (delay >= 100 || now - this.diagnosticLastSampleAt >= 1e4) {
2086
+ this.diagnosticLastSampleAt = now;
2087
+ this.recordDiagnostic({ operation: "browser_runtime", status: "succeeded", reason: "timer_sample" });
2088
+ if (delay >= 1e3) this.flushDiagnostics("browser_stall");
2089
+ }
2090
+ }
1980
2091
  recordDiagnostic(entry) {
2092
+ if (entry.operation === "connection_transport") {
2093
+ this.diagnosticRecoveryId ??= diagnosticId();
2094
+ }
1981
2095
  const snapshot = {
1982
2096
  ...entry,
2097
+ recovery_id: this.diagnosticRecoveryId,
2098
+ connection_id: entry.connection_id ?? this.connectionId,
2099
+ participant_sid: this.room.localParticipant.sid || void 0,
2100
+ livekit_version: LIVEKIT_VERSION,
2101
+ browser_online: globalThis.navigator?.onLine,
1983
2102
  connection_state: this.stateValue,
1984
- buffered_bytes: this.reliableDataChannelBufferedBytes()
2103
+ buffered_bytes: entry.buffered_bytes ?? this.reliableDataChannelBufferedBytes()
1985
2104
  };
1986
- if (entry.operation === "tool_rpc" || entry.operation === "connection_recovery") {
2105
+ if (["connection_transport", "connection_recovery"].includes(entry.operation)) {
2106
+ snapshot.transport_history ??= this.connectionDiagnostics?.snapshotHistory();
2107
+ }
2108
+ if (["tool_rpc", "rpc_transport", "browser_runtime", "connection_recovery", "connection_transport"].includes(entry.operation)) {
1987
2109
  snapshot.page_visibility = globalThis.document?.visibilityState;
1988
2110
  snapshot.active_context_transfers = this.activeContextTransfers;
1989
2111
  snapshot.active_context_bytes = this.activeContextBytes;
1990
2112
  if (this.diagnosticLoopTimer !== void 0) {
1991
- snapshot.event_loop_delay_ms = Math.max(
1992
- this.diagnosticLoopDelayMs ?? 0,
1993
- performance.now() - this.diagnosticLoopExpectedAt
2113
+ const now = performance.now();
2114
+ snapshot.event_loop_delay_ms = Math.max(this.diagnosticLoopDelayMs ?? 0, now - this.diagnosticLoopExpectedAt);
2115
+ snapshot.event_loop_delay_max_ms = Math.max(
2116
+ snapshot.event_loop_delay_ms,
2117
+ ...this.diagnosticLoopHistory.filter((sample) => now - sample.at < 3e4).map((sample) => sample.delay)
1994
2118
  );
1995
2119
  }
1996
2120
  }