@voxevolv/client 0.3.3 → 0.3.4

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
@@ -1,12 +1,14 @@
1
1
  import { ConnectionState as LiveKitConnectionState, DataPacket_Kind, DisconnectReason, Room, RoomEvent, RpcError, Track, } from "livekit-client";
2
2
  import { createClientMessage, CommandError, EventCursor, isCommandAckPayload, isCommandErrorPayload, isContextRejectedPayload, isContextUpdatePayload, isContextUpdatedPayload, isRealtimeEvent, } from "./protocol.js";
3
3
  import { ReplacementContextQueue } from "./context-queue.js";
4
+ import { ClientDiagnosticBuffer, } from "./diagnostics.js";
4
5
  import { TranscriptStore } from "./transcripts.js";
5
6
  export { TranscriptStore } from "./transcripts.js";
6
7
  export { CommandError, EventCursor, createClientMessage, isCommandAckPayload, isCommandErrorPayload, isContextRejectedPayload, isContextUpdatePayload, isContextUpdatedPayload, isRealtimeEvent, } from "./protocol.js";
7
8
  export { decodeAgentSnapshotPayload, decodeClientHelloPayload, decodeClientMessage, decodeCommandAckPayload, decodeCommandErrorPayload, decodeContextRejectedPayload, decodeContextUpdatePayload, decodeContextUpdatedPayload, decodeRealtimeEvent, } from "./generated/protocol-codecs.js";
8
- export const SDK_VERSION = "0.3.3";
9
+ export const SDK_VERSION = "0.3.4";
9
10
  const CLIENT_HELLO_TIMEOUT_MS = 30_000;
11
+ const DIAGNOSTIC_REPORT_TIMEOUT_MS = 5_000;
10
12
  export class RealtimeSessionHttpError extends Error {
11
13
  status;
12
14
  code;
@@ -78,6 +80,8 @@ export class RealtimeSession {
78
80
  refreshCredentialsCallback;
79
81
  autoRejoinOptions;
80
82
  lifecycleCredentials;
83
+ diagnostics = new ClientDiagnosticBuffer({ sdkVersion: SDK_VERSION });
84
+ diagnosticReportTimeoutMs = DIAGNOSTIC_REPORT_TIMEOUT_MS;
81
85
  constructor(credentials, options = {}) {
82
86
  validateSessionCredentials(credentials);
83
87
  this.id = credentials.id;
@@ -384,7 +388,7 @@ export class RealtimeSession {
384
388
  const normalizedApiBase = normalizeApiBase(apiBase);
385
389
  const recovering = this.hasConnectedValue;
386
390
  if (recovering) {
387
- this.beginRecovery("rejoin", 1);
391
+ this.beginRecovery("rejoin", 1, { trigger: "manual_rejoin" });
388
392
  this.setState("resuming");
389
393
  }
390
394
  try {
@@ -457,18 +461,73 @@ export class RealtimeSession {
457
461
  }
458
462
  async sendContextUpdate(update, timeoutMs) {
459
463
  const generation = this.contextGeneration;
464
+ const payloadBytes = contextUpdateBytes(update);
465
+ const transferStartedAt = Date.now();
466
+ let transferTimedOut = false;
467
+ this.recordDiagnostic({
468
+ operation: "context_transfer",
469
+ status: "started",
470
+ context_key: update.key,
471
+ payload_bytes: payloadBytes,
472
+ });
460
473
  let transferTimeout;
461
474
  const transferExpired = new Promise((_resolve, reject) => {
462
475
  transferTimeout = globalThis.setTimeout(() => {
476
+ transferTimedOut = true;
463
477
  const error = new Error(`Context transfer timed out after ${timeoutMs}ms`);
464
- this.startAutoRejoin(error);
478
+ this.recordDiagnostic({
479
+ operation: "context_transfer",
480
+ status: "timed_out",
481
+ reason: "context_transfer_timeout",
482
+ context_key: update.key,
483
+ payload_bytes: payloadBytes,
484
+ duration_ms: Date.now() - transferStartedAt,
485
+ timeout_ms: timeoutMs,
486
+ });
487
+ this.flushDiagnostics("context_transfer_timeout");
488
+ this.startAutoRejoin(error, {
489
+ trigger: "context_transfer_timeout",
490
+ operation: "context_transfer",
491
+ contextKey: update.key,
492
+ timeoutMs,
493
+ });
465
494
  reject(error);
466
495
  }, timeoutMs);
467
496
  });
468
- const transfer = await Promise.race([
469
- "image" in update ? this.uploadImageContext(update) : this.uploadTextContext(update),
470
- transferExpired,
471
- ]).finally(() => globalThis.clearTimeout(transferTimeout));
497
+ let transfer;
498
+ try {
499
+ transfer = await Promise.race([
500
+ "image" in update ? this.uploadImageContext(update) : this.uploadTextContext(update),
501
+ transferExpired,
502
+ ]);
503
+ }
504
+ catch (error) {
505
+ if (!transferTimedOut) {
506
+ this.recordDiagnostic({
507
+ operation: "context_transfer",
508
+ status: "failed",
509
+ reason: "context_transfer_failed",
510
+ context_key: update.key,
511
+ payload_bytes: payloadBytes,
512
+ duration_ms: Date.now() - transferStartedAt,
513
+ });
514
+ this.flushDiagnostics("command_failed");
515
+ }
516
+ throw error;
517
+ }
518
+ finally {
519
+ globalThis.clearTimeout(transferTimeout);
520
+ }
521
+ const transferDurationMs = Date.now() - transferStartedAt;
522
+ this.recordDiagnostic({
523
+ operation: "context_transfer",
524
+ status: "succeeded",
525
+ context_key: update.key,
526
+ payload_bytes: payloadBytes,
527
+ duration_ms: transferDurationMs,
528
+ });
529
+ if (transferDurationMs > 2_000)
530
+ this.flushDiagnostics("slow_operation");
472
531
  if (generation !== this.contextGeneration)
473
532
  throw new Error("Context transfer interrupted by recovery");
474
533
  const { payload } = transfer;
@@ -623,7 +682,7 @@ export class RealtimeSession {
623
682
  bindRoomEvents() {
624
683
  this.room.on(RoomEvent.ParticipantDisconnected, (participant) => {
625
684
  if (participant.isAgent && !this.disconnectMode) {
626
- this.startAutoRejoin(new Error("Agent disconnected from the room"));
685
+ this.startAutoRejoin(new Error("Agent disconnected from the room"), { trigger: "agent_disconnected" });
627
686
  }
628
687
  });
629
688
  this.room.on(RoomEvent.SignalReconnecting, () => this.handleLiveKitReconnecting());
@@ -732,7 +791,7 @@ export class RealtimeSession {
732
791
  this.reconnectStartedAt ??= Date.now();
733
792
  this.rejectPendingCommands(new Error("Connection interrupted before command completion"), true);
734
793
  if (this.activeRecovery === null) {
735
- this.beginRecovery("livekit", 1);
794
+ this.beginRecovery("livekit", 1, { trigger: "livekit_reconnecting" });
736
795
  }
737
796
  this.setState("reconnecting");
738
797
  }
@@ -750,7 +809,7 @@ export class RealtimeSession {
750
809
  this.reconnectStartedAt = null;
751
810
  }
752
811
  if (this.hasConnectedValue && this.activeRecovery === null) {
753
- this.beginRecovery("livekit", 1);
812
+ this.beginRecovery("livekit", 1, { trigger: "livekit_reconnecting" });
754
813
  }
755
814
  this.connectionId = `conn_${Math.random().toString(36).slice(2)}_${Date.now().toString(36)}`;
756
815
  this.clientSeq = 0;
@@ -774,7 +833,7 @@ export class RealtimeSession {
774
833
  return;
775
834
  const normalizedError = toError(error);
776
835
  if (this.shouldAutoRejoin()) {
777
- this.startAutoRejoin(normalizedError);
836
+ this.startAutoRejoin(normalizedError, { trigger: "livekit_resume_failed" });
778
837
  }
779
838
  else {
780
839
  this.failRecovery(normalizedError);
@@ -804,7 +863,7 @@ export class RealtimeSession {
804
863
  return;
805
864
  }
806
865
  if (this.shouldAutoRejoin(reason)) {
807
- this.startAutoRejoin(new Error(`LiveKit disconnected (${disconnectReasonName(reason)})`));
866
+ this.startAutoRejoin(new Error(`LiveKit disconnected (${disconnectReasonName(reason)})`), { trigger: "livekit_disconnected" });
808
867
  return;
809
868
  }
810
869
  if (this.activeRecovery !== null) {
@@ -828,12 +887,22 @@ export class RealtimeSession {
828
887
  this.endNotificationDelivered = true;
829
888
  this.notifyListeners(this.endedListeners, event);
830
889
  }
831
- beginRecovery(strategy, attempt) {
832
- this.activeRecovery = { strategy, attempt };
890
+ beginRecovery(strategy, attempt, details) {
891
+ this.activeRecovery = { strategy, attempt, details };
892
+ this.recordDiagnostic({
893
+ operation: "connection_recovery",
894
+ status: "started",
895
+ reason: details.trigger,
896
+ message_id: details.messageId,
897
+ context_key: details.contextKey,
898
+ timeout_ms: details.timeoutMs,
899
+ });
900
+ this.flushDiagnostics("recovery_started");
833
901
  this.notifyListeners(this.recoveryListeners, {
834
902
  status: "started",
835
903
  strategy,
836
904
  attempt,
905
+ ...details,
837
906
  });
838
907
  }
839
908
  completeRecovery() {
@@ -845,6 +914,7 @@ export class RealtimeSession {
845
914
  status: "succeeded",
846
915
  strategy: recovery.strategy,
847
916
  attempt: recovery.attempt,
917
+ ...recovery.details,
848
918
  });
849
919
  }
850
920
  failRecovery(error) {
@@ -856,6 +926,7 @@ export class RealtimeSession {
856
926
  status: "failed",
857
927
  strategy: recovery.strategy,
858
928
  attempt: recovery.attempt,
929
+ ...recovery.details,
859
930
  error,
860
931
  });
861
932
  }
@@ -866,7 +937,7 @@ export class RealtimeSession {
866
937
  && !["disconnected", "failed"].includes(this.stateValue)
867
938
  && !isTerminalDisconnectReason(reason));
868
939
  }
869
- startAutoRejoin(initialError) {
940
+ startAutoRejoin(initialError, details) {
870
941
  if (this.autoRejoinPromise || !this.shouldAutoRejoin())
871
942
  return;
872
943
  this.agentReady = false;
@@ -887,7 +958,7 @@ export class RealtimeSession {
887
958
  this.failRecovery(error);
888
959
  this.setState("failed");
889
960
  }, this.autoRejoinOptions.timeoutMs);
890
- const promise = this.runAutoRejoin(initialError, controller.signal)
961
+ const promise = this.runAutoRejoin(initialError, controller.signal, details)
891
962
  .finally(() => {
892
963
  globalThis.clearTimeout(timeout);
893
964
  if (this.autoRejoinController === controller) {
@@ -898,7 +969,7 @@ export class RealtimeSession {
898
969
  this.autoRejoinPromise = promise;
899
970
  void promise;
900
971
  }
901
- async runAutoRejoin(initialError, signal) {
972
+ async runAutoRejoin(initialError, signal, details) {
902
973
  const options = this.autoRejoinOptions;
903
974
  if (!options)
904
975
  return;
@@ -916,7 +987,7 @@ export class RealtimeSession {
916
987
  await abortableDelay(delayMs + Math.random() * Math.min(delayMs, 500), signal);
917
988
  }
918
989
  throwIfAborted(signal);
919
- this.beginRecovery("rejoin", attempt);
990
+ this.beginRecovery("rejoin", attempt, details);
920
991
  await this.performRejoin(this.apiBase, signal);
921
992
  throwIfAborted(signal);
922
993
  this.reconnectStartedAt = null;
@@ -1070,6 +1141,21 @@ export class RealtimeSession {
1070
1141
  this.pendingReconnect = null;
1071
1142
  }
1072
1143
  async sendTrackedCommand(message, timeoutMs, retryTransfer) {
1144
+ const contextKey = contextKeyFromMessage(message);
1145
+ const payloadBytes = contextPayloadBytes(message.payload);
1146
+ const startedAt = Date.now();
1147
+ this.recordDiagnostic({
1148
+ operation: "command_send",
1149
+ status: "started",
1150
+ message_id: message.message_id,
1151
+ connection_id: message.connection_id,
1152
+ client_seq: message.client_seq,
1153
+ context_key: contextKey,
1154
+ payload_bytes: payloadBytes,
1155
+ retry_count: 0,
1156
+ timeout_ms: timeoutMs,
1157
+ event_type: diagnosticEventType(message.type),
1158
+ });
1073
1159
  const response = new Promise((resolve, reject) => {
1074
1160
  const timeout = globalThis.setTimeout(() => {
1075
1161
  const pending = this.pendingCommands.get(message.message_id);
@@ -1077,17 +1163,46 @@ export class RealtimeSession {
1077
1163
  globalThis.clearInterval(pending.retry);
1078
1164
  this.pendingCommands.delete(message.message_id);
1079
1165
  const error = new Error(`Command ${message.type} timed out after ${timeoutMs}ms`);
1166
+ this.recordDiagnostic({
1167
+ operation: "command_send",
1168
+ status: "timed_out",
1169
+ reason: message.type === "context.update"
1170
+ ? "context_command_timeout"
1171
+ : "command_timeout",
1172
+ message_id: message.message_id,
1173
+ connection_id: message.connection_id,
1174
+ client_seq: message.client_seq,
1175
+ context_key: contextKey,
1176
+ payload_bytes: payloadBytes,
1177
+ duration_ms: Date.now() - startedAt,
1178
+ retry_count: pending?.retryCount ?? 0,
1179
+ timeout_ms: timeoutMs,
1180
+ event_type: diagnosticEventType(message.type),
1181
+ });
1182
+ this.flushDiagnostics("command_timeout");
1080
1183
  reject(error);
1081
- if (message.type === "context.update")
1082
- this.startAutoRejoin(error);
1184
+ if (message.type === "context.update") {
1185
+ this.startAutoRejoin(error, {
1186
+ trigger: "context_command_timeout",
1187
+ operation: "command_send",
1188
+ messageId: message.message_id,
1189
+ contextKey: contextKeyFromMessage(message),
1190
+ timeoutMs,
1191
+ });
1192
+ }
1083
1193
  }, timeoutMs);
1084
1194
  const retry = globalThis.setInterval(() => {
1085
- void this.publishCommand(message).catch(() => {
1195
+ void this.publishCommandAttempt(message, true).catch(() => {
1086
1196
  // A reconnect may temporarily reject writes; retry until timeout.
1087
1197
  });
1088
1198
  }, 500);
1089
1199
  this.pendingCommands.set(message.message_id, {
1090
1200
  commandType: message.type,
1201
+ message,
1202
+ startedAt,
1203
+ contextKey,
1204
+ payloadBytes,
1205
+ retryCount: 0,
1091
1206
  resolve,
1092
1207
  reject,
1093
1208
  timeout,
@@ -1095,14 +1210,14 @@ export class RealtimeSession {
1095
1210
  retryTransfer: retryTransfer
1096
1211
  ? async () => {
1097
1212
  await retryTransfer();
1098
- await this.publishCommand(message);
1213
+ await this.publishCommandAttempt(message, true);
1099
1214
  }
1100
1215
  : undefined,
1101
1216
  transferRetrying: false,
1102
1217
  });
1103
1218
  });
1104
1219
  try {
1105
- await this.publishCommand(message);
1220
+ await this.publishCommandAttempt(message, false);
1106
1221
  }
1107
1222
  catch {
1108
1223
  // The reliable data channel may still be warming immediately after a
@@ -1111,9 +1226,114 @@ export class RealtimeSession {
1111
1226
  }
1112
1227
  return response;
1113
1228
  }
1229
+ async publishCommandAttempt(message, retrying) {
1230
+ const pending = this.pendingCommands.get(message.message_id);
1231
+ if (!pending)
1232
+ return;
1233
+ if (retrying) {
1234
+ pending.retryCount += 1;
1235
+ this.recordDiagnostic({
1236
+ operation: "command_send",
1237
+ status: "retrying",
1238
+ message_id: message.message_id,
1239
+ connection_id: message.connection_id,
1240
+ client_seq: message.client_seq,
1241
+ context_key: pending.contextKey,
1242
+ payload_bytes: pending.payloadBytes,
1243
+ retry_count: pending.retryCount,
1244
+ event_type: diagnosticEventType(message.type),
1245
+ });
1246
+ }
1247
+ const attemptStartedAt = Date.now();
1248
+ try {
1249
+ await this.publishCommand(message);
1250
+ }
1251
+ catch (error) {
1252
+ this.recordDiagnostic({
1253
+ operation: "command_send",
1254
+ status: "failed",
1255
+ reason: "command_publish_failed",
1256
+ message_id: message.message_id,
1257
+ connection_id: message.connection_id,
1258
+ client_seq: message.client_seq,
1259
+ context_key: pending.contextKey,
1260
+ payload_bytes: pending.payloadBytes,
1261
+ duration_ms: Date.now() - attemptStartedAt,
1262
+ retry_count: pending.retryCount,
1263
+ event_type: diagnosticEventType(message.type),
1264
+ });
1265
+ this.flushDiagnostics("command_failed");
1266
+ throw error;
1267
+ }
1268
+ const durationMs = Date.now() - attemptStartedAt;
1269
+ this.recordDiagnostic({
1270
+ operation: "command_send",
1271
+ status: "succeeded",
1272
+ message_id: message.message_id,
1273
+ connection_id: message.connection_id,
1274
+ client_seq: message.client_seq,
1275
+ context_key: pending.contextKey,
1276
+ payload_bytes: pending.payloadBytes,
1277
+ duration_ms: durationMs,
1278
+ retry_count: pending.retryCount,
1279
+ event_type: diagnosticEventType(message.type),
1280
+ });
1281
+ if (durationMs > 2_000)
1282
+ this.flushDiagnostics("slow_operation");
1283
+ }
1114
1284
  async publishCommand(message) {
1115
1285
  await this.room.localParticipant.publishData(new TextEncoder().encode(JSON.stringify(message)), { reliable: true, topic: "voxevolv.commands" });
1116
1286
  }
1287
+ recordDiagnostic(entry) {
1288
+ this.diagnostics.record({
1289
+ ...entry,
1290
+ connection_state: this.stateValue,
1291
+ buffered_bytes: this.reliableDataChannelBufferedBytes(),
1292
+ });
1293
+ }
1294
+ reliableDataChannelBufferedBytes() {
1295
+ try {
1296
+ const bufferedAmount = this.room.engine.dataChannelForKind(0)?.bufferedAmount;
1297
+ return typeof bufferedAmount === "number" ? bufferedAmount : undefined;
1298
+ }
1299
+ catch {
1300
+ return undefined;
1301
+ }
1302
+ }
1303
+ flushDiagnostics(trigger) {
1304
+ void this.diagnostics.flush(trigger, (report) => this.postDiagnostics(report));
1305
+ }
1306
+ async postDiagnostics(report) {
1307
+ const controller = new AbortController();
1308
+ let timeout;
1309
+ const timeoutExpired = new Promise((_resolve, reject) => {
1310
+ timeout = globalThis.setTimeout(() => {
1311
+ controller.abort();
1312
+ reject(new Error(`Diagnostic report timed out after ${this.diagnosticReportTimeoutMs}ms`));
1313
+ }, this.diagnosticReportTimeoutMs);
1314
+ });
1315
+ try {
1316
+ const response = await Promise.race([
1317
+ this.fetcher(`${this.apiBase}/sessions/${this.id}/diagnostics`, {
1318
+ method: "POST",
1319
+ headers: {
1320
+ Authorization: `Bearer ${this.accessToken}`,
1321
+ "Content-Type": "application/json",
1322
+ },
1323
+ credentials: this.lifecycleCredentials,
1324
+ body: JSON.stringify(report),
1325
+ signal: controller.signal,
1326
+ }),
1327
+ timeoutExpired,
1328
+ ]);
1329
+ if (!response.ok)
1330
+ throw new Error(`Diagnostic report failed (${response.status})`);
1331
+ }
1332
+ finally {
1333
+ if (timeout !== undefined)
1334
+ globalThis.clearTimeout(timeout);
1335
+ }
1336
+ }
1117
1337
  waitForRemoteAudioTrack(deadline, signal) {
1118
1338
  if ((this.outputMediaStream?.getAudioTracks().length ?? 0) > 0)
1119
1339
  return Promise.resolve();
@@ -1177,7 +1397,7 @@ export class RealtimeSession {
1177
1397
  if (event.type !== "agent.ready" && !snapshotReady)
1178
1398
  return;
1179
1399
  if (this.agentStreamId && this.agentStreamId !== event.stream_id && !this.activeRecovery) {
1180
- this.startAutoRejoin(new Error("Agent worker was replaced"));
1400
+ this.startAutoRejoin(new Error("Agent worker was replaced"), { trigger: "worker_replaced" });
1181
1401
  return;
1182
1402
  }
1183
1403
  this.agentStreamId = event.stream_id;
@@ -1199,6 +1419,7 @@ export class RealtimeSession {
1199
1419
  if (!pending)
1200
1420
  return;
1201
1421
  if (event.type === "command.ack" && isCommandAckPayload(event.payload)) {
1422
+ this.recordCommandResult(pending, event.type, "acknowledged");
1202
1423
  globalThis.clearTimeout(pending.timeout);
1203
1424
  globalThis.clearInterval(pending.retry);
1204
1425
  this.pendingCommands.delete(event.payload.message_id);
@@ -1206,6 +1427,7 @@ export class RealtimeSession {
1206
1427
  return;
1207
1428
  }
1208
1429
  if (event.type === "context.updated" && isContextUpdatedPayload(event.payload)) {
1430
+ this.recordCommandResult(pending, event.type, "acknowledged");
1209
1431
  globalThis.clearTimeout(pending.timeout);
1210
1432
  globalThis.clearInterval(pending.retry);
1211
1433
  this.pendingCommands.delete(event.payload.message_id);
@@ -1214,6 +1436,8 @@ export class RealtimeSession {
1214
1436
  }
1215
1437
  if ((event.type === "command.error" && isCommandErrorPayload(event.payload))
1216
1438
  || (event.type === "context.rejected" && isContextRejectedPayload(event.payload))) {
1439
+ this.recordCommandResult(pending, event.type, "rejected", event.payload.code);
1440
+ this.flushDiagnostics("command_rejected");
1217
1441
  if (event.type === "context.rejected" && event.payload.retryable) {
1218
1442
  if (pending.retryTransfer && !pending.transferRetrying) {
1219
1443
  pending.transferRetrying = true;
@@ -1235,12 +1459,31 @@ export class RealtimeSession {
1235
1459
  for (const [messageId, pending] of this.pendingCommands) {
1236
1460
  if (preserveContext && pending.commandType === "context.update")
1237
1461
  continue;
1462
+ this.recordCommandResult(pending, pending.commandType === "context.update" ? "context.rejected" : "command.error", "failed", "pending_command_cancelled");
1238
1463
  globalThis.clearTimeout(pending.timeout);
1239
1464
  globalThis.clearInterval(pending.retry);
1240
1465
  pending.reject(error);
1241
1466
  this.pendingCommands.delete(messageId);
1242
1467
  }
1243
1468
  }
1469
+ recordCommandResult(pending, eventType, status, reason) {
1470
+ const durationMs = Date.now() - pending.startedAt;
1471
+ this.recordDiagnostic({
1472
+ operation: "command_result",
1473
+ status,
1474
+ reason,
1475
+ message_id: pending.message.message_id,
1476
+ connection_id: pending.message.connection_id,
1477
+ client_seq: pending.message.client_seq,
1478
+ context_key: pending.contextKey,
1479
+ payload_bytes: pending.payloadBytes,
1480
+ duration_ms: durationMs,
1481
+ retry_count: pending.retryCount,
1482
+ event_type: eventType,
1483
+ });
1484
+ if (durationMs > 2_000)
1485
+ this.flushDiagnostics("slow_operation");
1486
+ }
1244
1487
  async fetchReplay(apiBase, afterSeq) {
1245
1488
  let nextAfter = afterSeq;
1246
1489
  while (true) {
@@ -1389,6 +1632,43 @@ function contextStreamId() {
1389
1632
  return globalThis.crypto.randomUUID();
1390
1633
  return `ctx_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
1391
1634
  }
1635
+ function contextUpdateBytes(update) {
1636
+ return "image" in update
1637
+ ? update.image.size
1638
+ : new TextEncoder().encode(update.text).byteLength;
1639
+ }
1640
+ function contextPayloadBytes(payload) {
1641
+ const image = payload.image;
1642
+ if (image && typeof image === "object" && "bytes" in image) {
1643
+ const bytes = image.bytes;
1644
+ if (typeof bytes === "number")
1645
+ return bytes;
1646
+ }
1647
+ const textStream = payload.text_stream;
1648
+ if (textStream && typeof textStream === "object" && "bytes" in textStream) {
1649
+ const bytes = textStream.bytes;
1650
+ if (typeof bytes === "number")
1651
+ return bytes;
1652
+ }
1653
+ if (typeof payload.text === "string") {
1654
+ return new TextEncoder().encode(payload.text).byteLength;
1655
+ }
1656
+ try {
1657
+ return new TextEncoder().encode(JSON.stringify(payload)).byteLength;
1658
+ }
1659
+ catch {
1660
+ return undefined;
1661
+ }
1662
+ }
1663
+ function contextKeyFromMessage(message) {
1664
+ if (message.type !== "context.update")
1665
+ return undefined;
1666
+ const key = message.payload.key;
1667
+ return typeof key === "string" ? key : undefined;
1668
+ }
1669
+ function diagnosticEventType(type) {
1670
+ return type === "client.hello" || type === "context.update" ? type : undefined;
1671
+ }
1392
1672
  function validateSessionCredentials(value) {
1393
1673
  if (!value || typeof value !== "object") {
1394
1674
  throw new TypeError("Session credentials must be an object");