@absolutejs/voice 0.0.22-beta.34 → 0.0.22-beta.340

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 (242) hide show
  1. package/README.md +3361 -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 +4240 -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 +62 -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 +937 -14
  45. package/dist/client/index.d.ts +72 -0
  46. package/dist/client/index.js +6523 -50
  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 +149 -14
  89. package/dist/index.js +30457 -5611
  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 +351 -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 +611 -0
  110. package/dist/proofTrends.d.ts +325 -0
  111. package/dist/providerAdapters.d.ts +12 -1
  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 +6293 -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 +5666 -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 +136 -2
  191. package/dist/telephonyMediaRoutes.d.ts +72 -0
  192. package/dist/telephonyOutcome.d.ts +273 -0
  193. package/dist/testing/index.d.ts +1 -0
  194. package/dist/testing/index.js +5321 -153
  195. package/dist/testing/ioProviderSimulator.d.ts +41 -0
  196. package/dist/testing/telephony.d.ts +25 -0
  197. package/dist/toolContract.d.ts +161 -0
  198. package/dist/toolRuntime.d.ts +50 -0
  199. package/dist/trace.d.ts +19 -1
  200. package/dist/traceDeliveryRoutes.d.ts +86 -0
  201. package/dist/traceTimeline.d.ts +97 -0
  202. package/dist/turnLatency.d.ts +95 -0
  203. package/dist/turnQuality.d.ts +94 -0
  204. package/dist/types.d.ts +118 -3
  205. package/dist/voiceMonitoring.d.ts +444 -0
  206. package/dist/vue/VoiceDeliveryRuntime.d.ts +30 -0
  207. package/dist/vue/VoiceOpsActionCenter.d.ts +13 -0
  208. package/dist/vue/VoiceOpsStatus.d.ts +30 -0
  209. package/dist/vue/VoicePlatformCoverage.d.ts +23 -0
  210. package/dist/vue/VoiceProofTrends.d.ts +21 -0
  211. package/dist/vue/VoiceProviderCapabilities.d.ts +51 -0
  212. package/dist/vue/VoiceProviderContracts.d.ts +21 -0
  213. package/dist/vue/VoiceProviderSimulationControls.d.ts +88 -0
  214. package/dist/vue/VoiceProviderStatus.d.ts +51 -0
  215. package/dist/vue/VoiceReadinessFailures.d.ts +21 -0
  216. package/dist/vue/VoiceRoutingStatus.d.ts +51 -0
  217. package/dist/vue/VoiceTurnLatency.d.ts +69 -0
  218. package/dist/vue/VoiceTurnQuality.d.ts +51 -0
  219. package/dist/vue/index.d.ts +30 -0
  220. package/dist/vue/index.js +6062 -56
  221. package/dist/vue/useVoiceAgentSquadStatus.d.ts +9 -0
  222. package/dist/vue/useVoiceCampaignDialerProof.d.ts +11 -0
  223. package/dist/vue/useVoiceController.d.ts +2 -1
  224. package/dist/vue/useVoiceDeliveryRuntime.d.ts +13 -0
  225. package/dist/vue/useVoiceLiveOps.d.ts +9 -0
  226. package/dist/vue/useVoiceOpsActionCenter.d.ts +11 -0
  227. package/dist/vue/useVoiceOpsStatus.d.ts +9 -0
  228. package/dist/vue/useVoicePlatformCoverage.d.ts +9 -0
  229. package/dist/vue/useVoiceProofTrends.d.ts +9 -0
  230. package/dist/vue/useVoiceProviderCapabilities.d.ts +9 -0
  231. package/dist/vue/useVoiceProviderContracts.d.ts +9 -0
  232. package/dist/vue/useVoiceProviderSimulationControls.d.ts +24 -0
  233. package/dist/vue/useVoiceProviderStatus.d.ts +1 -1
  234. package/dist/vue/useVoiceReadinessFailures.d.ts +873 -0
  235. package/dist/vue/useVoiceRoutingStatus.d.ts +8 -0
  236. package/dist/vue/useVoiceStream.d.ts +2 -1
  237. package/dist/vue/useVoiceTraceTimeline.d.ts +9 -0
  238. package/dist/vue/useVoiceTurnLatency.d.ts +10 -0
  239. package/dist/vue/useVoiceTurnQuality.d.ts +9 -0
  240. package/dist/vue/useVoiceWorkflowStatus.d.ts +9 -0
  241. package/dist/workflowContract.d.ts +91 -0
  242. 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,232 @@ 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 statKey = (stat) => String(stat.id ?? stringStat(stat, "ssrc") ?? numericStat(stat, "ssrc") ?? stringStat(stat, "trackIdentifier") ?? stringStat(stat, "mid") ?? "unknown");
264
+ var secondsToMs = (value) => value === undefined ? undefined : value * 1000;
265
+ var normalizeWebRTCStat = (stat) => {
266
+ const sample = {};
267
+ for (const [key, value] of Object.entries(stat)) {
268
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
269
+ sample[key] = value;
270
+ }
271
+ }
272
+ return sample;
273
+ };
274
+ var buildMediaWebRTCStatsReport = (input = {}) => {
275
+ const stats = input.stats ?? [];
276
+ const issues = [];
277
+ const inbound = stats.filter((stat) => stat.type === "inbound-rtp" && stringStat(stat, "kind") !== "video");
278
+ const outbound = stats.filter((stat) => stat.type === "outbound-rtp" && stringStat(stat, "kind") !== "video");
279
+ const candidatePairs = stats.filter((stat) => stat.type === "candidate-pair");
280
+ const audioTracks = stats.filter((stat) => (stat.type === "track" || stat.type === "media-source") && stringStat(stat, "kind") === "audio");
281
+ const activeCandidatePairs = candidatePairs.filter((stat) => booleanStat(stat, "selected") === true || booleanStat(stat, "nominated") === true || stringStat(stat, "state") === "succeeded").length;
282
+ const liveAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") !== "ended" && stringStat(stat, "trackState") !== "ended" && booleanStat(stat, "ended") !== true).length;
283
+ const endedAudioTracks = audioTracks.filter((stat) => stringStat(stat, "readyState") === "ended" || stringStat(stat, "trackState") === "ended" || booleanStat(stat, "ended") === true).length;
284
+ const inboundPackets = inbound.reduce((total, stat) => total + (numericStat(stat, "packetsReceived") ?? 0), 0);
285
+ const outboundPackets = outbound.reduce((total, stat) => total + (numericStat(stat, "packetsSent") ?? 0), 0);
286
+ const packetsLost = [...inbound, ...outbound].reduce((total, stat) => total + Math.max(0, numericStat(stat, "packetsLost") ?? 0), 0);
287
+ const packetLossDenominator = inboundPackets + packetsLost;
288
+ const packetLossRatio = packetLossDenominator === 0 ? 0 : packetsLost / packetLossDenominator;
289
+ const bytesReceived = inbound.reduce((total, stat) => total + (numericStat(stat, "bytesReceived") ?? 0), 0);
290
+ const bytesSent = outbound.reduce((total, stat) => total + (numericStat(stat, "bytesSent") ?? 0), 0);
291
+ const roundTripTimeMs = max(candidatePairs.map((stat) => secondsToMs(numericStat(stat, "currentRoundTripTime") ?? numericStat(stat, "roundTripTime"))).filter((value) => value !== undefined));
292
+ const jitterMs = max([...inbound, ...outbound].map((stat) => secondsToMs(numericStat(stat, "jitter"))).filter((value) => value !== undefined));
293
+ const jitterBufferDelayMs = max(inbound.map((stat) => {
294
+ const delay = numericStat(stat, "jitterBufferDelay");
295
+ const emitted = numericStat(stat, "jitterBufferEmittedCount");
296
+ return delay !== undefined && emitted !== undefined && emitted > 0 ? delay / emitted * 1000 : undefined;
297
+ }).filter((value) => value !== undefined));
298
+ const audioLevels = audioTracks.map((stat) => numericStat(stat, "audioLevel")).filter((value) => value !== undefined);
299
+ if (input.requireConnectedCandidatePair && candidatePairs.length > 0 && activeCandidatePairs === 0) {
300
+ pushIssue(issues, "error", "media.webrtc_candidate_pair_missing", "No active WebRTC candidate pair was observed.");
301
+ }
302
+ if (input.requireLiveAudioTrack && liveAudioTracks === 0) {
303
+ pushIssue(issues, "error", "media.webrtc_audio_track_missing", "No live WebRTC audio track was observed.");
304
+ }
305
+ if (input.maxPacketLossRatio !== undefined && packetLossRatio > input.maxPacketLossRatio) {
306
+ pushIssue(issues, "warning", "media.webrtc_packet_loss", `Observed WebRTC packet loss ratio ${String(packetLossRatio)} above ${String(input.maxPacketLossRatio)}.`);
307
+ }
308
+ if (input.maxRoundTripTimeMs !== undefined && roundTripTimeMs !== undefined && roundTripTimeMs > input.maxRoundTripTimeMs) {
309
+ pushIssue(issues, "warning", "media.webrtc_round_trip_time", `Observed WebRTC RTT ${String(roundTripTimeMs)}ms above ${String(input.maxRoundTripTimeMs)}ms.`);
310
+ }
311
+ if (input.maxJitterMs !== undefined && jitterMs !== undefined && jitterMs > input.maxJitterMs) {
312
+ pushIssue(issues, "warning", "media.webrtc_jitter", `Observed WebRTC jitter ${String(jitterMs)}ms above ${String(input.maxJitterMs)}ms.`);
313
+ }
314
+ return {
315
+ activeCandidatePairs,
316
+ audioLevelAverage: average(audioLevels),
317
+ bytesReceived,
318
+ bytesSent,
319
+ checkedAt: Date.now(),
320
+ endedAudioTracks,
321
+ inboundPackets,
322
+ issues,
323
+ jitterBufferDelayMs,
324
+ jitterMs,
325
+ liveAudioTracks,
326
+ outboundPackets,
327
+ packetLossRatio,
328
+ packetsLost,
329
+ roundTripTimeMs,
330
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
331
+ totalStats: stats.length
332
+ };
333
+ };
334
+ var collectMediaWebRTCStats = async (input) => {
335
+ const report = await input.peerConnection.getStats(input.selector ?? null);
336
+ return [...report.values()].map(normalizeWebRTCStat);
337
+ };
338
+ var buildMediaWebRTCStreamContinuityReport = (input = {}) => {
339
+ const stats = input.stats ?? [];
340
+ const previousStats = input.previousStats ?? [];
341
+ const issues = [];
342
+ const previousByKey = new Map(previousStats.map((stat) => [statKey(stat), stat]));
343
+ const audioRtp = stats.filter((stat) => (stat.type === "inbound-rtp" || stat.type === "outbound-rtp") && stringStat(stat, "kind") !== "video" && stringStat(stat, "mediaType") !== "video");
344
+ const streams = audioRtp.map((stat) => {
345
+ const direction = stat.type === "outbound-rtp" ? "outbound" : "inbound";
346
+ const packetsKey = direction === "outbound" ? "packetsSent" : "packetsReceived";
347
+ const bytesKey = direction === "outbound" ? "bytesSent" : "bytesReceived";
348
+ const previous = previousByKey.get(statKey(stat));
349
+ const currentPackets = numericStat(stat, packetsKey);
350
+ const previousPackets = previous ? numericStat(previous, packetsKey) : undefined;
351
+ const currentBytes = numericStat(stat, bytesKey);
352
+ const previousBytes = previous ? numericStat(previous, bytesKey) : undefined;
353
+ const timeDeltaMs = stat.timestamp !== undefined && previous?.timestamp !== undefined ? stat.timestamp - previous.timestamp : undefined;
354
+ return {
355
+ bytesDelta: currentBytes !== undefined && previousBytes !== undefined ? currentBytes - previousBytes : undefined,
356
+ currentPackets,
357
+ direction,
358
+ id: statKey(stat),
359
+ packetDelta: currentPackets !== undefined && previousPackets !== undefined ? currentPackets - previousPackets : undefined,
360
+ previousPackets,
361
+ timeDeltaMs
362
+ };
363
+ });
364
+ const inbound = streams.filter((stream) => stream.direction === "inbound");
365
+ const outbound = streams.filter((stream) => stream.direction === "outbound");
366
+ const maxObservedGapMs = max(streams.map((stream) => stream.timeDeltaMs).filter((value) => value !== undefined));
367
+ const stalledInboundStreams = inbound.filter((stream) => input.maxInboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxInboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
368
+ const stalledOutboundStreams = outbound.filter((stream) => input.maxOutboundPacketStallMs !== undefined && stream.timeDeltaMs !== undefined && stream.timeDeltaMs >= input.maxOutboundPacketStallMs && stream.packetDelta !== undefined && stream.packetDelta <= 0).length;
369
+ if (input.requireInboundAudio && inbound.length === 0) {
370
+ pushIssue(issues, "error", "media.webrtc_inbound_audio_missing", "No inbound WebRTC audio RTP stream was observed.");
371
+ }
372
+ if (input.requireOutboundAudio && outbound.length === 0) {
373
+ pushIssue(issues, "error", "media.webrtc_outbound_audio_missing", "No outbound WebRTC audio RTP stream was observed.");
374
+ }
375
+ if (input.maxGapMs !== undefined && maxObservedGapMs !== undefined && maxObservedGapMs > input.maxGapMs) {
376
+ pushIssue(issues, "warning", "media.webrtc_stream_gap", `Observed WebRTC stream sample gap ${String(maxObservedGapMs)}ms above ${String(input.maxGapMs)}ms.`);
377
+ }
378
+ if (stalledInboundStreams > 0) {
379
+ pushIssue(issues, "error", "media.webrtc_inbound_stalled", `${String(stalledInboundStreams)} inbound WebRTC audio stream(s) stopped receiving packets.`);
380
+ }
381
+ if (stalledOutboundStreams > 0) {
382
+ pushIssue(issues, "error", "media.webrtc_outbound_stalled", `${String(stalledOutboundStreams)} outbound WebRTC audio stream(s) stopped sending packets.`);
383
+ }
384
+ return {
385
+ checkedAt: Date.now(),
386
+ inboundAudioStreams: inbound.length,
387
+ issues,
388
+ maxObservedGapMs,
389
+ outboundAudioStreams: outbound.length,
390
+ stalledInboundStreams,
391
+ stalledOutboundStreams,
392
+ status: issues.some((issue) => issue.severity === "error") ? "fail" : issues.length > 0 ? "warn" : "pass",
393
+ streams,
394
+ totalStats: stats.length
395
+ };
396
+ };
397
+
398
+ // src/client/browserMedia.ts
399
+ var DEFAULT_BROWSER_MEDIA_PATH = "/api/voice/browser-media";
400
+ var DEFAULT_BROWSER_MEDIA_INTERVAL_MS = 5000;
401
+ var resolvePeerConnection = async (options) => options.peerConnection ?? await options.getPeerConnection?.() ?? null;
402
+ var postBrowserMediaReport = async (payload, options) => {
403
+ const requestFetch = options.fetch ?? globalThis.fetch;
404
+ if (!requestFetch) {
405
+ return;
406
+ }
407
+ await requestFetch(options.path ?? DEFAULT_BROWSER_MEDIA_PATH, {
408
+ body: JSON.stringify(payload),
409
+ headers: {
410
+ "Content-Type": "application/json"
411
+ },
412
+ keepalive: true,
413
+ method: "POST"
414
+ });
415
+ };
416
+ var createVoiceBrowserMediaReporter = (options) => {
417
+ let interval = null;
418
+ let previousStats = [];
419
+ const reportOnce = async () => {
420
+ const peerConnection = await resolvePeerConnection(options);
421
+ if (!peerConnection) {
422
+ return;
423
+ }
424
+ const stats = await collectMediaWebRTCStats({ peerConnection });
425
+ const report = buildMediaWebRTCStatsReport({
426
+ ...options,
427
+ stats
428
+ });
429
+ const continuity = options.continuity === false ? undefined : buildMediaWebRTCStreamContinuityReport({
430
+ ...options.continuity,
431
+ previousStats,
432
+ stats
433
+ });
434
+ const payload = {
435
+ at: Date.now(),
436
+ continuity,
437
+ report,
438
+ scenarioId: options.getScenarioId?.() ?? null,
439
+ sessionId: options.getSessionId?.() ?? null
440
+ };
441
+ previousStats = stats;
442
+ options.onReport?.(payload);
443
+ await postBrowserMediaReport(payload, options);
444
+ return payload;
445
+ };
446
+ const run = () => {
447
+ reportOnce().catch((error) => {
448
+ options.onError?.(error);
449
+ });
450
+ };
451
+ const stop = () => {
452
+ if (interval) {
453
+ clearInterval(interval);
454
+ interval = null;
455
+ }
456
+ };
457
+ return {
458
+ close: stop,
459
+ reportOnce,
460
+ start: () => {
461
+ if (interval) {
462
+ return;
463
+ }
464
+ run();
465
+ interval = setInterval(run, options.intervalMs ?? DEFAULT_BROWSER_MEDIA_INTERVAL_MS);
466
+ },
467
+ stop
468
+ };
469
+ };
470
+
229
471
  // src/client/connection.ts
230
472
  var WS_OPEN = 1;
231
473
  var WS_CLOSED = 3;
@@ -269,10 +511,12 @@ var isVoiceServerMessage = (value) => {
269
511
  case "assistant":
270
512
  case "call_lifecycle":
271
513
  case "complete":
514
+ case "connection":
272
515
  case "error":
273
516
  case "final":
274
517
  case "partial":
275
518
  case "pong":
519
+ case "replay":
276
520
  case "session":
277
521
  case "turn":
278
522
  return true;
@@ -309,6 +553,9 @@ var createVoiceConnection = (path, options = {}) => {
309
553
  sessionId: options.sessionId ?? createSessionId(),
310
554
  ws: null
311
555
  };
556
+ const emitConnection = (reconnect) => {
557
+ listeners.forEach((listener) => listener(reconnect));
558
+ };
312
559
  const clearTimers = () => {
313
560
  if (state.pingInterval) {
314
561
  clearInterval(state.pingInterval);
@@ -331,9 +578,28 @@ var createVoiceConnection = (path, options = {}) => {
331
578
  }
332
579
  };
333
580
  const scheduleReconnect = () => {
581
+ const nextAttemptAt = Date.now() + RECONNECT_DELAY_MS;
334
582
  state.reconnectAttempts += 1;
583
+ emitConnection({
584
+ reconnect: {
585
+ attempts: state.reconnectAttempts,
586
+ lastDisconnectAt: Date.now(),
587
+ maxAttempts: maxReconnectAttempts,
588
+ nextAttemptAt,
589
+ status: "reconnecting"
590
+ },
591
+ type: "connection"
592
+ });
335
593
  state.reconnectTimeout = setTimeout(() => {
336
594
  if (state.reconnectAttempts > maxReconnectAttempts) {
595
+ emitConnection({
596
+ reconnect: {
597
+ attempts: state.reconnectAttempts,
598
+ maxAttempts: maxReconnectAttempts,
599
+ status: "exhausted"
600
+ },
601
+ type: "connection"
602
+ });
337
603
  return;
338
604
  }
339
605
  connect();
@@ -343,9 +609,21 @@ var createVoiceConnection = (path, options = {}) => {
343
609
  const ws = new WebSocket(buildWsUrl(path, state.sessionId, state.scenarioId));
344
610
  ws.binaryType = "arraybuffer";
345
611
  ws.onopen = () => {
612
+ const wasReconnecting = state.reconnectAttempts > 0;
346
613
  state.isConnected = true;
347
- state.reconnectAttempts = 0;
348
614
  flushPendingMessages();
615
+ if (wasReconnecting) {
616
+ emitConnection({
617
+ reconnect: {
618
+ attempts: state.reconnectAttempts,
619
+ lastResumedAt: Date.now(),
620
+ maxAttempts: maxReconnectAttempts,
621
+ status: "resumed"
622
+ },
623
+ type: "connection"
624
+ });
625
+ state.reconnectAttempts = 0;
626
+ }
349
627
  listeners.forEach((listener) => listener({
350
628
  scenarioId: state.scenarioId ?? undefined,
351
629
  sessionId: state.sessionId,
@@ -375,6 +653,16 @@ var createVoiceConnection = (path, options = {}) => {
375
653
  const reconnectable = shouldReconnect && event.code !== WS_NORMAL_CLOSURE && state.reconnectAttempts < maxReconnectAttempts;
376
654
  if (reconnectable) {
377
655
  scheduleReconnect();
656
+ } else if (shouldReconnect && event.code !== WS_NORMAL_CLOSURE) {
657
+ emitConnection({
658
+ reconnect: {
659
+ attempts: state.reconnectAttempts,
660
+ lastDisconnectAt: Date.now(),
661
+ maxAttempts: maxReconnectAttempts,
662
+ status: "exhausted"
663
+ },
664
+ type: "connection"
665
+ });
378
666
  }
379
667
  };
380
668
  state.ws = ws;
@@ -445,6 +733,11 @@ var createVoiceConnection = (path, options = {}) => {
445
733
  };
446
734
 
447
735
  // src/client/store.ts
736
+ var createInitialReconnectState = () => ({
737
+ attempts: 0,
738
+ maxAttempts: 0,
739
+ status: "idle"
740
+ });
448
741
  var createInitialState = () => ({
449
742
  assistantAudio: [],
450
743
  assistantTexts: [],
@@ -453,6 +746,7 @@ var createInitialState = () => ({
453
746
  isConnected: false,
454
747
  scenarioId: null,
455
748
  partial: "",
749
+ reconnect: createInitialReconnectState(),
456
750
  sessionId: null,
457
751
  status: "idle",
458
752
  turns: []
@@ -509,7 +803,19 @@ var createVoiceStreamStore = () => {
509
803
  case "connected":
510
804
  state = {
511
805
  ...state,
512
- isConnected: true
806
+ isConnected: true,
807
+ reconnect: state.reconnect.status === "reconnecting" ? {
808
+ ...state.reconnect,
809
+ lastResumedAt: Date.now(),
810
+ nextAttemptAt: undefined,
811
+ status: "resumed"
812
+ } : state.reconnect
813
+ };
814
+ break;
815
+ case "connection":
816
+ state = {
817
+ ...state,
818
+ reconnect: action.reconnect
513
819
  };
514
820
  break;
515
821
  case "disconnected":
@@ -537,6 +843,26 @@ var createVoiceStreamStore = () => {
537
843
  partial: action.transcript.text
538
844
  };
539
845
  break;
846
+ case "replay":
847
+ state = {
848
+ ...state,
849
+ assistantTexts: [...action.assistantTexts],
850
+ call: action.call ?? null,
851
+ error: null,
852
+ isConnected: action.status === "active",
853
+ partial: action.partial,
854
+ reconnect: state.reconnect.status === "reconnecting" ? {
855
+ ...state.reconnect,
856
+ lastResumedAt: Date.now(),
857
+ nextAttemptAt: undefined,
858
+ status: "resumed"
859
+ } : state.reconnect,
860
+ scenarioId: action.scenarioId ?? state.scenarioId,
861
+ sessionId: action.sessionId,
862
+ status: action.status,
863
+ turns: [...action.turns]
864
+ };
865
+ break;
540
866
  case "session":
541
867
  state = {
542
868
  ...state,
@@ -574,20 +900,50 @@ var createVoiceStreamStore = () => {
574
900
  var createVoiceStream = (path, options = {}) => {
575
901
  const connection = createVoiceConnection(path, options);
576
902
  const store = createVoiceStreamStore();
903
+ const browserMediaReporter = options.browserMedia && typeof window !== "undefined" ? createVoiceBrowserMediaReporter({
904
+ ...options.browserMedia,
905
+ getScenarioId: () => options.browserMedia ? options.browserMedia.getScenarioId?.() ?? connection.getScenarioId() : connection.getScenarioId(),
906
+ getSessionId: () => options.browserMedia ? options.browserMedia.getSessionId?.() ?? connection.getSessionId() : connection.getSessionId()
907
+ }) : null;
577
908
  const subscribers = new Set;
578
909
  const start = (input) => Promise.resolve().then(() => {
579
910
  if (!input?.sessionId && !input?.scenarioId) {
580
911
  return;
581
912
  }
582
913
  connection.start(input);
914
+ browserMediaReporter?.start();
583
915
  });
584
916
  const notify = () => {
585
917
  subscribers.forEach((subscriber) => subscriber());
586
918
  };
919
+ const reportReconnect = () => {
920
+ if (!options.reconnectReportPath || typeof fetch === "undefined") {
921
+ return;
922
+ }
923
+ const snapshot = store.getSnapshot();
924
+ const body = JSON.stringify({
925
+ at: Date.now(),
926
+ reconnect: snapshot.reconnect,
927
+ scenarioId: snapshot.scenarioId,
928
+ sessionId: connection.getSessionId(),
929
+ turnIds: snapshot.turns.map((turn) => turn.id)
930
+ });
931
+ fetch(options.reconnectReportPath, {
932
+ body,
933
+ headers: {
934
+ "Content-Type": "application/json"
935
+ },
936
+ keepalive: true,
937
+ method: "POST"
938
+ }).catch(() => {});
939
+ };
587
940
  const unsubscribeConnection = connection.subscribe((message) => {
588
941
  const action = serverMessageToAction(message);
589
942
  if (action) {
590
943
  store.dispatch(action);
944
+ if (message.type === "connection") {
945
+ reportReconnect();
946
+ }
591
947
  notify();
592
948
  }
593
949
  });
@@ -597,6 +953,7 @@ var createVoiceStream = (path, options = {}) => {
597
953
  },
598
954
  close() {
599
955
  unsubscribeConnection();
956
+ browserMediaReporter?.close();
600
957
  connection.close();
601
958
  store.dispatch({ type: "disconnected" });
602
959
  notify();
@@ -623,6 +980,9 @@ var createVoiceStream = (path, options = {}) => {
623
980
  get partial() {
624
981
  return store.getSnapshot().partial;
625
982
  },
983
+ get reconnect() {
984
+ return store.getSnapshot().reconnect;
985
+ },
626
986
  get sessionId() {
627
987
  return connection.getSessionId();
628
988
  },
@@ -941,6 +1301,7 @@ var createInitialState2 = (stream) => ({
941
1301
  isConnected: stream.isConnected,
942
1302
  isRecording: false,
943
1303
  partial: stream.partial,
1304
+ reconnect: stream.reconnect,
944
1305
  recordingError: null,
945
1306
  sessionId: stream.sessionId,
946
1307
  scenarioId: stream.scenarioId,
@@ -970,6 +1331,7 @@ var createVoiceController = (path, options = {}) => {
970
1331
  error: stream.error,
971
1332
  isConnected: stream.isConnected,
972
1333
  partial: stream.partial,
1334
+ reconnect: stream.reconnect,
973
1335
  sessionId: stream.sessionId,
974
1336
  scenarioId: stream.scenarioId,
975
1337
  status: stream.status,
@@ -994,7 +1356,13 @@ var createVoiceController = (path, options = {}) => {
994
1356
  capture = createMicrophoneCapture({
995
1357
  channelCount: options.capture?.channelCount ?? preset.capture.channelCount,
996
1358
  onLevel: options.capture?.onLevel,
997
- onAudio: (audio) => stream.sendAudio(audio),
1359
+ onAudio: (audio) => {
1360
+ if (options.capture?.onAudio) {
1361
+ options.capture.onAudio(audio, stream.sendAudio);
1362
+ return;
1363
+ }
1364
+ stream.sendAudio(audio);
1365
+ },
998
1366
  sampleRateHz: options.capture?.sampleRateHz ?? preset.capture.sampleRateHz
999
1367
  });
1000
1368
  return capture;
@@ -1064,6 +1432,9 @@ var createVoiceController = (path, options = {}) => {
1064
1432
  get recordingError() {
1065
1433
  return state.recordingError;
1066
1434
  },
1435
+ get reconnect() {
1436
+ return state.reconnect;
1437
+ },
1067
1438
  sendAudio: (audio) => stream.sendAudio(audio),
1068
1439
  get sessionId() {
1069
1440
  return state.sessionId;
@@ -1104,6 +1475,475 @@ var createVoiceController = (path, options = {}) => {
1104
1475
  };
1105
1476
  };
1106
1477
 
1478
+ // src/client/audioPlayer.ts
1479
+ var DEFAULT_LOOKAHEAD_MS = 15;
1480
+ var createInitialState3 = () => ({
1481
+ activeSourceCount: 0,
1482
+ error: null,
1483
+ isActive: false,
1484
+ isPlaying: false,
1485
+ lastInterruptLatencyMs: undefined,
1486
+ lastPlaybackStopLatencyMs: undefined,
1487
+ processedChunkCount: 0,
1488
+ queuedChunkCount: 0
1489
+ });
1490
+ var getAudioContextCtor = () => {
1491
+ if (typeof window === "undefined") {
1492
+ return typeof AudioContext === "undefined" ? undefined : AudioContext;
1493
+ }
1494
+ return window.AudioContext ?? window.webkitAudioContext;
1495
+ };
1496
+ var decodePCM16LEChunk = (audioContext, chunk) => {
1497
+ const format = chunk.format;
1498
+ if (format.container !== "raw" || format.encoding !== "pcm_s16le") {
1499
+ throw new Error(`Unsupported assistant audio format: ${format.container}/${format.encoding}`);
1500
+ }
1501
+ const bytes = chunk.chunk;
1502
+ const channels = Math.max(1, format.channels);
1503
+ const sampleCount = Math.floor(bytes.byteLength / 2);
1504
+ const frameCount = Math.max(1, Math.floor(sampleCount / channels));
1505
+ const audioBuffer = audioContext.createBuffer(channels, frameCount, format.sampleRateHz);
1506
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1507
+ for (let channelIndex = 0;channelIndex < channels; channelIndex += 1) {
1508
+ const channelData = audioBuffer.getChannelData(channelIndex);
1509
+ for (let frameIndex = 0;frameIndex < frameCount; frameIndex += 1) {
1510
+ const sampleIndex = frameIndex * channels + channelIndex;
1511
+ const sampleOffset = sampleIndex * 2;
1512
+ if (sampleOffset + 1 >= bytes.byteLength) {
1513
+ channelData[frameIndex] = 0;
1514
+ continue;
1515
+ }
1516
+ channelData[frameIndex] = view.getInt16(sampleOffset, true) / 32768;
1517
+ }
1518
+ }
1519
+ return audioBuffer;
1520
+ };
1521
+ var createVoiceAudioPlayer = (source, options = {}) => {
1522
+ const subscribers = new Set;
1523
+ const sourceNodes = new Set;
1524
+ const lookaheadSeconds = (options.lookaheadMs ?? DEFAULT_LOOKAHEAD_MS) / 1000;
1525
+ let state = createInitialState3();
1526
+ let audioContext = null;
1527
+ let outputNode = null;
1528
+ let queueEndTime = 0;
1529
+ let syncPromise = Promise.resolve();
1530
+ let interruptStartedAt = null;
1531
+ let interruptPromise = null;
1532
+ let resolveInterruptPromise = null;
1533
+ let interruptFallbackTimer = null;
1534
+ const notify = () => {
1535
+ for (const subscriber of subscribers) {
1536
+ subscriber();
1537
+ }
1538
+ };
1539
+ const setState = (next) => {
1540
+ state = {
1541
+ ...state,
1542
+ ...next
1543
+ };
1544
+ notify();
1545
+ };
1546
+ const clearError = () => {
1547
+ if (state.error !== null) {
1548
+ setState({ error: null });
1549
+ }
1550
+ };
1551
+ const clearInterruptTimer = () => {
1552
+ if (interruptFallbackTimer !== null) {
1553
+ clearTimeout(interruptFallbackTimer);
1554
+ interruptFallbackTimer = null;
1555
+ }
1556
+ };
1557
+ const resolveInterrupt = (latencyMs) => {
1558
+ clearInterruptTimer();
1559
+ interruptStartedAt = null;
1560
+ setState({
1561
+ activeSourceCount: sourceNodes.size,
1562
+ isPlaying: false,
1563
+ lastInterruptLatencyMs: latencyMs,
1564
+ lastPlaybackStopLatencyMs: state.lastPlaybackStopLatencyMs ?? latencyMs
1565
+ });
1566
+ resolveInterruptPromise?.();
1567
+ resolveInterruptPromise = null;
1568
+ interruptPromise = null;
1569
+ };
1570
+ const estimateOutputStopLatencyMs = (context) => {
1571
+ if (!context) {
1572
+ return 0;
1573
+ }
1574
+ return Math.max(0, ((context.baseLatency ?? 0) + (context.outputLatency ?? 0)) * 1000);
1575
+ };
1576
+ const restoreOutputGain = (context) => {
1577
+ if (!outputNode) {
1578
+ return;
1579
+ }
1580
+ const gainValue = 1;
1581
+ if (outputNode.gain.setValueAtTime) {
1582
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1583
+ return;
1584
+ }
1585
+ outputNode.gain.value = gainValue;
1586
+ };
1587
+ const muteOutputGain = (context) => {
1588
+ if (!outputNode) {
1589
+ return;
1590
+ }
1591
+ const gainValue = 0;
1592
+ if (outputNode.gain.setValueAtTime) {
1593
+ outputNode.gain.setValueAtTime(gainValue, context?.currentTime ?? 0);
1594
+ return;
1595
+ }
1596
+ outputNode.gain.value = gainValue;
1597
+ };
1598
+ const maybeResolveInterrupt = () => {
1599
+ if (interruptStartedAt === null || sourceNodes.size > 0) {
1600
+ return;
1601
+ }
1602
+ resolveInterrupt(Date.now() - interruptStartedAt);
1603
+ };
1604
+ const ensureAudioContext = async () => {
1605
+ if (audioContext) {
1606
+ return audioContext;
1607
+ }
1608
+ if (options.createAudioContext) {
1609
+ audioContext = options.createAudioContext();
1610
+ } else {
1611
+ const AudioContextCtor = getAudioContextCtor();
1612
+ if (!AudioContextCtor) {
1613
+ throw new Error("Assistant audio playback requires AudioContext support.");
1614
+ }
1615
+ audioContext = new AudioContextCtor;
1616
+ }
1617
+ if (audioContext.createGain) {
1618
+ outputNode = audioContext.createGain();
1619
+ outputNode.connect?.(audioContext.destination);
1620
+ }
1621
+ queueEndTime = audioContext.currentTime;
1622
+ return audioContext;
1623
+ };
1624
+ const scheduleChunk = async (chunk) => {
1625
+ const context = await ensureAudioContext();
1626
+ const buffer = decodePCM16LEChunk(context, chunk);
1627
+ const node = context.createBufferSource();
1628
+ node.buffer = buffer;
1629
+ node.connect(outputNode ?? context.destination);
1630
+ node.onended = () => {
1631
+ sourceNodes.delete(node);
1632
+ node.disconnect?.();
1633
+ setState({
1634
+ activeSourceCount: sourceNodes.size,
1635
+ isPlaying: sourceNodes.size > 0 && state.isActive
1636
+ });
1637
+ maybeResolveInterrupt();
1638
+ };
1639
+ const startAt = Math.max(context.currentTime + lookaheadSeconds, queueEndTime);
1640
+ queueEndTime = startAt + buffer.duration;
1641
+ sourceNodes.add(node);
1642
+ setState({
1643
+ activeSourceCount: sourceNodes.size,
1644
+ isPlaying: true
1645
+ });
1646
+ node.start(startAt);
1647
+ };
1648
+ const stopQueuedPlayback = (options2) => {
1649
+ for (const node of [...sourceNodes]) {
1650
+ node.stop?.();
1651
+ }
1652
+ queueEndTime = audioContext ? audioContext.currentTime : 0;
1653
+ if (options2?.forceClear) {
1654
+ for (const node of sourceNodes) {
1655
+ node.disconnect?.();
1656
+ }
1657
+ sourceNodes.clear();
1658
+ maybeResolveInterrupt();
1659
+ }
1660
+ };
1661
+ const sync = async () => {
1662
+ if (!state.isActive) {
1663
+ return;
1664
+ }
1665
+ const nextChunks = source.assistantAudio.slice(state.processedChunkCount);
1666
+ if (nextChunks.length === 0) {
1667
+ return;
1668
+ }
1669
+ try {
1670
+ clearError();
1671
+ for (const chunk of nextChunks) {
1672
+ await scheduleChunk(chunk);
1673
+ }
1674
+ setState({
1675
+ processedChunkCount: source.assistantAudio.length,
1676
+ queuedChunkCount: state.queuedChunkCount + nextChunks.length
1677
+ });
1678
+ } catch (error) {
1679
+ setState({
1680
+ error: error instanceof Error ? error.message : String(error)
1681
+ });
1682
+ }
1683
+ };
1684
+ const queueSync = () => {
1685
+ syncPromise = syncPromise.then(() => sync(), () => sync());
1686
+ return syncPromise;
1687
+ };
1688
+ const unsubscribeSource = source.subscribe(() => {
1689
+ if (options.autoStart && !state.isActive && source.assistantAudio.length > 0) {
1690
+ player.start();
1691
+ return;
1692
+ }
1693
+ if (state.isActive) {
1694
+ queueSync();
1695
+ }
1696
+ });
1697
+ const player = {
1698
+ close: async () => {
1699
+ unsubscribeSource();
1700
+ stopQueuedPlayback({ forceClear: true });
1701
+ clearInterruptTimer();
1702
+ resolveInterruptPromise?.();
1703
+ resolveInterruptPromise = null;
1704
+ interruptPromise = null;
1705
+ interruptStartedAt = null;
1706
+ if (audioContext && audioContext.state !== "closed") {
1707
+ await audioContext.close();
1708
+ }
1709
+ audioContext = null;
1710
+ outputNode?.disconnect?.();
1711
+ outputNode = null;
1712
+ queueEndTime = 0;
1713
+ setState({
1714
+ activeSourceCount: 0,
1715
+ isActive: false,
1716
+ isPlaying: false
1717
+ });
1718
+ },
1719
+ get activeSourceCount() {
1720
+ return state.activeSourceCount;
1721
+ },
1722
+ get error() {
1723
+ return state.error;
1724
+ },
1725
+ getSnapshot: () => state,
1726
+ get isActive() {
1727
+ return state.isActive;
1728
+ },
1729
+ get isPlaying() {
1730
+ return state.isPlaying;
1731
+ },
1732
+ interrupt: async () => {
1733
+ const startedAt = Date.now();
1734
+ const context = await ensureAudioContext();
1735
+ interruptStartedAt = startedAt;
1736
+ muteOutputGain(context);
1737
+ const playbackStopLatencyMs = Date.now() - startedAt + estimateOutputStopLatencyMs(context);
1738
+ setState({
1739
+ isActive: false,
1740
+ isPlaying: sourceNodes.size > 0,
1741
+ lastPlaybackStopLatencyMs: playbackStopLatencyMs
1742
+ });
1743
+ if (sourceNodes.size === 0) {
1744
+ resolveInterrupt(playbackStopLatencyMs);
1745
+ return;
1746
+ }
1747
+ if (!interruptPromise) {
1748
+ interruptPromise = new Promise((resolve) => {
1749
+ resolveInterruptPromise = resolve;
1750
+ });
1751
+ }
1752
+ clearInterruptTimer();
1753
+ interruptFallbackTimer = setTimeout(() => {
1754
+ for (const node of sourceNodes) {
1755
+ node.disconnect?.();
1756
+ }
1757
+ sourceNodes.clear();
1758
+ resolveInterrupt(Date.now() - startedAt);
1759
+ }, 250);
1760
+ stopQueuedPlayback();
1761
+ await interruptPromise;
1762
+ },
1763
+ get lastInterruptLatencyMs() {
1764
+ return state.lastInterruptLatencyMs;
1765
+ },
1766
+ get lastPlaybackStopLatencyMs() {
1767
+ return state.lastPlaybackStopLatencyMs;
1768
+ },
1769
+ pause: async () => {
1770
+ if (!audioContext) {
1771
+ setState({
1772
+ activeSourceCount: 0,
1773
+ isActive: false,
1774
+ isPlaying: false
1775
+ });
1776
+ return;
1777
+ }
1778
+ await audioContext.suspend();
1779
+ setState({
1780
+ activeSourceCount: sourceNodes.size,
1781
+ isActive: false,
1782
+ isPlaying: false
1783
+ });
1784
+ },
1785
+ get processedChunkCount() {
1786
+ return state.processedChunkCount;
1787
+ },
1788
+ get queuedChunkCount() {
1789
+ return state.queuedChunkCount;
1790
+ },
1791
+ start: async () => {
1792
+ try {
1793
+ clearError();
1794
+ const context = await ensureAudioContext();
1795
+ restoreOutputGain(context);
1796
+ if (context.state === "suspended") {
1797
+ await context.resume();
1798
+ }
1799
+ setState({
1800
+ activeSourceCount: sourceNodes.size,
1801
+ isActive: true,
1802
+ isPlaying: context.state === "running"
1803
+ });
1804
+ await queueSync();
1805
+ } catch (error) {
1806
+ setState({
1807
+ error: error instanceof Error ? error.message : String(error),
1808
+ isActive: false,
1809
+ isPlaying: false
1810
+ });
1811
+ throw error;
1812
+ }
1813
+ },
1814
+ subscribe: (subscriber) => {
1815
+ subscribers.add(subscriber);
1816
+ return () => {
1817
+ subscribers.delete(subscriber);
1818
+ };
1819
+ }
1820
+ };
1821
+ return player;
1822
+ };
1823
+
1824
+ // src/client/bargeInMonitor.ts
1825
+ var DEFAULT_THRESHOLD_MS = 250;
1826
+ var createEventId = () => `barge-in:${Date.now()}:${crypto.randomUUID?.() ?? Math.random().toString(36).slice(2)}`;
1827
+ var summarize = (events, thresholdMs) => {
1828
+ const stopped = events.filter((event) => event.status === "stopped");
1829
+ const latencies = stopped.map((event) => event.latencyMs).filter((value) => typeof value === "number");
1830
+ const failed = stopped.filter((event) => typeof event.latencyMs === "number" && event.latencyMs > thresholdMs).length;
1831
+ const passed = stopped.length - failed;
1832
+ return {
1833
+ averageLatencyMs: latencies.length > 0 ? Math.round(latencies.reduce((total, value) => total + value, 0) / latencies.length) : undefined,
1834
+ events: [...events],
1835
+ failed,
1836
+ lastEvent: events.at(-1),
1837
+ passed,
1838
+ status: events.length === 0 ? "empty" : failed > 0 ? "fail" : stopped.length === 0 ? "warn" : "pass",
1839
+ thresholdMs,
1840
+ total: stopped.length
1841
+ };
1842
+ };
1843
+ var createVoiceBargeInMonitor = (options = {}) => {
1844
+ const listeners = new Set;
1845
+ const thresholdMs = options.thresholdMs ?? DEFAULT_THRESHOLD_MS;
1846
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1847
+ const events = [];
1848
+ const emit = () => {
1849
+ for (const listener of listeners) {
1850
+ listener();
1851
+ }
1852
+ };
1853
+ const postEvent = (event) => {
1854
+ if (!options.path || typeof fetchImpl !== "function") {
1855
+ return;
1856
+ }
1857
+ fetchImpl(options.path, {
1858
+ body: JSON.stringify(event),
1859
+ headers: {
1860
+ "Content-Type": "application/json"
1861
+ },
1862
+ method: "POST"
1863
+ }).catch(() => {});
1864
+ };
1865
+ const record = (status, input) => {
1866
+ const event = {
1867
+ at: Date.now(),
1868
+ id: createEventId(),
1869
+ latencyMs: input.latencyMs,
1870
+ playbackStopLatencyMs: input.playbackStopLatencyMs,
1871
+ reason: input.reason,
1872
+ sessionId: input.sessionId,
1873
+ status,
1874
+ thresholdMs
1875
+ };
1876
+ events.push(event);
1877
+ postEvent(event);
1878
+ emit();
1879
+ return event;
1880
+ };
1881
+ return {
1882
+ getSnapshot: () => summarize(events, thresholdMs),
1883
+ recordRequested: (input) => record("requested", input),
1884
+ recordSkipped: (input) => record("skipped", input),
1885
+ recordStopped: (input) => record("stopped", input),
1886
+ subscribe: (subscriber) => {
1887
+ listeners.add(subscriber);
1888
+ return () => {
1889
+ listeners.delete(subscriber);
1890
+ };
1891
+ }
1892
+ };
1893
+ };
1894
+
1895
+ // src/client/duplex.ts
1896
+ var DEFAULT_INTERRUPT_THRESHOLD = 0.08;
1897
+ var shouldInterruptForLevel = (level, options = {}) => (options.enabled ?? true) && level >= (options.interruptThreshold ?? DEFAULT_INTERRUPT_THRESHOLD);
1898
+ var bindVoiceBargeIn = (controller, player, options = {}) => {
1899
+ let lastPartial = controller.partial;
1900
+ const interruptIfPlaying = (reason) => {
1901
+ if (!player.isPlaying || options.enabled === false) {
1902
+ options.monitor?.recordSkipped({
1903
+ reason,
1904
+ sessionId: controller.sessionId
1905
+ });
1906
+ return;
1907
+ }
1908
+ options.monitor?.recordRequested({
1909
+ reason,
1910
+ sessionId: controller.sessionId
1911
+ });
1912
+ player.interrupt().then(() => {
1913
+ options.monitor?.recordStopped({
1914
+ latencyMs: player.lastInterruptLatencyMs,
1915
+ playbackStopLatencyMs: player.lastPlaybackStopLatencyMs,
1916
+ reason,
1917
+ sessionId: controller.sessionId
1918
+ });
1919
+ });
1920
+ };
1921
+ const unsubscribe = controller.subscribe(() => {
1922
+ if (options.interruptOnPartial === false) {
1923
+ lastPartial = controller.partial;
1924
+ return;
1925
+ }
1926
+ if (!lastPartial && controller.partial) {
1927
+ interruptIfPlaying("partial-transcript");
1928
+ }
1929
+ lastPartial = controller.partial;
1930
+ });
1931
+ return {
1932
+ close: () => {
1933
+ unsubscribe();
1934
+ },
1935
+ handleLevel: (level) => {
1936
+ if (shouldInterruptForLevel(level, options)) {
1937
+ interruptIfPlaying("input-level");
1938
+ }
1939
+ },
1940
+ sendAudio: (audio) => {
1941
+ interruptIfPlaying("manual-audio");
1942
+ controller.sendAudio(audio);
1943
+ }
1944
+ };
1945
+ };
1946
+
1107
1947
  // src/client/htmxBootstrap.ts
1108
1948
  var VOICE_WAVE_POINTS = 48;
1109
1949
  var VOICE_WAVE_WIDTH = 320;
@@ -1126,7 +1966,7 @@ var DEFAULT_GUIDED_PROMPTS = [
1126
1966
  "Now describe what you are trying to do or test.",
1127
1967
  "Finish with any detail that feels blocked, risky, or unclear."
1128
1968
  ];
1129
- var clamp = (value, min, max) => Math.min(max, Math.max(min, value));
1969
+ var clamp = (value, min, max2) => Math.min(max2, Math.max(min, value));
1130
1970
  var escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1131
1971
  var readErrorField = (value, key) => {
1132
1972
  const candidate = value[key];
@@ -1160,6 +2000,17 @@ var formatErrorMessage = (error) => {
1160
2000
  }
1161
2001
  return "Unexpected error";
1162
2002
  };
2003
+ var formatReconnectState = (reconnect) => {
2004
+ const pieces = [reconnect.status];
2005
+ if (reconnect.attempts > 0 || reconnect.maxAttempts > 0) {
2006
+ pieces.push(`${reconnect.attempts}/${reconnect.maxAttempts} attempts`);
2007
+ }
2008
+ if (reconnect.nextAttemptAt) {
2009
+ const waitMs = Math.max(0, reconnect.nextAttemptAt - Date.now());
2010
+ pieces.push(`retry in ${Math.ceil(waitMs / 100) / 10}s`);
2011
+ }
2012
+ return pieces.join(" · ");
2013
+ };
1163
2014
  var createInitialVoiceWaveLevels = (count = VOICE_WAVE_POINTS) => Array.from({ length: count }, () => 0);
1164
2015
  var pushVoiceWaveLevel = (levels, nextLevel, count = VOICE_WAVE_POINTS) => {
1165
2016
  const next = levels.slice(-(count - 1));
@@ -1216,6 +2067,17 @@ var parsePromptList = (value) => {
1216
2067
  } catch {}
1217
2068
  return DEFAULT_GUIDED_PROMPTS;
1218
2069
  };
2070
+ var parseOptionalNumber = (value) => {
2071
+ if (!value) {
2072
+ return;
2073
+ }
2074
+ const parsed = Number(value);
2075
+ return Number.isFinite(parsed) ? parsed : undefined;
2076
+ };
2077
+ var resolveElement2 = (root, selector, ctor) => {
2078
+ const value = selector ? document.querySelector(selector) : root.querySelector(selector ?? "");
2079
+ return value instanceof ctor ? value : null;
2080
+ };
1219
2081
  var requireElement = (root, selector, ctor, name) => {
1220
2082
  const value = selector ? document.querySelector(selector) : null;
1221
2083
  if (value instanceof ctor) {
@@ -1266,11 +2128,20 @@ var initVoiceHTMXRoot = (root) => {
1266
2128
  const guidedPrompts = parsePromptList(root.dataset.voiceGuidedPrompts);
1267
2129
  const guidedLabel = root.dataset.voiceGuidedLabel ?? DEFAULT_GUIDED_LABEL;
1268
2130
  const generalLabel = root.dataset.voiceGeneralLabel ?? DEFAULT_GENERAL_LABEL;
2131
+ const reconnectReportPath = root.dataset.voiceReconnectReportPath;
2132
+ const bargeInPath = root.dataset.voiceBargeInPath;
2133
+ const bargeInMonitor = bargeInPath ? createVoiceBargeInMonitor({
2134
+ path: bargeInPath,
2135
+ thresholdMs: parseOptionalNumber(root.dataset.voiceBargeInThresholdMs)
2136
+ }) : null;
2137
+ const bargeInRecentWindowMs = parseOptionalNumber(root.dataset.voiceBargeInRecentWindowMs) ?? 4000;
2138
+ const bargeInSpeechThreshold = parseOptionalNumber(root.dataset.voiceBargeInSpeechThreshold) ?? 0.04;
1269
2139
  const syncElement = requireElement(document, root.dataset.voiceSync, HTMLElement, "voice-htmx-sync");
1270
2140
  const connectionMetric = requireElement(root, root.dataset.voiceConnection, HTMLElement, "metric-connection");
1271
2141
  const errorStatus = requireElement(root, root.dataset.voiceError, HTMLElement, "status-error");
1272
2142
  const microphoneStatus = requireElement(root, root.dataset.voiceMicrophone, HTMLElement, "status-mic");
1273
2143
  const promptStatus = requireElement(root, root.dataset.voicePrompt, HTMLElement, "status-prompt");
2144
+ const reconnectStatus = resolveElement2(root, root.dataset.voiceReconnect, HTMLElement);
1274
2145
  const chatList = requireElement(root, root.dataset.voiceChat, HTMLElement, "chat-list");
1275
2146
  const startGuidedButton = requireElement(root, root.dataset.voiceStartGuided, HTMLButtonElement, "start-guided");
1276
2147
  const startGeneralButton = requireElement(root, root.dataset.voiceStartGeneral, HTMLButtonElement, "start-general");
@@ -1279,35 +2150,70 @@ var initVoiceHTMXRoot = (root) => {
1279
2150
  const voiceMonitorCopy = requireElement(root, root.dataset.voiceMonitorCopy, HTMLElement, "voice-monitor-copy");
1280
2151
  const voiceWaveGlow = requireElement(root, root.dataset.voiceWaveGlow, SVGPathElement, "voice-wave-glow");
1281
2152
  const voiceWavePath = requireElement(root, root.dataset.voiceWavePath, SVGPathElement, "voice-wave-path");
2153
+ let activeMode = null;
2154
+ let hasStartedModes = {
2155
+ general: false,
2156
+ guided: false
2157
+ };
2158
+ let isCapturing = false;
2159
+ let micError = null;
2160
+ let waveLevels = createInitialVoiceWaveLevels();
2161
+ let guidedBargeInBinding = null;
2162
+ let generalBargeInBinding = null;
1282
2163
  const guidedVoice = createVoiceController(guidedPath, {
1283
2164
  capture: {
2165
+ onAudio: (audio, sendAudio) => {
2166
+ if (guidedBargeInBinding) {
2167
+ guidedBargeInBinding.sendAudio(audio);
2168
+ return;
2169
+ }
2170
+ sendAudio(audio);
2171
+ },
1284
2172
  onLevel: (level) => {
2173
+ guidedBargeInBinding?.handleLevel(level);
1285
2174
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1286
2175
  renderWave();
1287
2176
  }
1288
2177
  },
2178
+ connection: {
2179
+ reconnectReportPath
2180
+ },
1289
2181
  preset: "guided-intake"
1290
2182
  });
1291
2183
  const generalVoice = createVoiceController(generalPath, {
1292
2184
  capture: {
2185
+ onAudio: (audio, sendAudio) => {
2186
+ if (generalBargeInBinding) {
2187
+ generalBargeInBinding.sendAudio(audio);
2188
+ return;
2189
+ }
2190
+ sendAudio(audio);
2191
+ },
1293
2192
  onLevel: (level) => {
2193
+ generalBargeInBinding?.handleLevel(level);
1294
2194
  waveLevels = pushVoiceWaveLevel(waveLevels, level);
1295
2195
  renderWave();
1296
2196
  }
1297
2197
  },
2198
+ connection: {
2199
+ reconnectReportPath
2200
+ },
1298
2201
  preset: "dictation"
1299
2202
  });
1300
2203
  const stopGuidedBinding = guidedVoice.bindHTMX({ element: syncElement });
1301
2204
  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();
2205
+ const guidedAudioPlayer = createVoiceAudioPlayer(guidedVoice);
2206
+ const generalAudioPlayer = createVoiceAudioPlayer(generalVoice);
2207
+ guidedBargeInBinding = bindVoiceBargeIn(guidedVoice, guidedAudioPlayer, {
2208
+ interruptThreshold: bargeInSpeechThreshold,
2209
+ monitor: bargeInMonitor ?? undefined
2210
+ });
2211
+ generalBargeInBinding = bindVoiceBargeIn(generalVoice, generalAudioPlayer, {
2212
+ interruptThreshold: bargeInSpeechThreshold,
2213
+ monitor: bargeInMonitor ?? undefined
2214
+ });
1310
2215
  const currentVoice = () => activeMode === "general" ? generalVoice : guidedVoice;
2216
+ const currentAudioPlayer = () => activeMode === "general" ? generalAudioPlayer : guidedAudioPlayer;
1311
2217
  const renderWave = () => {
1312
2218
  const path = createVoiceWavePath(waveLevels);
1313
2219
  voiceWaveGlow.setAttribute("d", path);
@@ -1322,6 +2228,9 @@ var initVoiceHTMXRoot = (root) => {
1322
2228
  const status = voice.status;
1323
2229
  connectionMetric.textContent = voice.isConnected ? "Connected" : "Waiting";
1324
2230
  errorStatus.textContent = micError || voice.error || "None";
2231
+ if (reconnectStatus) {
2232
+ reconnectStatus.textContent = formatReconnectState(voice.reconnect);
2233
+ }
1325
2234
  microphoneStatus.textContent = isCapturing ? DEFAULT_MIC_LIVE : DEFAULT_MIC_IDLE;
1326
2235
  promptStatus.textContent = resolvePromptMessage({
1327
2236
  guidedPrompts,
@@ -1385,8 +2294,18 @@ var initVoiceHTMXRoot = (root) => {
1385
2294
  render();
1386
2295
  }
1387
2296
  };
1388
- guidedVoice.subscribe(render);
1389
- generalVoice.subscribe(render);
2297
+ guidedVoice.subscribe(() => {
2298
+ if (guidedVoice.assistantAudio.length > 0) {
2299
+ guidedAudioPlayer.start().catch(() => {});
2300
+ }
2301
+ render();
2302
+ });
2303
+ generalVoice.subscribe(() => {
2304
+ if (generalVoice.assistantAudio.length > 0) {
2305
+ generalAudioPlayer.start().catch(() => {});
2306
+ }
2307
+ render();
2308
+ });
1390
2309
  startGuidedButton.addEventListener("click", () => {
1391
2310
  startMode("guided");
1392
2311
  });
@@ -1399,6 +2318,10 @@ var initVoiceHTMXRoot = (root) => {
1399
2318
  window.addEventListener("beforeunload", () => {
1400
2319
  guidedVoice.stopRecording();
1401
2320
  generalVoice.stopRecording();
2321
+ guidedBargeInBinding?.close();
2322
+ generalBargeInBinding?.close();
2323
+ guidedAudioPlayer.close();
2324
+ generalAudioPlayer.close();
1402
2325
  stopGuidedBinding();
1403
2326
  stopGeneralBinding();
1404
2327
  guidedVoice.close();