@memberjunction/server 5.43.0 → 5.45.0

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 (309) hide show
  1. package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
  2. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
  3. package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
  4. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
  5. package/dist/agentSessions/SessionJanitor.d.ts +13 -0
  6. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  7. package/dist/agentSessions/SessionJanitor.js +65 -7
  8. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  9. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  10. package/dist/agentSessions/SessionManager.js +40 -0
  11. package/dist/agentSessions/SessionManager.js.map +1 -1
  12. package/dist/agentSessions/index.d.ts.map +1 -1
  13. package/dist/agentSessions/index.js +3 -1
  14. package/dist/agentSessions/index.js.map +1 -1
  15. package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
  16. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  17. package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
  18. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  19. package/dist/auth/magicLink/index.d.ts +1 -1
  20. package/dist/auth/magicLink/index.d.ts.map +1 -1
  21. package/dist/auth/magicLink/index.js +1 -1
  22. package/dist/auth/magicLink/index.js.map +1 -1
  23. package/dist/auth/magicLink/types.d.ts +26 -0
  24. package/dist/auth/magicLink/types.d.ts.map +1 -1
  25. package/dist/config.d.ts +1883 -144
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +160 -36
  28. package/dist/config.js.map +1 -1
  29. package/dist/context.d.ts.map +1 -1
  30. package/dist/context.js +45 -2
  31. package/dist/context.js.map +1 -1
  32. package/dist/generated/generated.d.ts +1684 -54
  33. package/dist/generated/generated.d.ts.map +1 -1
  34. package/dist/generated/generated.js +57105 -48029
  35. package/dist/generated/generated.js.map +1 -1
  36. package/dist/generic/ResolverBase.d.ts +13 -0
  37. package/dist/generic/ResolverBase.d.ts.map +1 -1
  38. package/dist/generic/ResolverBase.js +22 -0
  39. package/dist/generic/ResolverBase.js.map +1 -1
  40. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  41. package/dist/generic/RunViewResolver.js +4 -0
  42. package/dist/generic/RunViewResolver.js.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +110 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  48. package/dist/integration/CustomColumnPromoter.js +6 -0
  49. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  50. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  51. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  52. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  53. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  54. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  55. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  56. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  57. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  58. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  59. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  60. package/dist/realtimeWidget/host-identity.js +58 -0
  61. package/dist/realtimeWidget/host-identity.js.map +1 -0
  62. package/dist/realtimeWidget/index.d.ts +10 -0
  63. package/dist/realtimeWidget/index.d.ts.map +1 -0
  64. package/dist/realtimeWidget/index.js +9 -0
  65. package/dist/realtimeWidget/index.js.map +1 -0
  66. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  67. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  68. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  69. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  70. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  71. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  72. package/dist/realtimeWidget/widgetCore.js +173 -0
  73. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  74. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  75. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  76. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  77. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  78. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  79. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  80. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  81. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  82. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  83. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  84. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  85. package/dist/resolvers/FileResolver.d.ts +74 -0
  86. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  87. package/dist/resolvers/FileResolver.js +211 -2
  88. package/dist/resolvers/FileResolver.js.map +1 -1
  89. package/dist/resolvers/QueryResolver.d.ts +6 -4
  90. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  91. package/dist/resolvers/QueryResolver.js +29 -14
  92. package/dist/resolvers/QueryResolver.js.map +1 -1
  93. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  94. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  95. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  96. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  97. package/dist/resolvers/RealtimeBridgeResolver.d.ts +8 -1
  98. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  99. package/dist/resolvers/RealtimeBridgeResolver.js +28 -4
  100. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  101. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +91 -2
  102. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  103. package/dist/resolvers/RealtimeClientSessionResolver.js +337 -17
  104. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  105. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  106. package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
  107. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  108. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  109. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  110. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  111. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  112. package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
  113. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  114. package/dist/resolvers/RunAIAgentResolver.js +50 -17
  115. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  116. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  117. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  118. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  119. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  120. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  121. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  122. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  123. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  124. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  125. package/dist/resolvers/TelephonyResolver.js +86 -0
  126. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  127. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  128. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  129. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  130. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  131. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  132. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  133. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  134. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  135. package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
  136. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
  137. package/dist/resolvers/meetingRecordingRegistration.js +311 -0
  138. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
  139. package/dist/resolvers/peaksSidecar.d.ts +30 -0
  140. package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
  141. package/dist/resolvers/peaksSidecar.js +51 -0
  142. package/dist/resolvers/peaksSidecar.js.map +1 -0
  143. package/dist/rest/MediaAccessKeys.d.ts +68 -0
  144. package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
  145. package/dist/rest/MediaAccessKeys.js +96 -0
  146. package/dist/rest/MediaAccessKeys.js.map +1 -0
  147. package/dist/rest/MediaStreamHandler.d.ts +26 -0
  148. package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
  149. package/dist/rest/MediaStreamHandler.js +195 -0
  150. package/dist/rest/MediaStreamHandler.js.map +1 -0
  151. package/dist/rest/mediaRange.d.ts +41 -0
  152. package/dist/rest/mediaRange.d.ts.map +1 -0
  153. package/dist/rest/mediaRange.js +60 -0
  154. package/dist/rest/mediaRange.js.map +1 -0
  155. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  156. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  157. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  158. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  159. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  160. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  161. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  162. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  163. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  164. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  165. package/dist/telephony/TeamsMeetingsService.js +196 -0
  166. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  167. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  168. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  169. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  170. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  171. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  172. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  173. package/dist/telephony/TwilioTelephonyService.js +193 -0
  174. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  175. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  176. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  177. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  178. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  179. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  180. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  181. package/dist/telephony/VonageTelephonyService.js +191 -0
  182. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  183. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  184. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  185. package/dist/telephony/calendar-scheduler.js +133 -0
  186. package/dist/telephony/calendar-scheduler.js.map +1 -0
  187. package/dist/telephony/index.d.ts +29 -0
  188. package/dist/telephony/index.d.ts.map +1 -0
  189. package/dist/telephony/index.js +38 -0
  190. package/dist/telephony/index.js.map +1 -0
  191. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  192. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  193. package/dist/telephony/media-upgrade-router.js +56 -0
  194. package/dist/telephony/media-upgrade-router.js.map +1 -0
  195. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  196. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  197. package/dist/telephony/ringcentral-runtime.js +18 -0
  198. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  199. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  200. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  201. package/dist/telephony/teams-meetings-runtime.js +20 -0
  202. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  203. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  204. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  205. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  206. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  207. package/dist/telephony/telephony-runtime.d.ts +14 -0
  208. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  209. package/dist/telephony/telephony-runtime.js +18 -0
  210. package/dist/telephony/telephony-runtime.js.map +1 -0
  211. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  212. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  213. package/dist/telephony/twilioMediaRegistry.js +106 -0
  214. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  215. package/dist/telephony/vonage-runtime.d.ts +14 -0
  216. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  217. package/dist/telephony/vonage-runtime.js +18 -0
  218. package/dist/telephony/vonage-runtime.js.map +1 -0
  219. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  220. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  221. package/dist/telephony/vonageMediaRegistry.js +158 -0
  222. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  223. package/package.json +88 -82
  224. package/src/__tests__/FileResolverPeaks.test.ts +64 -0
  225. package/src/__tests__/MediaAccessKeys.test.ts +68 -0
  226. package/src/__tests__/MediaStreamHandler.test.ts +91 -0
  227. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
  228. package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
  229. package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
  230. package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
  231. package/src/__tests__/SessionManager.test.ts +2 -0
  232. package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
  233. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  234. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  235. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  236. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  237. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  238. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  239. package/src/__tests__/widget.test.ts +204 -0
  240. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  241. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  242. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  243. package/src/agentSessions/SessionJanitor.ts +66 -6
  244. package/src/agentSessions/SessionManager.ts +41 -0
  245. package/src/agentSessions/index.ts +3 -1
  246. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  247. package/src/auth/magicLink/index.ts +1 -0
  248. package/src/auth/magicLink/types.ts +26 -0
  249. package/src/config.ts +172 -38
  250. package/src/context.ts +50 -3
  251. package/src/generated/generated.ts +26726 -20464
  252. package/src/generic/ResolverBase.ts +28 -0
  253. package/src/generic/RunViewResolver.ts +4 -0
  254. package/src/index.ts +119 -3
  255. package/src/integration/CustomColumnPromoter.ts +6 -0
  256. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  257. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  258. package/src/realtimeWidget/host-identity.ts +84 -0
  259. package/src/realtimeWidget/index.ts +15 -0
  260. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  261. package/src/realtimeWidget/widgetCore.ts +231 -0
  262. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  263. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  264. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  265. package/src/resolvers/FileResolver.ts +199 -1
  266. package/src/resolvers/QueryResolver.ts +33 -19
  267. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  268. package/src/resolvers/RealtimeBridgeResolver.ts +36 -4
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +383 -9
  270. package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
  271. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  272. package/src/resolvers/RunAIAgentResolver.ts +60 -15
  273. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  274. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  275. package/src/resolvers/TelephonyResolver.ts +63 -0
  276. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  277. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  278. package/src/resolvers/meetingRecordingRegistration.ts +432 -0
  279. package/src/resolvers/peaksSidecar.ts +52 -0
  280. package/src/rest/MediaAccessKeys.ts +121 -0
  281. package/src/rest/MediaStreamHandler.ts +223 -0
  282. package/src/rest/mediaRange.ts +76 -0
  283. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  284. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  285. package/src/telephony/TeamsMeetingsService.ts +268 -0
  286. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  287. package/src/telephony/TwilioTelephonyService.ts +261 -0
  288. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  289. package/src/telephony/VonageTelephonyService.ts +259 -0
  290. package/src/telephony/calendar-scheduler.ts +176 -0
  291. package/src/telephony/index.ts +43 -0
  292. package/src/telephony/media-upgrade-router.ts +62 -0
  293. package/src/telephony/ringcentral-runtime.ts +22 -0
  294. package/src/telephony/teams-meetings-runtime.ts +24 -0
  295. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  296. package/src/telephony/telephony-runtime.ts +22 -0
  297. package/src/telephony/twilioMediaRegistry.ts +137 -0
  298. package/src/telephony/vonage-runtime.ts +22 -0
  299. package/src/telephony/vonageMediaRegistry.ts +200 -0
  300. package/dist/agents/skip-agent.d.ts +0 -111
  301. package/dist/agents/skip-agent.d.ts.map +0 -1
  302. package/dist/agents/skip-agent.js +0 -1487
  303. package/dist/agents/skip-agent.js.map +0 -1
  304. package/dist/agents/skip-sdk.d.ts +0 -261
  305. package/dist/agents/skip-sdk.d.ts.map +0 -1
  306. package/dist/agents/skip-sdk.js +0 -909
  307. package/dist/agents/skip-sdk.js.map +0 -1
  308. package/src/agents/skip-agent.ts +0 -1594
  309. package/src/agents/skip-sdk.ts +0 -1210
@@ -24,11 +24,12 @@
24
24
  *
25
25
  * @module @memberjunction/server
26
26
  */
27
- import { Resolver, Mutation, Arg, Ctx, Int, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
27
+ import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
28
28
  import { AppContext, UserPayload } from '../types.js';
29
29
  import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView } from '@memberjunction/core';
30
- import { UUIDsEqual } from '@memberjunction/global';
30
+ import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
31
31
  import {
32
+ MJAIAgentEntity,
32
33
  MJAIAgentSessionEntity,
33
34
  MJAIAgentSessionChannelEntity,
34
35
  MJArtifactEntity,
@@ -46,14 +47,20 @@ import {
46
47
  EvaluateRuntimeOverrideAuthorization,
47
48
  ParseRealtimeTypeConfiguration,
48
49
  ResolveEffectiveRealtimeConfig,
50
+ RealtimeAllowedAgent,
49
51
  REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION,
52
+ resolveRecordingStorageAccountID,
53
+ storeRealtimeRecording,
54
+ writeRealtimeRecordingSegment,
55
+ deleteRealtimeRecordingSegments,
50
56
  } from '@memberjunction/ai-agents';
51
- import { AgentExecutionProgressCallback, MJAIAgentEntityExtended } from '@memberjunction/ai-core-plus';
57
+ import { AgentExecutionProgressCallback, MJAIAgentEntityExtended, AppContextSnapshot } from '@memberjunction/ai-core-plus';
52
58
  import { RealtimeToolDefinition } from '@memberjunction/ai';
53
59
  import { ResolverBase } from '../generic/ResolverBase.js';
54
60
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
55
61
  import { GetReadWriteProvider } from '../util.js';
56
62
  import { SessionManager } from '../agentSessions/index.js';
63
+ import { resolveWidgetGuestRunContext } from '../realtimeWidget/widgetGuestElevation.js';
57
64
 
58
65
  /**
59
66
  * Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
@@ -155,6 +162,31 @@ interface RealtimeSessionConfig {
155
162
  * rather than starting fresh. Re-stored if the resumed run pauses again.
156
163
  */
157
164
  pendingFeedbackRunID?: string;
165
+ /**
166
+ * Absolute server-authoritative deadline (ISO 8601) past which this session is hard-closed by the
167
+ * {@link SessionJanitor}, independent of idle activity. Stamped at session start for abuse-sensitive
168
+ * deployments (a public web-widget voice guest's `VoiceMaxSessionMinutes`); absent for uncapped
169
+ * sessions. Preserved across config rewrites so the cap survives run-id / feedback updates.
170
+ */
171
+ maxSessionDeadlineIso?: string;
172
+ /**
173
+ * The application the session runs in (sourced the app cascade layer + RelevantAgents). Persisted
174
+ * for observability / continuity; absent when the session carries no app context.
175
+ */
176
+ applicationID?: string;
177
+ /**
178
+ * The session's effective allowed delegation targets (union from the config cascade, incl. the app's
179
+ * `RelevantAgents`). Persisted at start so each relayed `invoke-target-agent` call can validate a
180
+ * model-named colleague against it without re-resolving the cascade. Absent ⇒ single-target behavior.
181
+ */
182
+ allowedAgents?: RealtimeAllowedAgent[];
183
+ /**
184
+ * Per-session override for the agent's media kit — a `MJ: Collections` id that takes precedence
185
+ * over the agent's `DefaultMediaCollectionID` when the server-side `MediaChannelServer` resolves
186
+ * the media manifest at session start. Validated as a UUID at the mutation boundary; absent ⇒ the
187
+ * agent default kit is used (the common case).
188
+ */
189
+ mediaCollectionID?: string;
158
190
  }
159
191
 
160
192
  /**
@@ -289,6 +321,27 @@ export class CancelRealtimeSessionToolResult {
289
321
  ErrorMessage?: string;
290
322
  }
291
323
 
324
+ /**
325
+ * Result of {@link RealtimeClientSessionResolver.UploadRealtimeRecording} — a structured
326
+ * success/failure envelope. A recording-storage failure must never throw to the browser (the
327
+ * session it belongs to has already ended), so problems come back as `Success: false` with a
328
+ * human-readable reason; only ownership/authn violations (from `loadOwnedSession`) still throw.
329
+ */
330
+ @ObjectType()
331
+ export class UploadRealtimeRecordingResult {
332
+ /** True when the audio was stored and the session was stamped with the recording file. */
333
+ @Field(() => Boolean)
334
+ Success: boolean;
335
+
336
+ /** Human-readable failure reason. Null on success. */
337
+ @Field(() => String, { nullable: true })
338
+ ErrorMessage?: string;
339
+
340
+ /** ID of the created `MJ: Files` row holding the uploaded recording. Null on failure. */
341
+ @Field(() => String, { nullable: true })
342
+ FileID?: string;
343
+ }
344
+
292
345
  /**
293
346
  * Resolver for the client-direct realtime voice topology. A single {@link SessionManager} and a
294
347
  * single {@link RealtimeClientSessionService} are shared across requests — neither holds per-user or
@@ -348,6 +401,11 @@ export class RealtimeClientSessionResolver extends ResolverBase {
348
401
  @Arg('clientToolsJson', () => String, { nullable: true }) clientToolsJson?: string,
349
402
  @Arg('coAgentId', () => String, { nullable: true }) coAgentId?: string,
350
403
  @Arg('configOverridesJson', () => String, { nullable: true }) configOverridesJson?: string,
404
+ @Arg('recordingStartedAt', () => String, { nullable: true }) recordingStartedAt?: string,
405
+ @Arg('recordingConsent', () => Boolean, { nullable: true }) recordingConsent?: boolean,
406
+ @Arg('mediaCollectionId', () => String, { nullable: true }) mediaCollectionId?: string,
407
+ @Arg('applicationId', () => String, { nullable: true }) applicationId?: string,
408
+ @Arg('appContextJson', () => String, { nullable: true }) appContextJson?: string,
351
409
  ): Promise<StartRealtimeClientSessionResult> {
352
410
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
353
411
 
@@ -362,7 +420,29 @@ export class RealtimeClientSessionResolver extends ResolverBase {
362
420
  // silent ignore). Plain starts and within-pairing target selection are never gated here.
363
421
  await this.assertRuntimeOverridesAuthorized(coAgentID, configOverridesJson, preferredModelId, contextUser, provider);
364
422
 
423
+ // PUBLIC WEB-WIDGET VOICE CAP (public-web-widget.md W4): when the caller is a widget guest,
424
+ // resolve the deployment's hard duration ceiling and stamp an absolute deadline on the session.
425
+ // The SessionJanitor force-closes (finalizing runs) past the deadline — a server-authoritative
426
+ // bound that does not depend on the client honoring its own abuse guard or the ephemeral token TTL.
427
+ const maxSessionSeconds = await this.resolveWidgetVoiceCapSeconds(userPayload, providers);
365
428
  const config: RealtimeSessionConfig = { targetAgentID: effectiveTargetId };
429
+ if (maxSessionSeconds) {
430
+ config.maxSessionDeadlineIso = new Date(Date.now() + maxSessionSeconds * 1000).toISOString();
431
+ }
432
+ // Per-session media-kit override: stored on the session config so the server-side MediaChannelServer
433
+ // resolves it (over the agent default) at start. Validate the id here — a malformed value is
434
+ // dropped (logged), never interpolated; the session still starts and falls back to the agent kit.
435
+ const mediaOverride = mediaCollectionId?.trim();
436
+ if (mediaOverride) {
437
+ if (IsValidUUID(mediaOverride)) {
438
+ config.mediaCollectionID = mediaOverride;
439
+ } else {
440
+ LogStatus(
441
+ `StartRealtimeClientSession: ignoring malformed mediaCollectionId '${mediaCollectionId}' — ` +
442
+ 'using the agent default media kit.',
443
+ );
444
+ }
445
+ }
366
446
  const session = await this.sessionManager.CreateSession(
367
447
  {
368
448
  agentID: coAgentID,
@@ -375,6 +455,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
375
455
  provider,
376
456
  );
377
457
 
458
+ // Best-effort: stamp recording-start metadata when the browser captured (with consent) at start.
459
+ await this.stampRecordingStart(session, recordingConsent, recordingStartedAt);
460
+
378
461
  const clientTools = this.parseClientTools(clientToolsJson);
379
462
  // Best-effort model-context hydration: the PRIOR session chain's transcript (ownership-
380
463
  // checked, capped) is framed into the system prompt so the model REMEMBERS the last leg.
@@ -382,7 +465,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
382
465
  const priorTranscript = await this.loadPriorTranscript(lastSessionId, contextUser, provider);
383
466
  const result = await this.prepareClientSessionOrClose(
384
467
  session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, priorTranscript,
385
- configOverridesJson,
468
+ configOverridesJson, maxSessionSeconds, applicationId, this.parseAppContext(appContextJson),
386
469
  );
387
470
  // Best-effort restore of the PRIOR session's persisted channel states (e.g. the whiteboard
388
471
  // board). Strictly tolerant — any problem yields a null field, never a failed start.
@@ -416,6 +499,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
416
499
  {
417
500
  AgentSessionID: agentSessionId,
418
501
  TargetAgentID: config.targetAgentID,
502
+ // Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
503
+ // colleague in the call is validated against this; absent ⇒ single-target behavior.
504
+ AllowedAgents: config.allowedAgents,
419
505
  // Nest the delegated target-agent run under the co-agent observability run (when present).
420
506
  ParentRunID: config.coAgentRunID,
421
507
  Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
@@ -492,6 +578,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
492
578
  promptRunID: config.promptRunID,
493
579
  coAgentRunStepID: config.coAgentRunStepID,
494
580
  pendingFeedbackRunID: pausedRunID,
581
+ // Preserve the server-authoritative voice deadline across config rewrites.
582
+ maxSessionDeadlineIso: config.maxSessionDeadlineIso,
583
+ applicationID: config.applicationID,
584
+ allowedAgents: config.allowedAgents,
495
585
  };
496
586
  session.Config_ = JSON.stringify(next);
497
587
  if (!(await session.Save())) {
@@ -521,13 +611,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
521
611
  @Arg('text', () => String) text: string,
522
612
  @Ctx() { userPayload, providers }: AppContext,
523
613
  @Arg('replacesPrevious', () => Boolean, { nullable: true }) replacesPrevious?: boolean,
614
+ @Arg('utteranceStartMs', () => Int, { nullable: true }) utteranceStartMs?: number,
615
+ @Arg('utteranceEndMs', () => Int, { nullable: true }) utteranceEndMs?: number,
524
616
  ): Promise<boolean> {
525
617
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
526
618
  const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
527
619
 
528
620
  const saved = replacesPrevious
529
- ? await this.replacePreviousTranscriptTurn(session, role, text, contextUser, provider)
530
- : await this.persistTranscriptTurn(session, role, text, contextUser, provider);
621
+ ? await this.replacePreviousTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs)
622
+ : await this.persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs);
531
623
  if (!saved) {
532
624
  return false;
533
625
  }
@@ -557,6 +649,145 @@ export class RealtimeClientSessionResolver extends ResolverBase {
557
649
  return role.trim().toLowerCase() === 'user' ? 'user' : 'assistant';
558
650
  }
559
651
 
652
+ /**
653
+ * Upload a CLIENT-DIRECT session audio recording, store it in MJStorage, link it to the owning
654
+ * `AIAgentSession`, and stamp the session's recording fields. The browser records locally during
655
+ * the call and uploads the assembled blob (base64) once the session ends.
656
+ *
657
+ * Hard gates (in order):
658
+ * 1. Ownership — `loadOwnedSession` enforces `UserID === contextUser.ID` (throws on violation).
659
+ * 2. Consent — `consent !== true` is a hard refusal: no audio is ever stored without it.
660
+ * 3. Storage configuration — the session's agent must resolve a recording storage account (the
661
+ * agent's `RecordingStorageProviderID`, else `AttachmentStorageProviderID`).
662
+ *
663
+ * Storage failures (and any unexpected throw) come back as `Success: false` with a reason — they
664
+ * NEVER throw to the browser, mirroring the shared {@link storeRealtimeRecording} contract.
665
+ *
666
+ * @param agentSessionId The session the recording belongs to (ownership-gated).
667
+ * @param audioBase64 The base64-encoded recording bytes (client-direct: seekable WAV/PCM).
668
+ * @param mimeType The audio MIME type (`audio/wav` for client-direct; legacy `audio/webm`/`ogg`/`mp4`).
669
+ * @param durationMs Optional client-measured recording duration (ms) — informational.
670
+ * @param consent Whether the user consented to recording. MUST be `true` or the upload is refused.
671
+ * @param peaks Optional capture-time waveform peaks (max-abs per bucket, normalized 0..1). When
672
+ * present, persisted as a `peaks.json` sidecar beside the recording for fast waveform rendering
673
+ * without re-decoding the audio.
674
+ * @returns A structured {@link UploadRealtimeRecordingResult} with the created `MJ: Files` id.
675
+ */
676
+ @Mutation(() => UploadRealtimeRecordingResult)
677
+ async UploadRealtimeRecording(
678
+ @Arg('agentSessionId', () => String) agentSessionId: string,
679
+ @Arg('audioBase64', () => String) audioBase64: string,
680
+ @Arg('mimeType', () => String) mimeType: string,
681
+ @Ctx() ctx: AppContext,
682
+ @Arg('durationMs', () => Int, { nullable: true }) durationMs?: number,
683
+ @Arg('consent', () => Boolean, { nullable: true }) consent?: boolean,
684
+ @Arg('peaks', () => [Float], { nullable: true }) peaks?: number[],
685
+ ): Promise<UploadRealtimeRecordingResult> {
686
+ const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
687
+ const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
688
+
689
+ // HARD consent gate — no audio is ever stored without an explicit grant.
690
+ if (consent !== true) {
691
+ return { Success: false, ErrorMessage: 'Recording consent was not granted.' };
692
+ }
693
+
694
+ try {
695
+ const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', contextUser);
696
+ if (!(await agent.Load(session.AgentID))) {
697
+ return { Success: false, ErrorMessage: `Co-agent ${session.AgentID} for the session could not be loaded.` };
698
+ }
699
+
700
+ const accountID = await resolveRecordingStorageAccountID(agent, contextUser, provider);
701
+ if (!accountID) {
702
+ return { Success: false, ErrorMessage: 'No recording storage account is configured for this agent.' };
703
+ }
704
+
705
+ const buffer = Buffer.from(audioBase64, 'base64');
706
+ if (buffer.length === 0) {
707
+ return { Success: false, ErrorMessage: 'The uploaded recording was empty.' };
708
+ }
709
+
710
+ const fileID = await storeRealtimeRecording({
711
+ Audio: buffer,
712
+ MimeType: mimeType,
713
+ Media: 'Audio',
714
+ StartedAt: session.RecordingStartedAt ?? new Date(),
715
+ StorageAccountID: accountID,
716
+ SessionID: agentSessionId,
717
+ ContextUser: contextUser,
718
+ Provider: provider,
719
+ // Sanitized capture-time waveform peaks → persisted as a peaks.json sidecar.
720
+ Peaks: this.sanitizePeaks(peaks),
721
+ });
722
+
723
+ // Canonical consolidated file written — drop the crash-recovery shards (best-effort).
724
+ if (fileID) {
725
+ await deleteRealtimeRecordingSegments(agentSessionId, accountID, contextUser);
726
+ }
727
+
728
+ return {
729
+ Success: !!fileID,
730
+ FileID: fileID ?? undefined,
731
+ ErrorMessage: fileID ? undefined : 'Storage upload failed.',
732
+ };
733
+ } catch (error) {
734
+ const message = error instanceof Error ? error.message : String(error);
735
+ LogError(`RealtimeClientSessionResolver.UploadRealtimeRecording failed for session ${agentSessionId}: ${message}`);
736
+ return { Success: false, ErrorMessage: message };
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
742
+ * folder (`realtime-recordings/<sessionId>/seg-NNNN.<ext>`) as a raw storage object. Durability
743
+ * insurance during a live call so a browser/tab death loses at most the last window; the shards are
744
+ * deleted once the canonical consolidated file lands via {@link UploadRealtimeRecording}. Ownership-
745
+ * gated; consent was already established at session start. Best-effort — returns `false` (never
746
+ * throws) on any problem so a failed shard never disrupts the live call.
747
+ *
748
+ * @param agentSessionId The in-progress session (ownership-gated).
749
+ * @param segmentIndex 0-based shard index within the session.
750
+ * @param audioBase64 The base64-encoded shard bytes.
751
+ * @param mimeType The audio MIME type.
752
+ * @returns `true` when the shard was stored.
753
+ */
754
+ @Mutation(() => Boolean)
755
+ async UploadRealtimeRecordingSegment(
756
+ @Arg('agentSessionId', () => String) agentSessionId: string,
757
+ @Arg('segmentIndex', () => Int) segmentIndex: number,
758
+ @Arg('audioBase64', () => String) audioBase64: string,
759
+ @Arg('mimeType', () => String) mimeType: string,
760
+ @Ctx() ctx: AppContext,
761
+ ): Promise<boolean> {
762
+ try {
763
+ const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
764
+ const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
765
+ const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', contextUser);
766
+ if (!(await agent.Load(session.AgentID))) {
767
+ return false;
768
+ }
769
+ const accountID = await resolveRecordingStorageAccountID(agent, contextUser, provider);
770
+ if (!accountID) {
771
+ return false;
772
+ }
773
+ const buffer = Buffer.from(audioBase64, 'base64');
774
+ if (buffer.length === 0) {
775
+ return false;
776
+ }
777
+ return await writeRealtimeRecordingSegment({
778
+ SessionID: agentSessionId,
779
+ SegmentIndex: segmentIndex,
780
+ Audio: buffer,
781
+ MimeType: mimeType,
782
+ StorageAccountID: accountID,
783
+ ContextUser: contextUser,
784
+ });
785
+ } catch (error) {
786
+ LogError(`RealtimeClientSessionResolver.UploadRealtimeRecordingSegment failed for session ${agentSessionId}: ${error instanceof Error ? error.message : String(error)}`);
787
+ return false;
788
+ }
789
+ }
790
+
560
791
  /**
561
792
  * Relays a co-agent CHANNEL tool-call (browser_ / Whiteboard_ etc.) onto the session's co-agent
562
793
  * AIPromptRun.Messages — run-only observability so the run captures what the co-agent DID, not just
@@ -690,6 +921,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
690
921
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
691
922
  }
692
923
 
924
+ /**
925
+ * Sanitizes client-supplied waveform peaks before they're persisted: drops anything that isn't a
926
+ * finite number, clamps each to `[0, 1]`, and caps the array length so a hostile client can't bloat
927
+ * the sidecar. Returns `undefined` for a missing/empty array (no sidecar written).
928
+ */
929
+ private sanitizePeaks(peaks: number[] | undefined): number[] | undefined {
930
+ if (!Array.isArray(peaks) || peaks.length === 0) {
931
+ return undefined;
932
+ }
933
+ const MAX_PEAKS = 4096; // generous ceiling above the client's ~600-bucket target
934
+ const cleaned = peaks
935
+ .slice(0, MAX_PEAKS)
936
+ .filter((v) => Number.isFinite(v))
937
+ .map((v) => (v < 0 ? 0 : v > 1 ? 1 : v));
938
+ return cleaned.length > 0 ? cleaned : undefined;
939
+ }
940
+
693
941
  /**
694
942
  * Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
695
943
  * throw-on-missing-target semantics — usage relay is best-effort, so a missing/malformed
@@ -1305,6 +1553,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1305
1553
  clientTools?: RealtimeToolDefinition[],
1306
1554
  priorTranscript?: string,
1307
1555
  configOverridesJson?: string,
1556
+ maxSessionSeconds?: number,
1557
+ applicationId?: string,
1558
+ appContext?: AppContextSnapshot,
1308
1559
  ): Promise<StartRealtimeClientSessionResult> {
1309
1560
  const prep = await this.clientSessionService.PrepareClientSession(
1310
1561
  {
@@ -1312,6 +1563,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1312
1563
  TargetAgentID: targetAgentId,
1313
1564
  AgentSessionID: session.ID,
1314
1565
  ConversationID: session.ConversationID ?? undefined,
1566
+ // Server-authoritative voice duration cap (widget guests) — bounds the driver session
1567
+ // where supported; the janitor enforces the session deadline regardless.
1568
+ MaxSessionSeconds: maxSessionSeconds,
1315
1569
  // MVP: conversation history is not yet hydrated into ChatMessage[]; the co-agent
1316
1570
  // companion prompt runs without prior turns. A later phase loads the session's
1317
1571
  // Conversation into ChatMessage[] for richer context.
@@ -1325,6 +1579,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1325
1579
  PriorTranscript: priorTranscript,
1326
1580
  // Pre-authorized runtime override layer (assertRuntimeOverridesAuthorized gated it).
1327
1581
  ConfigOverridesJson: configOverridesJson,
1582
+ // App awareness (Move 1/3/4): the app sources the app cascade layer + RelevantAgents
1583
+ // (allowed-agent union); the snapshot is injected into the system prompt at mint.
1584
+ ApplicationID: applicationId,
1585
+ AppContext: appContext,
1328
1586
  },
1329
1587
  contextUser,
1330
1588
  provider,
@@ -1336,7 +1594,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1336
1594
  throw new Error(prep.ErrorMessage ?? 'Failed to prepare the client realtime session.');
1337
1595
  }
1338
1596
 
1339
- await this.persistObservabilityRunIDs(session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID);
1597
+ await this.persistObservabilityRunIDs(
1598
+ session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID,
1599
+ applicationId, prep.EffectiveConfig?.realtime?.allowedAgents,
1600
+ );
1340
1601
 
1341
1602
  const cfg = prep.ClientConfig;
1342
1603
  return {
@@ -1366,8 +1627,21 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1366
1627
  coAgentRunID?: string,
1367
1628
  promptRunID?: string,
1368
1629
  coAgentRunStepID?: string,
1630
+ applicationID?: string,
1631
+ allowedAgents?: RealtimeAllowedAgent[],
1369
1632
  ): Promise<void> {
1370
- const config: RealtimeSessionConfig = { targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID };
1633
+ // Preserve any server-authoritative voice deadline stamped at session start (this rebuilds the
1634
+ // full config, so read the existing value forward rather than dropping it).
1635
+ const existing = this.tryReadSessionConfig(session);
1636
+ const config: RealtimeSessionConfig = {
1637
+ targetAgentID,
1638
+ coAgentRunID,
1639
+ promptRunID,
1640
+ coAgentRunStepID,
1641
+ maxSessionDeadlineIso: existing?.maxSessionDeadlineIso,
1642
+ applicationID,
1643
+ allowedAgents: allowedAgents && allowedAgents.length > 0 ? allowedAgents : undefined,
1644
+ };
1371
1645
  session.Config_ = JSON.stringify(config);
1372
1646
  const saved = await session.Save();
1373
1647
  if (!saved) {
@@ -1997,6 +2271,28 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1997
2271
  }
1998
2272
  }
1999
2273
 
2274
+ /**
2275
+ * Tolerantly parses the `appContextJson` mutation arg into an {@link AppContextSnapshot}. Returns
2276
+ * `undefined` for absent/blank/malformed/non-object payloads — app context is best-effort enrichment
2277
+ * and must never fail a session start.
2278
+ *
2279
+ * @param appContextJson The raw JSON string the browser sent, or undefined.
2280
+ * @returns The parsed snapshot, or `undefined`.
2281
+ */
2282
+ private parseAppContext(appContextJson?: string): AppContextSnapshot | undefined {
2283
+ if (!appContextJson || !appContextJson.trim()) {
2284
+ return undefined;
2285
+ }
2286
+ try {
2287
+ const parsed: unknown = JSON.parse(appContextJson);
2288
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
2289
+ ? (parsed as AppContextSnapshot)
2290
+ : undefined;
2291
+ } catch {
2292
+ return undefined;
2293
+ }
2294
+ }
2295
+
2000
2296
  /**
2001
2297
  * Tolerantly parses + validates client-declared UI tool definitions (see the SECURITY NOTE on
2002
2298
  * {@link StartRealtimeClientSession}). Never throws — any rejection logs and returns
@@ -2069,6 +2365,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2069
2365
  coAgentRunStepID: typeof parsed.coAgentRunStepID === 'string' ? parsed.coAgentRunStepID : undefined,
2070
2366
  pendingFeedbackRunID:
2071
2367
  typeof parsed.pendingFeedbackRunID === 'string' ? parsed.pendingFeedbackRunID : undefined,
2368
+ maxSessionDeadlineIso:
2369
+ typeof parsed.maxSessionDeadlineIso === 'string' ? parsed.maxSessionDeadlineIso : undefined,
2370
+ applicationID: typeof parsed.applicationID === 'string' ? parsed.applicationID : undefined,
2371
+ allowedAgents: Array.isArray(parsed.allowedAgents) ? parsed.allowedAgents : undefined,
2072
2372
  };
2073
2373
  }
2074
2374
  } catch {
@@ -2078,18 +2378,76 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2078
2378
  throw new Error(`Realtime session ${session.ID} has no target agent configured`);
2079
2379
  }
2080
2380
 
2381
+ /** Like {@link readSessionConfig} but returns `undefined` instead of throwing (for best-effort reads). */
2382
+ private tryReadSessionConfig(session: MJAIAgentSessionEntity): RealtimeSessionConfig | undefined {
2383
+ try {
2384
+ return this.readSessionConfig(session);
2385
+ } catch {
2386
+ return undefined;
2387
+ }
2388
+ }
2389
+
2390
+ /**
2391
+ * Resolves the server-authoritative voice duration cap (seconds) for a session start, or `undefined`
2392
+ * when no cap applies (the common, uncapped case). A cap applies only when the caller is a public
2393
+ * web-widget guest with a configured `VoiceMaxSessionMinutes` on its widget instance (the same value
2394
+ * surfaced to the client guard at mint). Never throws — a resolution failure yields no cap rather
2395
+ * than blocking the start.
2396
+ */
2397
+ private async resolveWidgetVoiceCapSeconds(
2398
+ userPayload: UserPayload,
2399
+ providers: AppContext['providers'],
2400
+ ): Promise<number | undefined> {
2401
+ const elevation = await resolveWidgetGuestRunContext(userPayload, GetReadWriteProvider(providers));
2402
+ const minutes = elevation?.widget.VoiceMaxSessionMinutes;
2403
+ return minutes && minutes > 0 ? minutes * 60 : undefined;
2404
+ }
2405
+
2081
2406
  /**
2082
2407
  * Persists a single transcript turn as a `Conversation Detail` stamped with the session's
2083
2408
  * conversation, the mapped role, the turn text, the session id, and the owning user.
2084
2409
  *
2085
2410
  * @returns The boolean save result (logs `CompleteMessage` on failure).
2086
2411
  */
2412
+ /**
2413
+ * Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
2414
+ * just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
2415
+ * is logged and swallowed — a recording-metadata problem must never fail the session start.
2416
+ *
2417
+ * @param session The freshly created/loaded session.
2418
+ * @param recordingConsent Whether the user consented to recording — only `true` stamps anything.
2419
+ * @param recordingStartedAt ISO-8601 recording start timestamp from the browser.
2420
+ */
2421
+ private async stampRecordingStart(
2422
+ session: MJAIAgentSessionEntity,
2423
+ recordingConsent: boolean | undefined,
2424
+ recordingStartedAt: string | undefined,
2425
+ ): Promise<void> {
2426
+ if (recordingConsent !== true || !recordingStartedAt) {
2427
+ return;
2428
+ }
2429
+ const startedAt = new Date(recordingStartedAt);
2430
+ if (Number.isNaN(startedAt.getTime())) {
2431
+ LogError(`RealtimeClientSessionResolver.stampRecordingStart: invalid recordingStartedAt '${recordingStartedAt}' for session ${session.ID}`);
2432
+ return;
2433
+ }
2434
+ session.RecordingStartedAt = startedAt;
2435
+ session.RecordingMedia = 'Audio';
2436
+ if (!(await session.Save())) {
2437
+ LogError(
2438
+ `RealtimeClientSessionResolver.stampRecordingStart save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`,
2439
+ );
2440
+ }
2441
+ }
2442
+
2087
2443
  private async persistTranscriptTurn(
2088
2444
  session: MJAIAgentSessionEntity,
2089
2445
  role: string,
2090
2446
  text: string,
2091
2447
  contextUser: UserInfo,
2092
2448
  provider: IMetadataProvider,
2449
+ utteranceStartMs?: number,
2450
+ utteranceEndMs?: number,
2093
2451
  ): Promise<boolean> {
2094
2452
  const detail = await provider.GetEntityObject<MJConversationDetailEntity>(
2095
2453
  CONVERSATION_DETAIL_ENTITY,
@@ -2101,6 +2459,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2101
2459
  detail.Message = text;
2102
2460
  detail.AgentSessionID = session.ID;
2103
2461
  detail.UserID = contextUser.ID;
2462
+ if (typeof utteranceStartMs === 'number' && utteranceStartMs >= 0) {
2463
+ detail.UtteranceStartMs = utteranceStartMs;
2464
+ }
2465
+ if (typeof utteranceEndMs === 'number' && utteranceEndMs >= 0) {
2466
+ detail.UtteranceEndMs = utteranceEndMs;
2467
+ }
2104
2468
 
2105
2469
  const saved = await detail.Save();
2106
2470
  if (!saved) {
@@ -2124,6 +2488,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2124
2488
  text: string,
2125
2489
  contextUser: UserInfo,
2126
2490
  provider: IMetadataProvider,
2491
+ utteranceStartMs?: number,
2492
+ utteranceEndMs?: number,
2127
2493
  ): Promise<boolean> {
2128
2494
  const mappedRole = this.mapTranscriptRole(role);
2129
2495
  const rv = RunView.FromMetadataProvider(provider);
@@ -2139,9 +2505,17 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2139
2505
  );
2140
2506
  const previous = result.Success ? (result.Results?.[0] ?? null) : null;
2141
2507
  if (!previous) {
2142
- return this.persistTranscriptTurn(session, role, text, contextUser, provider);
2508
+ return this.persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs);
2143
2509
  }
2144
2510
  previous.Message = text;
2511
+ // The correction extends the existing turn: always refresh the end boundary when provided,
2512
+ // but only set the start when it wasn't already captured on the superseded turn.
2513
+ if (typeof utteranceEndMs === 'number' && utteranceEndMs >= 0) {
2514
+ previous.UtteranceEndMs = utteranceEndMs;
2515
+ }
2516
+ if (typeof utteranceStartMs === 'number' && utteranceStartMs >= 0 && previous.UtteranceStartMs == null) {
2517
+ previous.UtteranceStartMs = utteranceStartMs;
2518
+ }
2145
2519
  const saved = await previous.Save();
2146
2520
  if (!saved) {
2147
2521
  LogError(
@@ -408,8 +408,24 @@ export class RemoteBrowserActionResolver extends ResolverBase {
408
408
  if (!liveSession) {
409
409
  return {};
410
410
  }
411
- const screenshot = await liveSession.CaptureScreenshot();
412
- return { ScreenshotBase64: screenshot, CurrentUrl: liveSession.GetCurrentUrl() };
411
+ // The snapshot is a best-effort PERCEPTION poll (the surface fires it every ~700ms). A session
412
+ // whose underlying browser adapter has been torn down — a stale/dead handle still in the live
413
+ // map, a mid-poll teardown race, or a recycled backend container — makes CaptureScreenshot()
414
+ // throw "Browser not launched". That must degrade to an empty snapshot (the surface keeps its
415
+ // last good frame), NOT surface as a recurring GraphQL error the client logs on every tick —
416
+ // exactly as this query's contract above promises ("null rather than an error"). The live
417
+ // navigate/click path (ExecuteRemoteBrowserAction) is where a genuine browser failure is
418
+ // reported to the agent; the read-only view poll never should be.
419
+ try {
420
+ const screenshot = await liveSession.CaptureScreenshot();
421
+ return { ScreenshotBase64: screenshot, CurrentUrl: liveSession.GetCurrentUrl() };
422
+ } catch (err) {
423
+ LogError(
424
+ `[RemoteBrowserActionResolver] Snapshot capture failed for agent session ${agentSessionID} ` +
425
+ `(returning empty snapshot): ${err instanceof Error ? err.message : String(err)}`,
426
+ );
427
+ return {};
428
+ }
413
429
  }
414
430
 
415
431
  /**