@absolutejs/voice 0.0.22-beta.32 → 0.0.22-beta.321

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 (240) hide show
  1. package/README.md +3354 -55
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agentSquadContract.d.ts +98 -0
  4. package/dist/angular/index.d.ts +16 -0
  5. package/dist/angular/index.js +3911 -1128
  6. package/dist/angular/voice-agent-squad-status.service.d.ts +12 -0
  7. package/dist/angular/voice-campaign-dialer-proof.service.d.ts +14 -0
  8. package/dist/angular/voice-controller.service.d.ts +1 -0
  9. package/dist/angular/voice-delivery-runtime.component.d.ts +17 -0
  10. package/dist/angular/voice-delivery-runtime.service.d.ts +16 -0
  11. package/dist/angular/voice-live-ops.service.d.ts +11 -0
  12. package/dist/angular/voice-ops-action-center.service.d.ts +13 -0
  13. package/dist/angular/voice-ops-status.component.d.ts +15 -0
  14. package/dist/angular/voice-ops-status.service.d.ts +12 -0
  15. package/dist/angular/voice-platform-coverage.service.d.ts +12 -0
  16. package/dist/angular/voice-proof-trends.service.d.ts +12 -0
  17. package/dist/angular/voice-provider-capabilities.service.d.ts +12 -0
  18. package/dist/angular/voice-provider-contracts.service.d.ts +12 -0
  19. package/dist/angular/voice-readiness-failures.service.d.ts +13 -0
  20. package/dist/angular/voice-routing-status.service.d.ts +11 -0
  21. package/dist/angular/voice-stream.service.d.ts +1 -0
  22. package/dist/angular/voice-trace-timeline.service.d.ts +12 -0
  23. package/dist/angular/voice-turn-latency.service.d.ts +13 -0
  24. package/dist/angular/voice-turn-quality.service.d.ts +12 -0
  25. package/dist/angular/voice-workflow-status.service.d.ts +12 -0
  26. package/dist/audit.d.ts +128 -0
  27. package/dist/auditDeliveryRoutes.d.ts +85 -0
  28. package/dist/auditExport.d.ts +34 -0
  29. package/dist/auditRoutes.d.ts +66 -0
  30. package/dist/auditSinks.d.ts +151 -0
  31. package/dist/bargeInRoutes.d.ts +56 -0
  32. package/dist/browserMediaRoutes.d.ts +61 -0
  33. package/dist/campaign.d.ts +768 -0
  34. package/dist/campaignDialers.d.ts +111 -0
  35. package/dist/client/actions.d.ts +83 -0
  36. package/dist/client/agentSquadStatus.d.ts +37 -0
  37. package/dist/client/agentSquadStatusWidget.d.ts +24 -0
  38. package/dist/client/bargeInMonitor.d.ts +7 -0
  39. package/dist/client/browserMedia.d.ts +8 -0
  40. package/dist/client/campaignDialerProof.d.ts +23 -0
  41. package/dist/client/deliveryRuntime.d.ts +34 -0
  42. package/dist/client/deliveryRuntimeWidget.d.ts +37 -0
  43. package/dist/client/duplex.d.ts +1 -1
  44. package/dist/client/htmxBootstrap.js +875 -14
  45. package/dist/client/index.d.ts +72 -0
  46. package/dist/client/index.js +5671 -19
  47. package/dist/client/liveOps.d.ts +22 -0
  48. package/dist/client/liveOpsWidget.d.ts +23 -0
  49. package/dist/client/liveTurnLatency.d.ts +41 -0
  50. package/dist/client/opsActionCenter.d.ts +54 -0
  51. package/dist/client/opsActionCenterWidget.d.ts +29 -0
  52. package/dist/client/opsActionHistory.d.ts +19 -0
  53. package/dist/client/opsActionHistoryWidget.d.ts +11 -0
  54. package/dist/client/opsStatus.d.ts +19 -0
  55. package/dist/client/opsStatusWidget.d.ts +40 -0
  56. package/dist/client/platformCoverage.d.ts +19 -0
  57. package/dist/client/platformCoverageWidget.d.ts +37 -0
  58. package/dist/client/proofTrends.d.ts +19 -0
  59. package/dist/client/proofTrendsWidget.d.ts +37 -0
  60. package/dist/client/providerCapabilities.d.ts +19 -0
  61. package/dist/client/providerCapabilitiesWidget.d.ts +32 -0
  62. package/dist/client/providerContracts.d.ts +19 -0
  63. package/dist/client/providerContractsWidget.d.ts +37 -0
  64. package/dist/client/providerSimulationControls.d.ts +33 -0
  65. package/dist/client/providerSimulationControlsWidget.d.ts +20 -0
  66. package/dist/client/providerStatusWidget.d.ts +32 -0
  67. package/dist/client/readinessFailures.d.ts +19 -0
  68. package/dist/client/readinessFailuresWidget.d.ts +42 -0
  69. package/dist/client/routingStatus.d.ts +19 -0
  70. package/dist/client/routingStatusWidget.d.ts +28 -0
  71. package/dist/client/traceTimeline.d.ts +19 -0
  72. package/dist/client/traceTimelineWidget.d.ts +36 -0
  73. package/dist/client/turnLatency.d.ts +22 -0
  74. package/dist/client/turnLatencyWidget.d.ts +33 -0
  75. package/dist/client/turnQuality.d.ts +19 -0
  76. package/dist/client/turnQualityWidget.d.ts +32 -0
  77. package/dist/client/workflowStatus.d.ts +19 -0
  78. package/dist/competitiveCoverage.d.ts +141 -0
  79. package/dist/dataControl.d.ts +180 -0
  80. package/dist/deliveryRuntime.d.ts +158 -0
  81. package/dist/deliverySinkRoutes.d.ts +117 -0
  82. package/dist/demoReadyRoutes.d.ts +98 -0
  83. package/dist/diagnosticsRoutes.d.ts +44 -0
  84. package/dist/evalRoutes.d.ts +219 -0
  85. package/dist/fileStore.d.ts +14 -2
  86. package/dist/guardrails.d.ts +128 -0
  87. package/dist/incidentBundle.d.ts +116 -0
  88. package/dist/index.d.ts +146 -13
  89. package/dist/index.js +28615 -5228
  90. package/dist/latencySlo.d.ts +56 -0
  91. package/dist/liveLatency.d.ts +78 -0
  92. package/dist/liveOps.d.ts +190 -0
  93. package/dist/mediaPipelineRoutes.d.ts +117 -0
  94. package/dist/modelAdapters.d.ts +54 -2
  95. package/dist/observabilityExport.d.ts +481 -0
  96. package/dist/openaiTTS.d.ts +18 -0
  97. package/dist/operationsRecord.d.ts +254 -0
  98. package/dist/opsActionAuditRoutes.d.ts +99 -0
  99. package/dist/opsConsoleRoutes.d.ts +80 -0
  100. package/dist/opsRecovery.d.ts +137 -0
  101. package/dist/opsStatus.d.ts +76 -0
  102. package/dist/opsStatusRoutes.d.ts +33 -0
  103. package/dist/outcomeContract.d.ts +146 -0
  104. package/dist/phoneAgent.d.ts +139 -0
  105. package/dist/phoneAgentProductionSmoke.d.ts +115 -0
  106. package/dist/platformCoverage.d.ts +91 -0
  107. package/dist/postCallAnalysis.d.ts +98 -0
  108. package/dist/postgresStore.d.ts +13 -2
  109. package/dist/productionReadiness.d.ts +594 -0
  110. package/dist/proofTrends.d.ts +133 -0
  111. package/dist/providerAdapters.d.ts +48 -0
  112. package/dist/providerCapabilities.d.ts +92 -0
  113. package/dist/providerDecisionTraces.d.ts +130 -0
  114. package/dist/providerOrchestration.d.ts +109 -0
  115. package/dist/providerRoutingContract.d.ts +71 -0
  116. package/dist/providerSlo.d.ts +142 -0
  117. package/dist/providerStackRecommendations.d.ts +187 -0
  118. package/dist/qualityRoutes.d.ts +76 -0
  119. package/dist/queue.d.ts +9 -0
  120. package/dist/react/VoiceAgentSquadStatus.d.ts +5 -0
  121. package/dist/react/VoiceDeliveryRuntime.d.ts +7 -0
  122. package/dist/react/VoiceOpsActionCenter.d.ts +5 -0
  123. package/dist/react/VoiceOpsStatus.d.ts +6 -0
  124. package/dist/react/VoicePlatformCoverage.d.ts +6 -0
  125. package/dist/react/VoiceProofTrends.d.ts +6 -0
  126. package/dist/react/VoiceProviderCapabilities.d.ts +6 -0
  127. package/dist/react/VoiceProviderContracts.d.ts +6 -0
  128. package/dist/react/VoiceProviderSimulationControls.d.ts +5 -0
  129. package/dist/react/VoiceProviderStatus.d.ts +6 -0
  130. package/dist/react/VoiceReadinessFailures.d.ts +6 -0
  131. package/dist/react/VoiceRoutingStatus.d.ts +6 -0
  132. package/dist/react/VoiceTraceTimeline.d.ts +6 -0
  133. package/dist/react/VoiceTurnLatency.d.ts +6 -0
  134. package/dist/react/VoiceTurnQuality.d.ts +6 -0
  135. package/dist/react/index.d.ts +32 -0
  136. package/dist/react/index.js +5472 -31
  137. package/dist/react/useVoiceAgentSquadStatus.d.ts +8 -0
  138. package/dist/react/useVoiceCampaignDialerProof.d.ts +10 -0
  139. package/dist/react/useVoiceController.d.ts +1 -0
  140. package/dist/react/useVoiceDeliveryRuntime.d.ts +13 -0
  141. package/dist/react/useVoiceLiveOps.d.ts +9 -0
  142. package/dist/react/useVoiceOpsActionCenter.d.ts +11 -0
  143. package/dist/react/useVoiceOpsStatus.d.ts +8 -0
  144. package/dist/react/useVoicePlatformCoverage.d.ts +8 -0
  145. package/dist/react/useVoiceProofTrends.d.ts +8 -0
  146. package/dist/react/useVoiceProviderCapabilities.d.ts +8 -0
  147. package/dist/react/useVoiceProviderContracts.d.ts +8 -0
  148. package/dist/react/useVoiceProviderSimulationControls.d.ts +10 -0
  149. package/dist/react/useVoiceReadinessFailures.d.ts +8 -0
  150. package/dist/react/useVoiceRoutingStatus.d.ts +8 -0
  151. package/dist/react/useVoiceStream.d.ts +1 -0
  152. package/dist/react/useVoiceTraceTimeline.d.ts +8 -0
  153. package/dist/react/useVoiceTurnLatency.d.ts +9 -0
  154. package/dist/react/useVoiceTurnQuality.d.ts +8 -0
  155. package/dist/react/useVoiceWorkflowStatus.d.ts +8 -0
  156. package/dist/readinessProfiles.d.ts +38 -0
  157. package/dist/realtimeChannel.d.ts +136 -0
  158. package/dist/realtimeProviderContracts.d.ts +133 -0
  159. package/dist/reconnectContract.d.ts +88 -0
  160. package/dist/resilienceRoutes.d.ts +143 -0
  161. package/dist/sessionReplay.d.ts +12 -0
  162. package/dist/simulationSuite.d.ts +143 -0
  163. package/dist/sloCalibration.d.ts +185 -0
  164. package/dist/sqliteStore.d.ts +13 -2
  165. package/dist/svelte/createVoiceAgentSquadStatus.d.ts +9 -0
  166. package/dist/svelte/createVoiceCampaignDialerProof.d.ts +9 -0
  167. package/dist/svelte/createVoiceDeliveryRuntime.d.ts +11 -0
  168. package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
  169. package/dist/svelte/createVoiceOpsActionCenter.d.ts +10 -0
  170. package/dist/svelte/createVoiceOpsStatus.d.ts +9 -0
  171. package/dist/svelte/createVoicePlatformCoverage.d.ts +7 -0
  172. package/dist/svelte/createVoiceProofTrends.d.ts +7 -0
  173. package/dist/svelte/createVoiceProviderCapabilities.d.ts +10 -0
  174. package/dist/svelte/createVoiceProviderContracts.d.ts +10 -0
  175. package/dist/svelte/createVoiceProviderSimulationControls.d.ts +11 -0
  176. package/dist/svelte/createVoiceProviderStatus.d.ts +4 -2
  177. package/dist/svelte/createVoiceReadinessFailures.d.ts +7 -0
  178. package/dist/svelte/createVoiceRoutingStatus.d.ts +10 -0
  179. package/dist/svelte/createVoiceTraceTimeline.d.ts +10 -0
  180. package/dist/svelte/createVoiceTurnLatency.d.ts +11 -0
  181. package/dist/svelte/createVoiceTurnQuality.d.ts +10 -0
  182. package/dist/svelte/createVoiceWorkflowStatus.d.ts +8 -0
  183. package/dist/svelte/index.d.ts +17 -0
  184. package/dist/svelte/index.js +5337 -420
  185. package/dist/telephony/contract.d.ts +61 -0
  186. package/dist/telephony/matrix.d.ts +97 -0
  187. package/dist/telephony/plivo.d.ts +303 -0
  188. package/dist/telephony/security.d.ts +182 -0
  189. package/dist/telephony/telnyx.d.ts +291 -0
  190. package/dist/telephony/twilio.d.ts +135 -2
  191. package/dist/telephonyOutcome.d.ts +273 -0
  192. package/dist/testing/index.d.ts +1 -0
  193. package/dist/testing/index.js +2172 -76
  194. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  195. package/dist/toolContract.d.ts +161 -0
  196. package/dist/toolRuntime.d.ts +50 -0
  197. package/dist/trace.d.ts +19 -1
  198. package/dist/traceDeliveryRoutes.d.ts +86 -0
  199. package/dist/traceTimeline.d.ts +97 -0
  200. package/dist/turnLatency.d.ts +95 -0
  201. package/dist/turnQuality.d.ts +94 -0
  202. package/dist/types.d.ts +116 -3
  203. package/dist/voiceMonitoring.d.ts +444 -0
  204. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  205. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  206. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  207. package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
  208. package/dist/vue/VoiceProofTrends.d.ts +21 -0
  209. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  210. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  211. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  212. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  213. package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
  214. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  215. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  216. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  217. package/dist/vue/index.d.ts +30 -0
  218. package/dist/vue/index.js +5241 -56
  219. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  220. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  221. package/dist/vue/useVoiceController.d.ts +2 -1
  222. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  223. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  224. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  225. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  226. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  227. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  228. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  229. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  230. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  231. package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
  232. package/dist/vue/useVoiceReadinessFailures.d.ts +837 -0
  233. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  234. package/dist/vue/useVoiceStream.d.ts +2 -1
  235. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  236. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  237. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  238. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  239. package/dist/workflowContract.d.ts +91 -0
  240. package/package.json +4 -1
@@ -188,6 +188,11 @@ var serverMessageToAction = (message) => {
188
188
  sessionId: message.sessionId,
189
189
  type: "complete"
190
190
  };
191
+ case "connection":
192
+ return {
193
+ reconnect: message.reconnect,
194
+ type: "connection"
195
+ };
191
196
  case "call_lifecycle":
192
197
  return {
193
198
  event: message.event,
@@ -209,6 +214,17 @@ var serverMessageToAction = (message) => {
209
214
  transcript: message.transcript,
210
215
  type: "partial"
211
216
  };
217
+ case "replay":
218
+ return {
219
+ assistantTexts: message.assistantTexts,
220
+ call: message.call,
221
+ partial: message.partial,
222
+ scenarioId: message.scenarioId,
223
+ sessionId: message.sessionId,
224
+ status: message.status,
225
+ turns: message.turns,
226
+ type: "replay"
227
+ };
212
228
  case "session":
213
229
  return {
214
230
  sessionId: message.sessionId,
@@ -226,6 +242,170 @@ var serverMessageToAction = (message) => {
226
242
  }
227
243
  };
228
244
 
245
+ // node_modules/@absolutejs/media/dist/index.js
246
+ var pushIssue = (issues, severity, code, message) => {
247
+ issues.push({ code, message, severity });
248
+ };
249
+ var average = (values) => values.length === 0 ? undefined : values.reduce((total, value) => total + value, 0) / values.length;
250
+ var max = (values) => values.length === 0 ? undefined : Math.max(...values);
251
+ var numericStat = (stat, key) => {
252
+ const value = stat[key];
253
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
254
+ };
255
+ var booleanStat = (stat, key) => {
256
+ const value = stat[key];
257
+ return typeof value === "boolean" ? value : undefined;
258
+ };
259
+ var stringStat = (stat, key) => {
260
+ const value = stat[key];
261
+ return typeof value === "string" ? value : undefined;
262
+ };
263
+ var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
264
+ var normalizeWebRTCStat = (stat) => {
265
+ const sample = {};
266
+ for (const [key, value] of Object.entries(stat)) {
267
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
268
+ sample[key] = value;
269
+ }
270
+ }
271
+ return sample;
272
+ };
273
+ var buildMediaWebRTCStatsReport = (input = {}) => {
274
+ const stats = input.stats ?? [];
275
+ const issues = [];
276
+ const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
277
+ const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
278
+ const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
279
+ const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
280
+ const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
281
+ const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
282
+ const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
283
+ const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
284
+ const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
285
+ const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
286
+ const packetLossDenominator = inboundPackets + packetsLost;
287
+ const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
288
+ const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
289
+ const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
290
+ const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
291
+ const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
292
+ const jitterBufferDelayMs = max(inbound.map((stat) => {
293
+ const delay = numericStat(stat, "jitterBufferDelay");
294
+ const emitted = numericStat(stat, "jitterBufferEmittedCount");
295
+ return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
296
+ }).filter((value) => value !== undefined));
297
+ const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
298
+ if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
299
+ pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
300
+ }
301
+ if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
302
+ pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
303
+ }
304
+ if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
305
+ pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
306
+ }
307
+ if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
308
+ pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
309
+ }
310
+ if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
311
+ pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
312
+ }
313
+ return {
314
+ activeCandidatePairs,
315
+ audioLevelAverage: average(audioLevels),
316
+ bytesReceived,
317
+ bytesSent,
318
+ checkedAt: Date.now(),
319
+ endedAudioTracks,
320
+ inboundPackets,
321
+ issues,
322
+ jitterBufferDelayMs,
323
+ jitterMs,
324
+ liveAudioTracks,
325
+ outboundPackets,
326
+ packetLossRatio,
327
+ packetsLost,
328
+ roundTripTimeMs,
329
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
330
+ totalStats: stats.length
331
+ };
332
+ };
333
+ var collectMediaWebRTCStats = async (input) => {
334
+ const report = await input.peerConnection.getStats(input.selector ?? null);
335
+ return [...report.values()].map(normalizeWebRTCStat);
336
+ };
337
+ var collectMediaWebRTCStatsReport = async (input) => {
338
+ const stats = await collectMediaWebRTCStats(input);
339
+ return buildMediaWebRTCStatsReport({
340
+ ...input,
341
+ stats
342
+ });
343
+ };
344
+
345
+ // src/client/browserMedia.ts
346
+ var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
347
+ var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
348
+ var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
349
+ var postBrowserMediaReport = async (payload, options) => {
350
+ const requestFetch = options.fetch ?? globalThis.fetch;
351
+ if (!requestFetch) {
352
+ return;
353
+ }
354
+ await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
355
+ body: JSON.stringify(payload),
356
+ headers: {
357
+ "Content-Type": "application/json"
358
+ },
359
+ keepalive: true,
360
+ method: "POST"
361
+ });
362
+ };
363
+ var createVoiceBrowserMediaReporter = (options) => {
364
+ let interval = null;
365
+ const reportOnce = async () => {
366
+ const peerConnection = await resolvePeerConnection(options);
367
+ if (!peerConnection) {
368
+ return;
369
+ }
370
+ const report = await collectMediaWebRTCStatsReport({
371
+ ...options,
372
+ peerConnection
373
+ });
374
+ const payload = {
375
+ at: Date.now(),
376
+ report,
377
+ scenarioId: options.getScenarioId?.() ?? null,
378
+ sessionId: options.getSessionId?.() ?? null
379
+ };
380
+ options.onReport?.(payload);
381
+ await postBrowserMediaReport(payload, options);
382
+ return payload;
383
+ };
384
+ const run = () => {
385
+ reportOnce().catch((error) => {
386
+ options.onError?.(error);
387
+ });
388
+ };
389
+ const stop = () => {
390
+ if (interval) {
391
+ clearInterval(interval);
392
+ interval = null;
393
+ }
394
+ };
395
+ return {
396
+ close: stop,
397
+ reportOnce,
398
+ start: () => {
399
+ if (interval) {
400
+ return;
401
+ }
402
+ run();
403
+ interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
404
+ },
405
+ stop
406
+ };
407
+ };
408
+
229
409
  // src/client/connection.ts
230
410
  var WS_OPEN = 1;
231
411
  var WS_CLOSED = 3;
@@ -269,10 +449,12 @@ var isVoiceServerMessage = (value) => {
269
449
  case "assistant":
270
450
  case "call_lifecycle":
271
451
  case "complete":
452
+ case "connection":
272
453
  case "error":
273
454
  case "final":
274
455
  case "partial":
275
456
  case "pong":
457
+ case "replay":
276
458
  case "session":
277
459
  case "turn":
278
460
  return true;
@@ -309,6 +491,9 @@ var createVoiceConnection = (path, options = {}) => {
309
491
  sessionId: options.sessionId ?? createSessionId(),
310
492
  ws: null
311
493
  };
494
+ const emitConnection = (reconnect) => {
495
+ listeners.forEach((listener) => listener(reconnect));
496
+ };
312
497
  const clearTimers = () => {
313
498
  if (state.pingInterval) {
314
499
  clearInterval(state.pingInterval);
@@ -331,9 +516,28 @@ var createVoiceConnection = (path, options = {}) => {
331
516
  }
332
517
  };
333
518
  const scheduleReconnect = () => {
519
+ const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
334
520
  state.reconnectAttempts += 1;
521
+ emitConnection({
522
+ reconnect: {
523
+ attempts: state.reconnectAttempts,
524
+ lastDisconnectAt: Date.now(),
525
+ maxAttempts: maxReconnectAttempts,
526
+ nextAttemptAt,
527
+ status: "reconnecting"
528
+ },
529
+ type: "connection"
530
+ });
335
531
  state.reconnectTimeout = setTimeout(() => {
336
532
  if (state.reconnectAttempts > maxReconnectAttempts) {
533
+ emitConnection({
534
+ reconnect: {
535
+ attempts: state.reconnectAttempts,
536
+ maxAttempts: maxReconnectAttempts,
537
+ status: "exhausted"
538
+ },
539
+ type: "connection"
540
+ });
337
541
  return;
338
542
  }
339
543
  connect();
@@ -343,9 +547,21 @@ var createVoiceConnection = (path, options = {}) => {
343
547
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
344
548
  ws.binaryType = "arraybuffer";
345
549
  ws.onopen = () => {
550
+ const wasReconnecting = state.reconnectAttempts > 0;
346
551
  state.isConnected = true;
347
- state.reconnectAttempts = 0;
348
552
  flushPendingMessages();
553
+ if (wasReconnecting) {
554
+ emitConnection({
555
+ reconnect: {
556
+ attempts: state.reconnectAttempts,
557
+ lastResumedAt: Date.now(),
558
+ maxAttempts: maxReconnectAttempts,
559
+ status: "resumed"
560
+ },
561
+ type: "connection"
562
+ });
563
+ state.reconnectAttempts = 0;
564
+ }
349
565
  listeners.forEach((listener) => listener({
350
566
  scenarioId: state.scenarioId ?? undefined,
351
567
  sessionId: state.sessionId,
@@ -375,6 +591,16 @@ var createVoiceConnection = (path, options = {}) => {
375
591
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
376
592
  if (reconnectable) {
377
593
  scheduleReconnect();
594
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
595
+ emitConnection({
596
+ reconnect: {
597
+ attempts: state.reconnectAttempts,
598
+ lastDisconnectAt: Date.now(),
599
+ maxAttempts: maxReconnectAttempts,
600
+ status: "exhausted"
601
+ },
602
+ type: "connection"
603
+ });
378
604
  }
379
605
  };
380
606
  state.ws = ws;
@@ -445,6 +671,11 @@ var createVoiceConnection = (path, options = {}) => {
445
671
  };
446
672
 
447
673
  // src/client/store.ts
674
+ var createInitialReconnectState = () => ({
675
+ attempts: 0,
676
+ maxAttempts: 0,
677
+ status: "idle"
678
+ });
448
679
  var createInitialState = () => ({
449
680
  assistantAudio: [],
450
681
  assistantTexts: [],
@@ -453,6 +684,7 @@ var createInitialState = () => ({
453
684
  isConnected: false,
454
685
  scenarioId: null,
455
686
  partial: "",
687
+ reconnect: createInitialReconnectState(),
456
688
  sessionId: null,
457
689
  status: "idle",
458
690
  turns: []
@@ -509,7 +741,19 @@ var createVoiceStreamStore = () => {
509
741
  case "connected":
510
742
  state = {
511
743
  ...state,
512
- isConnected: true
744
+ isConnected: true,
745
+ reconnect: state.reconnect.status === "reconnecting" ? {
746
+ ...state.reconnect,
747
+ lastResumedAt: Date.now(),
748
+ nextAttemptAt: undefined,
749
+ status: "resumed"
750
+ } : state.reconnect
751
+ };
752
+ break;
753
+ case "connection":
754
+ state = {
755
+ ...state,
756
+ reconnect: action.reconnect
513
757
  };
514
758
  break;
515
759
  case "disconnected":
@@ -537,6 +781,26 @@ var createVoiceStreamStore = () => {
537
781
  partial: action.transcript.text
538
782
  };
539
783
  break;
784
+ case "replay":
785
+ state = {
786
+ ...state,
787
+ assistantTexts: [...action.assistantTexts],
788
+ call: action.call ?? null,
789
+ error: null,
790
+ isConnected: action.status === "active",
791
+ partial: action.partial,
792
+ reconnect: state.reconnect.status === "reconnecting" ? {
793
+ ...state.reconnect,
794
+ lastResumedAt: Date.now(),
795
+ nextAttemptAt: undefined,
796
+ status: "resumed"
797
+ } : state.reconnect,
798
+ scenarioId: action.scenarioId ?? state.scenarioId,
799
+ sessionId: action.sessionId,
800
+ status: action.status,
801
+ turns: [...action.turns]
802
+ };
803
+ break;
540
804
  case "session":
541
805
  state = {
542
806
  ...state,
@@ -574,20 +838,50 @@ var createVoiceStreamStore = () => {
574
838
  var createVoiceStream = (path, options = {}) => {
575
839
  const connection = createVoiceConnection(path, options);
576
840
  const store = createVoiceStreamStore();
841
+ const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
842
+ ...options.browserMedia,
843
+ getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
844
+ getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
845
+ }) : null;
577
846
  const subscribers = new Set;
578
847
  const start = (input) => Promise.resolve().then(() => {
579
848
  if (!input?.sessionId && !input?.scenarioId) {
580
849
  return;
581
850
  }
582
851
  connection.start(input);
852
+ browserMediaReporter?.start();
583
853
  });
584
854
  const notify = () => {
585
855
  subscribers.forEach((subscriber) => subscriber());
586
856
  };
857
+ const reportReconnect = () => {
858
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
859
+ return;
860
+ }
861
+ const snapshot = store.getSnapshot();
862
+ const body = JSON.stringify({
863
+ at: Date.now(),
864
+ reconnect: snapshot.reconnect,
865
+ scenarioId: snapshot.scenarioId,
866
+ sessionId: connection.getSessionId(),
867
+ turnIds: snapshot.turns.map((turn) => turn.id)
868
+ });
869
+ fetch(options.reconnectReportPath, {
870
+ body,
871
+ headers: {
872
+ "Content-Type": "application/json"
873
+ },
874
+ keepalive: true,
875
+ method: "POST"
876
+ }).catch(() => {});
877
+ };
587
878
  const unsubscribeConnection = connection.subscribe((message) => {
588
879
  const action = serverMessageToAction(message);
589
880
  if (action) {
590
881
  store.dispatch(action);
882
+ if (message.type === "connection") {
883
+ reportReconnect();
884
+ }
591
885
  notify();
592
886
  }
593
887
  });
@@ -597,6 +891,7 @@ var createVoiceStream = (path, options = {}) => {
597
891
  },
598
892
  close() {
599
893
  unsubscribeConnection();
894
+ browserMediaReporter?.close();
600
895
  connection.close();
601
896
  store.dispatch({ type: "disconnected" });
602
897
  notify();
@@ -623,6 +918,9 @@ var createVoiceStream = (path, options = {}) => {
623
918
  get partial() {
624
919
  return store.getSnapshot().partial;
625
920
  },
921
+ get reconnect() {
922
+ return store.getSnapshot().reconnect;
923
+ },
626
924
  get sessionId() {
627
925
  return connection.getSessionId();
628
926
  },
@@ -941,6 +1239,7 @@ var createInitialState2 = (stream) => ({
941
1239
  isConnected: stream.isConnected,
942
1240
  isRecording: false,
943
1241
  partial: stream.partial,
1242
+ reconnect: stream.reconnect,
944
1243
  recordingError: null,
945
1244
  sessionId: stream.sessionId,
946
1245
  scenarioId: stream.scenarioId,
@@ -970,6 +1269,7 @@ var createVoiceController = (path, options = {}) => {
970
1269
  error: stream.error,
971
1270
  isConnected: stream.isConnected,
972
1271
  partial: stream.partial,
1272
+ reconnect: stream.reconnect,
973
1273
  sessionId: stream.sessionId,
974
1274
  scenarioId: stream.scenarioId,
975
1275
  status: stream.status,
@@ -994,7 +1294,13 @@ var createVoiceController = (path, options = {}) => {
994
1294
  capture = createMicrophoneCapture({
995
1295
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
1296
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
1297
+ onAudio: (audio) => {
1298
+ if (options.capture?.onAudio) {
1299
+ options.capture.onAudio(audio, stream.sendAudio);
1300
+ return;
1301
+ }
1302
+ stream.sendAudio(audio);
1303
+ },
998
1304
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
999
1305
  });
1000
1306
  return capture;
@@ -1064,6 +1370,9 @@ var createVoiceController = (path, options = {}) => {
1064
1370
  get recordingError() {
1065
1371
  return state.recordingError;
1066
1372
  },
1373
+ get reconnect() {
1374
+ return state.reconnect;
1375
+ },
1067
1376
  sendAudio: (audio) => stream.sendAudio(audio),
1068
1377
  get sessionId() {
1069
1378
  return state.sessionId;
@@ -1104,6 +1413,475 @@ var createVoiceController = (path, options = {}) => {
1104
1413
  };
1105
1414
  };
1106
1415
 
1416
+ // src/client/audioPlayer.ts
1417
+ var DEFAULT_LOOKAHEAD_MS = 15;
1418
+ var createInitialState3 = () => ({
1419
+ activeSourceCount: 0,
1420
+ error: null,
1421
+ isActive: false,
1422
+ isPlaying: false,
1423
+ lastInterruptLatencyMs: undefined,
1424
+ lastPlaybackStopLatencyMs: undefined,
1425
+ processedChunkCount: 0,
1426
+ queuedChunkCount: 0
1427
+ });
1428
+ var getAudioContextCtor = () => {
1429
+ if (typeof window === "undefined") {
1430
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1431
+ }
1432
+ return window.AudioContext ?? window.webkitAudioContext;
1433
+ };
1434
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1435
+ const format = chunk.format;
1436
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1437
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1438
+ }
1439
+ const bytes = chunk.chunk;
1440
+ const channels = Math.max(1, format.channels);
1441
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1442
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1443
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1444
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1445
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1446
+ const channelData = audioBuffer.getChannelData(channelIndex);
1447
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1448
+ const sampleIndex = frameIndex * channels + channelIndex;
1449
+ const sampleOffset = sampleIndex * 2;
1450
+ if (sampleOffset + 1 >= bytes.byteLength) {
1451
+ channelData[frameIndex] = 0;
1452
+ continue;
1453
+ }
1454
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1455
+ }
1456
+ }
1457
+ return audioBuffer;
1458
+ };
1459
+ var createVoiceAudioPlayer = (source, options = {}) => {
1460
+ const subscribers = new Set;
1461
+ const sourceNodes = new Set;
1462
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1463
+ let state = createInitialState3();
1464
+ let audioContext = null;
1465
+ let outputNode = null;
1466
+ let queueEndTime = 0;
1467
+ let syncPromise = Promise.resolve();
1468
+ let interruptStartedAt = null;
1469
+ let interruptPromise = null;
1470
+ let resolveInterruptPromise = null;
1471
+ let interruptFallbackTimer = null;
1472
+ const notify = () => {
1473
+ for (const subscriber of subscribers) {
1474
+ subscriber();
1475
+ }
1476
+ };
1477
+ const setState = (next) => {
1478
+ state = {
1479
+ ...state,
1480
+ ...next
1481
+ };
1482
+ notify();
1483
+ };
1484
+ const clearError = () => {
1485
+ if (state.error !== null) {
1486
+ setState({ error: null });
1487
+ }
1488
+ };
1489
+ const clearInterruptTimer = () => {
1490
+ if (interruptFallbackTimer !== null) {
1491
+ clearTimeout(interruptFallbackTimer);
1492
+ interruptFallbackTimer = null;
1493
+ }
1494
+ };
1495
+ const resolveInterrupt = (latencyMs) => {
1496
+ clearInterruptTimer();
1497
+ interruptStartedAt = null;
1498
+ setState({
1499
+ activeSourceCount: sourceNodes.size,
1500
+ isPlaying: false,
1501
+ lastInterruptLatencyMs: latencyMs,
1502
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1503
+ });
1504
+ resolveInterruptPromise?.();
1505
+ resolveInterruptPromise = null;
1506
+ interruptPromise = null;
1507
+ };
1508
+ const estimateOutputStopLatencyMs = (context) => {
1509
+ if (!context) {
1510
+ return 0;
1511
+ }
1512
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1513
+ };
1514
+ const restoreOutputGain = (context) => {
1515
+ if (!outputNode) {
1516
+ return;
1517
+ }
1518
+ const gainValue = 1;
1519
+ if (outputNode.gain.setValueAtTime) {
1520
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1521
+ return;
1522
+ }
1523
+ outputNode.gain.value = gainValue;
1524
+ };
1525
+ const muteOutputGain = (context) => {
1526
+ if (!outputNode) {
1527
+ return;
1528
+ }
1529
+ const gainValue = 0;
1530
+ if (outputNode.gain.setValueAtTime) {
1531
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1532
+ return;
1533
+ }
1534
+ outputNode.gain.value = gainValue;
1535
+ };
1536
+ const maybeResolveInterrupt = () => {
1537
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1538
+ return;
1539
+ }
1540
+ resolveInterrupt(Date.now() - interruptStartedAt);
1541
+ };
1542
+ const ensureAudioContext = async () => {
1543
+ if (audioContext) {
1544
+ return audioContext;
1545
+ }
1546
+ if (options.createAudioContext) {
1547
+ audioContext = options.createAudioContext();
1548
+ } else {
1549
+ const AudioContextCtor = getAudioContextCtor();
1550
+ if (!AudioContextCtor) {
1551
+ throw new Error("Assistant audio playback requires AudioContext support.");
1552
+ }
1553
+ audioContext = new AudioContextCtor;
1554
+ }
1555
+ if (audioContext.createGain) {
1556
+ outputNode = audioContext.createGain();
1557
+ outputNode.connect?.(audioContext.destination);
1558
+ }
1559
+ queueEndTime = audioContext.currentTime;
1560
+ return audioContext;
1561
+ };
1562
+ const scheduleChunk = async (chunk) => {
1563
+ const context = await ensureAudioContext();
1564
+ const buffer = decodePCM16LEChunk(context, chunk);
1565
+ const node = context.createBufferSource();
1566
+ node.buffer = buffer;
1567
+ node.connect(outputNode ?? context.destination);
1568
+ node.onended = () => {
1569
+ sourceNodes.delete(node);
1570
+ node.disconnect?.();
1571
+ setState({
1572
+ activeSourceCount: sourceNodes.size,
1573
+ isPlaying: sourceNodes.size > 0 && state.isActive
1574
+ });
1575
+ maybeResolveInterrupt();
1576
+ };
1577
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1578
+ queueEndTime = startAt + buffer.duration;
1579
+ sourceNodes.add(node);
1580
+ setState({
1581
+ activeSourceCount: sourceNodes.size,
1582
+ isPlaying: true
1583
+ });
1584
+ node.start(startAt);
1585
+ };
1586
+ const stopQueuedPlayback = (options2) => {
1587
+ for (const node of [...sourceNodes]) {
1588
+ node.stop?.();
1589
+ }
1590
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1591
+ if (options2?.forceClear) {
1592
+ for (const node of sourceNodes) {
1593
+ node.disconnect?.();
1594
+ }
1595
+ sourceNodes.clear();
1596
+ maybeResolveInterrupt();
1597
+ }
1598
+ };
1599
+ const sync = async () => {
1600
+ if (!state.isActive) {
1601
+ return;
1602
+ }
1603
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1604
+ if (nextChunks.length === 0) {
1605
+ return;
1606
+ }
1607
+ try {
1608
+ clearError();
1609
+ for (const chunk of nextChunks) {
1610
+ await scheduleChunk(chunk);
1611
+ }
1612
+ setState({
1613
+ processedChunkCount: source.assistantAudio.length,
1614
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1615
+ });
1616
+ } catch (error) {
1617
+ setState({
1618
+ error: error instanceof Error ? error.message : String(error)
1619
+ });
1620
+ }
1621
+ };
1622
+ const queueSync = () => {
1623
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1624
+ return syncPromise;
1625
+ };
1626
+ const unsubscribeSource = source.subscribe(() => {
1627
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1628
+ player.start();
1629
+ return;
1630
+ }
1631
+ if (state.isActive) {
1632
+ queueSync();
1633
+ }
1634
+ });
1635
+ const player = {
1636
+ close: async () => {
1637
+ unsubscribeSource();
1638
+ stopQueuedPlayback({ forceClear: true });
1639
+ clearInterruptTimer();
1640
+ resolveInterruptPromise?.();
1641
+ resolveInterruptPromise = null;
1642
+ interruptPromise = null;
1643
+ interruptStartedAt = null;
1644
+ if (audioContext && audioContext.state !== "closed") {
1645
+ await audioContext.close();
1646
+ }
1647
+ audioContext = null;
1648
+ outputNode?.disconnect?.();
1649
+ outputNode = null;
1650
+ queueEndTime = 0;
1651
+ setState({
1652
+ activeSourceCount: 0,
1653
+ isActive: false,
1654
+ isPlaying: false
1655
+ });
1656
+ },
1657
+ get activeSourceCount() {
1658
+ return state.activeSourceCount;
1659
+ },
1660
+ get error() {
1661
+ return state.error;
1662
+ },
1663
+ getSnapshot: () => state,
1664
+ get isActive() {
1665
+ return state.isActive;
1666
+ },
1667
+ get isPlaying() {
1668
+ return state.isPlaying;
1669
+ },
1670
+ interrupt: async () => {
1671
+ const startedAt = Date.now();
1672
+ const context = await ensureAudioContext();
1673
+ interruptStartedAt = startedAt;
1674
+ muteOutputGain(context);
1675
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
1676
+ setState({
1677
+ isActive: false,
1678
+ isPlaying: sourceNodes.size > 0,
1679
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
1680
+ });
1681
+ if (sourceNodes.size === 0) {
1682
+ resolveInterrupt(playbackStopLatencyMs);
1683
+ return;
1684
+ }
1685
+ if (!interruptPromise) {
1686
+ interruptPromise = new Promise((resolve) => {
1687
+ resolveInterruptPromise = resolve;
1688
+ });
1689
+ }
1690
+ clearInterruptTimer();
1691
+ interruptFallbackTimer = setTimeout(() => {
1692
+ for (const node of sourceNodes) {
1693
+ node.disconnect?.();
1694
+ }
1695
+ sourceNodes.clear();
1696
+ resolveInterrupt(Date.now() - startedAt);
1697
+ }, 250);
1698
+ stopQueuedPlayback();
1699
+ await interruptPromise;
1700
+ },
1701
+ get lastInterruptLatencyMs() {
1702
+ return state.lastInterruptLatencyMs;
1703
+ },
1704
+ get lastPlaybackStopLatencyMs() {
1705
+ return state.lastPlaybackStopLatencyMs;
1706
+ },
1707
+ pause: async () => {
1708
+ if (!audioContext) {
1709
+ setState({
1710
+ activeSourceCount: 0,
1711
+ isActive: false,
1712
+ isPlaying: false
1713
+ });
1714
+ return;
1715
+ }
1716
+ await audioContext.suspend();
1717
+ setState({
1718
+ activeSourceCount: sourceNodes.size,
1719
+ isActive: false,
1720
+ isPlaying: false
1721
+ });
1722
+ },
1723
+ get processedChunkCount() {
1724
+ return state.processedChunkCount;
1725
+ },
1726
+ get queuedChunkCount() {
1727
+ return state.queuedChunkCount;
1728
+ },
1729
+ start: async () => {
1730
+ try {
1731
+ clearError();
1732
+ const context = await ensureAudioContext();
1733
+ restoreOutputGain(context);
1734
+ if (context.state === "suspended") {
1735
+ await context.resume();
1736
+ }
1737
+ setState({
1738
+ activeSourceCount: sourceNodes.size,
1739
+ isActive: true,
1740
+ isPlaying: context.state === "running"
1741
+ });
1742
+ await queueSync();
1743
+ } catch (error) {
1744
+ setState({
1745
+ error: error instanceof Error ? error.message : String(error),
1746
+ isActive: false,
1747
+ isPlaying: false
1748
+ });
1749
+ throw error;
1750
+ }
1751
+ },
1752
+ subscribe: (subscriber) => {
1753
+ subscribers.add(subscriber);
1754
+ return () => {
1755
+ subscribers.delete(subscriber);
1756
+ };
1757
+ }
1758
+ };
1759
+ return player;
1760
+ };
1761
+
1762
+ // src/client/bargeInMonitor.ts
1763
+ var DEFAULT_THRESHOLD_MS = 250;
1764
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1765
+ var summarize = (events, thresholdMs) => {
1766
+ const stopped = events.filter((event) => event.status === "stopped");
1767
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1768
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1769
+ const passed = stopped.length - failed;
1770
+ return {
1771
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1772
+ events: [...events],
1773
+ failed,
1774
+ lastEvent: events.at(-1),
1775
+ passed,
1776
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1777
+ thresholdMs,
1778
+ total: stopped.length
1779
+ };
1780
+ };
1781
+ var createVoiceBargeInMonitor = (options = {}) => {
1782
+ const listeners = new Set;
1783
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1784
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1785
+ const events = [];
1786
+ const emit = () => {
1787
+ for (const listener of listeners) {
1788
+ listener();
1789
+ }
1790
+ };
1791
+ const postEvent = (event) => {
1792
+ if (!options.path || typeof fetchImpl !== "function") {
1793
+ return;
1794
+ }
1795
+ fetchImpl(options.path, {
1796
+ body: JSON.stringify(event),
1797
+ headers: {
1798
+ "Content-Type": "application/json"
1799
+ },
1800
+ method: "POST"
1801
+ }).catch(() => {});
1802
+ };
1803
+ const record = (status, input) => {
1804
+ const event = {
1805
+ at: Date.now(),
1806
+ id: createEventId(),
1807
+ latencyMs: input.latencyMs,
1808
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1809
+ reason: input.reason,
1810
+ sessionId: input.sessionId,
1811
+ status,
1812
+ thresholdMs
1813
+ };
1814
+ events.push(event);
1815
+ postEvent(event);
1816
+ emit();
1817
+ return event;
1818
+ };
1819
+ return {
1820
+ getSnapshot: () => summarize(events, thresholdMs),
1821
+ recordRequested: (input) => record("requested", input),
1822
+ recordSkipped: (input) => record("skipped", input),
1823
+ recordStopped: (input) => record("stopped", input),
1824
+ subscribe: (subscriber) => {
1825
+ listeners.add(subscriber);
1826
+ return () => {
1827
+ listeners.delete(subscriber);
1828
+ };
1829
+ }
1830
+ };
1831
+ };
1832
+
1833
+ // src/client/duplex.ts
1834
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1835
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1836
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
1837
+ let lastPartial = controller.partial;
1838
+ const interruptIfPlaying = (reason) => {
1839
+ if (!player.isPlaying || options.enabled === false) {
1840
+ options.monitor?.recordSkipped({
1841
+ reason,
1842
+ sessionId: controller.sessionId
1843
+ });
1844
+ return;
1845
+ }
1846
+ options.monitor?.recordRequested({
1847
+ reason,
1848
+ sessionId: controller.sessionId
1849
+ });
1850
+ player.interrupt().then(() => {
1851
+ options.monitor?.recordStopped({
1852
+ latencyMs: player.lastInterruptLatencyMs,
1853
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1854
+ reason,
1855
+ sessionId: controller.sessionId
1856
+ });
1857
+ });
1858
+ };
1859
+ const unsubscribe = controller.subscribe(() => {
1860
+ if (options.interruptOnPartial === false) {
1861
+ lastPartial = controller.partial;
1862
+ return;
1863
+ }
1864
+ if (!lastPartial && controller.partial) {
1865
+ interruptIfPlaying("partial-transcript");
1866
+ }
1867
+ lastPartial = controller.partial;
1868
+ });
1869
+ return {
1870
+ close: () => {
1871
+ unsubscribe();
1872
+ },
1873
+ handleLevel: (level) => {
1874
+ if (shouldInterruptForLevel(level, options)) {
1875
+ interruptIfPlaying("input-level");
1876
+ }
1877
+ },
1878
+ sendAudio: (audio) => {
1879
+ interruptIfPlaying("manual-audio");
1880
+ controller.sendAudio(audio);
1881
+ }
1882
+ };
1883
+ };
1884
+
1107
1885
  // src/client/htmxBootstrap.ts
1108
1886
  var VOICE_WAVE_POINTS = 48;
1109
1887
  var VOICE_WAVE_WIDTH = 320;
@@ -1126,7 +1904,7 @@ var DEFAULT_GUIDED_PROMPTS = [
1126
1904
  "Now describe what you are trying to do or test.",
1127
1905
  "Finish with any detail that feels blocked, risky, or unclear."
1128
1906
  ];
1129
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1907
+ var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
1130
1908
  var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1131
1909
  var readErrorField = (value, key) => {
1132
1910
  const candidate = value[key];
@@ -1160,6 +1938,17 @@ var formatErrorMessage = (error) => {
1160
1938
  }
1161
1939
  return "Unexpected error";
1162
1940
  };
1941
+ var formatReconnectState = (reconnect) => {
1942
+ const pieces = [reconnect.status];
1943
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
1944
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
1945
+ }
1946
+ if (reconnect.nextAttemptAt) {
1947
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
1948
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
1949
+ }
1950
+ return pieces.join(" · ");
1951
+ };
1163
1952
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1164
1953
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1165
1954
  const next = levels.slice(-(count - 1));
@@ -1216,6 +2005,17 @@ var parsePromptList = (value) => {
1216
2005
  } catch {}
1217
2006
  return DEFAULT_GUIDED_PROMPTS;
1218
2007
  };
2008
+ var parseOptionalNumber = (value) => {
2009
+ if (!value) {
2010
+ return;
2011
+ }
2012
+ const parsed = Number(value);
2013
+ return Number.isFinite(parsed) ? parsed : undefined;
2014
+ };
2015
+ var resolveElement2 = (root, selector, ctor) => {
2016
+ const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
2017
+ return value instanceof ctor ? value : null;
2018
+ };
1219
2019
  var requireElement = (root, selector, ctor, name) => {
1220
2020
  const value = selector ? document.querySelector(selector) : null;
1221
2021
  if (value instanceof ctor) {
@@ -1266,11 +2066,20 @@ var initVoiceHTMXRoot = (root) => {
1266
2066
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
2067
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
2068
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
2069
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
2070
+ const bargeInPath = root.dataset.voiceBargeInPath;
2071
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
2072
+ path: bargeInPath,
2073
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
2074
+ }) : null;
2075
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
2076
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
2077
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
2078
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
2079
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1272
2080
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1273
2081
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
2082
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1274
2083
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1275
2084
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1276
2085
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1279,35 +2088,70 @@ var initVoiceHTMXRoot = (root) => {
1279
2088
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
2089
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
2090
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
2091
+ let activeMode = null;
2092
+ let hasStartedModes = {
2093
+ general: false,
2094
+ guided: false
2095
+ };
2096
+ let isCapturing = false;
2097
+ let micError = null;
2098
+ let waveLevels = createInitialVoiceWaveLevels();
2099
+ let guidedBargeInBinding = null;
2100
+ let generalBargeInBinding = null;
1282
2101
  const guidedVoice = createVoiceController(guidedPath, {
1283
2102
  capture: {
2103
+ onAudio: (audio, sendAudio) => {
2104
+ if (guidedBargeInBinding) {
2105
+ guidedBargeInBinding.sendAudio(audio);
2106
+ return;
2107
+ }
2108
+ sendAudio(audio);
2109
+ },
1284
2110
  onLevel: (level) => {
2111
+ guidedBargeInBinding?.handleLevel(level);
1285
2112
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
2113
  renderWave();
1287
2114
  }
1288
2115
  },
2116
+ connection: {
2117
+ reconnectReportPath
2118
+ },
1289
2119
  preset: "guided-intake"
1290
2120
  });
1291
2121
  const generalVoice = createVoiceController(generalPath, {
1292
2122
  capture: {
2123
+ onAudio: (audio, sendAudio) => {
2124
+ if (generalBargeInBinding) {
2125
+ generalBargeInBinding.sendAudio(audio);
2126
+ return;
2127
+ }
2128
+ sendAudio(audio);
2129
+ },
1293
2130
  onLevel: (level) => {
2131
+ generalBargeInBinding?.handleLevel(level);
1294
2132
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
2133
  renderWave();
1296
2134
  }
1297
2135
  },
2136
+ connection: {
2137
+ reconnectReportPath
2138
+ },
1298
2139
  preset: "dictation"
1299
2140
  });
1300
2141
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
2142
  const stopGeneralBinding = generalVoice.bindHTMX({ element: syncElement });
1302
- let activeMode = null;
1303
- let hasStartedModes = {
1304
- general: false,
1305
- guided: false
1306
- };
1307
- let isCapturing = false;
1308
- let micError = null;
1309
- let waveLevels = createInitialVoiceWaveLevels();
2143
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
2144
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
2145
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
2146
+ interruptThreshold: bargeInSpeechThreshold,
2147
+ monitor: bargeInMonitor ?? undefined
2148
+ });
2149
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
2150
+ interruptThreshold: bargeInSpeechThreshold,
2151
+ monitor: bargeInMonitor ?? undefined
2152
+ });
1310
2153
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
2154
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1311
2155
  const renderWave = () => {
1312
2156
  const path = createVoiceWavePath(waveLevels);
1313
2157
  voiceWaveGlow.setAttribute("d", path);
@@ -1322,6 +2166,9 @@ var initVoiceHTMXRoot = (root) => {
1322
2166
  const status = voice.status;
1323
2167
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1324
2168
  errorStatus.textContent = micError || voice.error || "None";
2169
+ if (reconnectStatus) {
2170
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2171
+ }
1325
2172
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1326
2173
  promptStatus.textContent = resolvePromptMessage({
1327
2174
  guidedPrompts,
@@ -1385,8 +2232,18 @@ var initVoiceHTMXRoot = (root) => {
1385
2232
  render();
1386
2233
  }
1387
2234
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
2235
+ guidedVoice.subscribe(() => {
2236
+ if (guidedVoice.assistantAudio.length > 0) {
2237
+ guidedAudioPlayer.start().catch(() => {});
2238
+ }
2239
+ render();
2240
+ });
2241
+ generalVoice.subscribe(() => {
2242
+ if (generalVoice.assistantAudio.length > 0) {
2243
+ generalAudioPlayer.start().catch(() => {});
2244
+ }
2245
+ render();
2246
+ });
1390
2247
  startGuidedButton.addEventListener("click", () => {
1391
2248
  startMode("guided");
1392
2249
  });
@@ -1399,6 +2256,10 @@ var initVoiceHTMXRoot = (root) => {
1399
2256
  window.addEventListener("beforeunload", () => {
1400
2257
  guidedVoice.stopRecording();
1401
2258
  generalVoice.stopRecording();
2259
+ guidedBargeInBinding?.close();
2260
+ generalBargeInBinding?.close();
2261
+ guidedAudioPlayer.close();
2262
+ generalAudioPlayer.close();
1402
2263
  stopGuidedBinding();
1403
2264
  stopGeneralBinding();
1404
2265
  guidedVoice.close();