@memberjunction/server 6.2.0-edge.3 → 6.2.0-edge.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (346) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  3. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  4. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  5. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  6. package/dist/agentSessions/HostInstance.d.ts +75 -11
  7. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  8. package/dist/agentSessions/HostInstance.js +99 -28
  9. package/dist/agentSessions/HostInstance.js.map +1 -1
  10. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  12. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  13. package/dist/agentSessions/SessionJanitor.d.ts +173 -25
  14. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  15. package/dist/agentSessions/SessionJanitor.js +452 -54
  16. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  17. package/dist/agentSessions/SessionManager.d.ts +43 -2
  18. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  19. package/dist/agentSessions/SessionManager.js +99 -8
  20. package/dist/agentSessions/SessionManager.js.map +1 -1
  21. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  25. package/dist/agentSessions/index.d.ts +2 -0
  26. package/dist/agentSessions/index.d.ts.map +1 -1
  27. package/dist/agentSessions/index.js +10 -3
  28. package/dist/agentSessions/index.js.map +1 -1
  29. package/dist/auth/actionAuthorization.d.ts +40 -0
  30. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  31. package/dist/auth/actionAuthorization.js +94 -0
  32. package/dist/auth/actionAuthorization.js.map +1 -0
  33. package/dist/auth/index.d.ts.map +1 -1
  34. package/dist/auth/index.js +3 -10
  35. package/dist/auth/index.js.map +1 -1
  36. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  37. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  38. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  39. package/dist/auth/newUsers.d.ts +10 -0
  40. package/dist/auth/newUsers.d.ts.map +1 -1
  41. package/dist/auth/newUsers.js +80 -11
  42. package/dist/auth/newUsers.js.map +1 -1
  43. package/dist/config.d.ts +2378 -0
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/config.js +123 -0
  46. package/dist/config.js.map +1 -1
  47. package/dist/context.d.ts +8 -0
  48. package/dist/context.d.ts.map +1 -1
  49. package/dist/context.js +14 -11
  50. package/dist/context.js.map +1 -1
  51. package/dist/generated/graphql-schemas/__mj.d.ts +135 -0
  52. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  53. package/dist/generated/graphql-schemas/__mj.js +706 -0
  54. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  55. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  56. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  57. package/dist/generic/ResolverBase.d.ts +20 -4
  58. package/dist/generic/ResolverBase.d.ts.map +1 -1
  59. package/dist/generic/ResolverBase.js +39 -10
  60. package/dist/generic/ResolverBase.js.map +1 -1
  61. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  62. package/dist/generic/RunViewResolver.js +4 -0
  63. package/dist/generic/RunViewResolver.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +120 -131
  67. package/dist/index.js.map +1 -1
  68. package/dist/isolatedProvider.d.ts +20 -0
  69. package/dist/isolatedProvider.d.ts.map +1 -0
  70. package/dist/isolatedProvider.js +22 -0
  71. package/dist/isolatedProvider.js.map +1 -0
  72. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  73. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  74. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  75. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  76. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  77. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  78. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  79. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  80. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  81. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  82. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  83. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  84. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  85. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  86. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +138 -0
  87. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  88. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  89. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  90. package/dist/realtimeSessions/RealtimeVerifyRouter.js +98 -0
  91. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  92. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  93. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  94. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  95. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  96. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  97. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  98. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  99. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  100. package/dist/realtimeSessions/index.d.ts +17 -0
  101. package/dist/realtimeSessions/index.d.ts.map +1 -0
  102. package/dist/realtimeSessions/index.js +17 -0
  103. package/dist/realtimeSessions/index.js.map +1 -0
  104. package/dist/realtimeSessions/sessionAccess.d.ts +86 -0
  105. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  106. package/dist/realtimeSessions/sessionAccess.js +90 -0
  107. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  108. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  109. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  110. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  111. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  112. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  113. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  114. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  115. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  116. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  117. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  118. package/dist/realtimeSessions/verificationCore.js +356 -0
  119. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  120. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  121. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  122. package/dist/realtimeSessions/verificationPages.js +110 -0
  123. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  124. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  125. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  126. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  127. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  128. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  129. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  130. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  131. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  132. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  133. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  134. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  135. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  136. package/dist/resolvers/ActionResolver.d.ts +31 -5
  137. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  138. package/dist/resolvers/ActionResolver.js +75 -14
  139. package/dist/resolvers/ActionResolver.js.map +1 -1
  140. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  141. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  142. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  143. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  144. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  145. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  146. package/dist/resolvers/CacheStatsResolver.js +113 -2
  147. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  148. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  149. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  150. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  151. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  152. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +12 -1
  153. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  154. package/dist/resolvers/FetchEntityVectorsResolver.js +83 -2
  155. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  156. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  157. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  158. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  159. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  160. package/dist/resolvers/MCPResolver.js +9 -4
  161. package/dist/resolvers/MCPResolver.js.map +1 -1
  162. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  163. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  164. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  165. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  166. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +155 -25
  167. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  168. package/dist/resolvers/RealtimeClientSessionResolver.js +394 -90
  169. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  170. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  171. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  172. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  173. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  174. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  175. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  176. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  177. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  178. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  179. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  180. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  181. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  182. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  183. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  184. package/dist/resolvers/RunAIAgentResolver.js +68 -11
  185. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  186. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  187. package/dist/resolvers/RunTemplateResolver.js +12 -11
  188. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  189. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  190. package/dist/resolvers/SyncDataResolver.js +6 -2
  191. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  192. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  193. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  194. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  195. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  196. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  197. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  198. package/dist/resolvers/UserAvatarResolver.js +124 -0
  199. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  200. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  201. package/dist/resolvers/UserResolver.js +2 -17
  202. package/dist/resolvers/UserResolver.js.map +1 -1
  203. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  204. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  205. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  206. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  207. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  208. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  209. package/dist/resolvers/avatarInputValidation.js +97 -0
  210. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  211. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  212. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  213. package/dist/resolvers/currentUserRoles.js +30 -0
  214. package/dist/resolvers/currentUserRoles.js.map +1 -1
  215. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  216. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  217. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  218. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  219. package/dist/services/TaskGraphContinuationDeliverer.d.ts +4 -3
  220. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  221. package/dist/services/TaskGraphContinuationDeliverer.js +13 -7
  222. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  223. package/dist/sharedCache.d.ts +105 -0
  224. package/dist/sharedCache.d.ts.map +1 -0
  225. package/dist/sharedCache.js +276 -0
  226. package/dist/sharedCache.js.map +1 -0
  227. package/dist/types.d.ts +6 -0
  228. package/dist/types.d.ts.map +1 -1
  229. package/dist/types.js.map +1 -1
  230. package/package.json +97 -95
  231. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  232. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  233. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  234. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  235. package/src/__tests__/FetchEntityVectorsResolver.test.ts +285 -0
  236. package/src/__tests__/HostInstance.test.ts +84 -0
  237. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  238. package/src/__tests__/MeetingResolver.test.ts +75 -1
  239. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  240. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  241. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  242. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  243. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  244. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  245. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  246. package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
  247. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  248. package/src/__tests__/RunAIAgentResolver.branch.test.ts +169 -0
  249. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +4 -4
  250. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  251. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  252. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  253. package/src/__tests__/SessionJanitor.test.ts +845 -18
  254. package/src/__tests__/SessionManager.test.ts +213 -17
  255. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  256. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  257. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +50 -3
  258. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  259. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  260. package/src/__tests__/actionAuthorization.test.ts +130 -0
  261. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  262. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  263. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  264. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  265. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  266. package/src/__tests__/newUsers.template.test.ts +151 -0
  267. package/src/__tests__/newUsers.test.ts +74 -5
  268. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  269. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  270. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  271. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  272. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  273. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  274. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  275. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  276. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  277. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  278. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  279. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  280. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  281. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  282. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  283. package/src/__tests__/serverExtensionReservedRoots.test.ts +2 -0
  284. package/src/__tests__/sharedCache.test.ts +310 -0
  285. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  286. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  287. package/src/agentSessions/HostInstance.ts +124 -28
  288. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  289. package/src/agentSessions/SessionJanitor.ts +543 -57
  290. package/src/agentSessions/SessionManager.ts +125 -25
  291. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  292. package/src/agentSessions/index.ts +10 -3
  293. package/src/auth/actionAuthorization.ts +116 -0
  294. package/src/auth/index.ts +3 -8
  295. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  296. package/src/auth/newUsers.ts +99 -11
  297. package/src/config.ts +125 -0
  298. package/src/context.ts +15 -10
  299. package/src/generated/graphql-schemas/__mj.ts +496 -1
  300. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  301. package/src/generic/ResolverBase.ts +39 -9
  302. package/src/generic/RunViewResolver.ts +4 -0
  303. package/src/index.ts +132 -143
  304. package/src/isolatedProvider.ts +22 -0
  305. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  306. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  307. package/src/realtimeSessions/README.md +179 -0
  308. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  309. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +167 -0
  310. package/src/realtimeSessions/RealtimeVerifyRouter.ts +108 -0
  311. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  312. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  313. package/src/realtimeSessions/index.ts +16 -0
  314. package/src/realtimeSessions/sessionAccess.ts +141 -0
  315. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  316. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  317. package/src/realtimeSessions/verificationCore.ts +502 -0
  318. package/src/realtimeSessions/verificationPages.ts +149 -0
  319. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  320. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  321. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  322. package/src/resolvers/ActionResolver.ts +84 -21
  323. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  324. package/src/resolvers/CacheStatsResolver.ts +89 -2
  325. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  326. package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
  327. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  328. package/src/resolvers/MCPResolver.ts +8 -5
  329. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  330. package/src/resolvers/RealtimeClientSessionResolver.ts +501 -96
  331. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  332. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  333. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  334. package/src/resolvers/RunAIAgentResolver.ts +78 -11
  335. package/src/resolvers/RunTemplateResolver.ts +13 -12
  336. package/src/resolvers/SyncDataResolver.ts +6 -2
  337. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  338. package/src/resolvers/UserAvatarResolver.ts +111 -0
  339. package/src/resolvers/UserResolver.ts +2 -16
  340. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  341. package/src/resolvers/avatarInputValidation.ts +123 -0
  342. package/src/resolvers/currentUserRoles.ts +32 -1
  343. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  344. package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
  345. package/src/sharedCache.ts +302 -0
  346. package/src/types.ts +7 -0
@@ -23,9 +23,9 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
23
23
  * and **executes** every tool call the browser relays back. The three mutations here cover the MVP:
24
24
  *
25
25
  * 1. {@link RealtimeClientSessionResolver.StartRealtimeClientSession} — authorize, create the
26
- * session (storing the target agent id server-side, authoritatively), mint the ephemeral config.
27
- * 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call,
28
- * reading the target agent id from the session (NOT the client) so the browser can't swap targets.
26
+ * session (storing the target agent id on it), mint the ephemeral config.
27
+ * 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
28
+ * for the target agent stored on the session, re-authorized for the caller on every call.
29
29
  * 3. {@link RealtimeClientSessionResolver.RelayRealtimeTranscript} — persist a transcript turn as a
30
30
  * `Conversation Detail`.
31
31
  *
@@ -33,25 +33,44 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
33
33
  * - **Start**: the caller must have `CanRun` on the *target* agent (the Realtime Co-Agent is an internal
34
34
  * orchestration agent, so the meaningful gate is the agent doing the real work).
35
35
  * - **Execute / Relay**: inbound ownership — the session's `UserID` must equal `contextUser.ID`.
36
+ * - **Session `Config`**: the owner can edit it through the generic entity API, so nothing in it is
37
+ * trusted. Execute re-checks `CanRun` on its target, direct actions never come from it, and its
38
+ * run ids are used only when {@link SessionRunIDVerifier} finds them to be records of the session.
39
+ * - **Scope-limited callers** (anonymous and resource-scoped magic-link sessions, including public
40
+ * web-widget guests): no direct actions. Their scope exists only as row-level filters, which an
41
+ * action can read around, and a scoped anonymous caller's relay runs as the system user. They
42
+ * reach the target agent only through `invoke-target-agent`.
36
43
  *
37
44
  * @module @memberjunction/server
38
45
  */
39
46
  import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
40
- import { AuthorizationEvaluator, LogError, LogStatus, RunView } from '@memberjunction/core';
47
+ import { AuthorizationEvaluator, LogError, LogErrorEx, LogStatus, RunView } from '@memberjunction/core';
41
48
  import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
49
+ import { ConversationEngine, } from '@memberjunction/core-entities';
42
50
  import { AIEngine } from '@memberjunction/aiengine';
43
- import { AIAgentPermissionHelper, AIEngineBase } from '@memberjunction/ai-engine-base';
44
- import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, ResolveRealtimeCoAgentID, FilterAllowedAgentsByCanRun, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
51
+ import { AIAgentPermissionHelper, AIEngineBase, HasRealtimeUsage, ParseRealtimeUsageRecord } from '@memberjunction/ai-engine-base';
52
+ import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, ResolveRealtimeCoAgentID, FilterAllowedAgentsByCanRun, INVOKE_TARGET_AGENT_TOOL_NAME, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
53
+ import { IsValidRealtimeToolDefinition, NormalizeChannelKey, ParseRealtimeChannelCandidates } from '@memberjunction/ai-core-plus';
45
54
  import { ResolverBase } from '../generic/ResolverBase.js';
46
55
  import { GetReadWriteProvider } from '../util.js';
47
56
  import { SessionManager } from '../agentSessions/index.js';
48
- import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
57
+ import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
58
+ import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
59
+ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser, ResolveRecordingStoreUser } from '../realtimeWidget/widgetGuestElevation.js';
60
+ import { RunWithTrustedSessionConfigWrites } from '@memberjunction/core-entities-server';
61
+ import { BuildMintConfigStamps, LoadInheritedVerifiedIdentity } from '../realtimeSessions/sessionConfigStamps.js';
62
+ import { ParseConfigObject } from '../realtimeSessions/verificationCore.js';
49
63
  /**
50
64
  * Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
51
65
  * in {@link import('./RunAIAgentResolver.js').RunAIAgentResolver}. Initialization/validation/finalization
52
66
  * noise is dropped so the model only narrates meaningful work.
53
67
  */
54
68
  const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
69
+ /**
70
+ * The longest `usageDetailsJson` a usage relay may carry. Every field of both detail blocks fits in a few hundred
71
+ * characters, so anything longer is not a relay the browser runtime sends.
72
+ */
73
+ const MAX_USAGE_DETAILS_JSON_LENGTH = 4096;
55
74
  /** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
56
75
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
57
76
  const CO_AGENT_ENTITY = 'MJ: AI Agent Co Agents';
@@ -77,7 +96,8 @@ const WHITEBOARD_ARTIFACT_TYPE_NAME = 'Whiteboard';
77
96
  */
78
97
  const MAX_CLIENT_TOOLS = 64;
79
98
  /** Maximum accepted size (chars) of the serialized client tool declarations. Raised in step with
80
- * {@link MAX_CLIENT_TOOLS} — multi-channel tool sets with verbose descriptions + JSON schemas run large. */
99
+ * {@link MAX_CLIENT_TOOLS} — multi-channel tool sets with verbose descriptions + JSON schemas run large.
100
+ * Also the largest serialized app-context snapshot stored on the session config. */
81
101
  const MAX_CLIENT_TOOLS_JSON_CHARS = 256_000;
82
102
  /** Maximum accepted size (chars) of a persisted channel state blob. */
83
103
  const MAX_CHANNEL_STATE_CHARS = 2_000_000;
@@ -121,6 +141,14 @@ __decorate([
121
141
  Field(() => String),
122
142
  __metadata("design:type", String)
123
143
  ], StartRealtimeClientSessionResult.prototype, "ExpiresAt", void 0);
144
+ __decorate([
145
+ Field(() => String, { nullable: true }),
146
+ __metadata("design:type", String)
147
+ ], StartRealtimeClientSessionResult.prototype, "Transport", void 0);
148
+ __decorate([
149
+ Field(() => String, { nullable: true }),
150
+ __metadata("design:type", String)
151
+ ], StartRealtimeClientSessionResult.prototype, "RelayUrl", void 0);
124
152
  __decorate([
125
153
  Field(() => String),
126
154
  __metadata("design:type", String)
@@ -149,6 +177,14 @@ __decorate([
149
177
  Field(() => String, { nullable: true }),
150
178
  __metadata("design:type", String)
151
179
  ], StartRealtimeClientSessionResult.prototype, "EffectiveConfigJson", void 0);
180
+ __decorate([
181
+ Field(() => String, { nullable: true }),
182
+ __metadata("design:type", String)
183
+ ], StartRealtimeClientSessionResult.prototype, "ClientPolicyJson", void 0);
184
+ __decorate([
185
+ Field(() => String, { nullable: true }),
186
+ __metadata("design:type", String)
187
+ ], StartRealtimeClientSessionResult.prototype, "AvatarStatusJson", void 0);
152
188
  StartRealtimeClientSessionResult = __decorate([
153
189
  ObjectType()
154
190
  ], StartRealtimeClientSessionResult);
@@ -246,6 +282,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
246
282
  // state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
247
283
  this.clientSessionService = new RealtimeClientSessionService();
248
284
  this.sessionManager = new SessionManager(this.clientSessionService);
285
+ this.runIDVerifier = new SessionRunIDVerifier();
249
286
  }
250
287
  /**
251
288
  * The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
@@ -281,7 +318,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
281
318
  * agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
282
319
  * see {@link ResolveRealtimeCoAgentID} for the full contract.
283
320
  * 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
284
- * config server-side — this is the authoritative target for all later relays.
321
+ * config — the target later relays use, re-authorized for the caller on each relay.
285
322
  * 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
286
323
  * On failure the just-created session is closed so no half-open session leaks.
287
324
  *
@@ -307,10 +344,16 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
307
344
  * `TypeConfiguration` ← this). **Authorization-gated**: requires the
308
345
  * `Realtime: Advanced Session Controls` authorization — unauthorized callers receive a
309
346
  * structured rejection (never a silent ignore). Must be a JSON object.
347
+ * @param showsAgentVideo `false` from an app that shows no agent video (no channel of it shows it: the embeddable
348
+ * widgets, the mobile app): the session asks the model for no avatar, and `AvatarStatusJson` says `host`. Absent or
349
+ * `true`: the app may show it. It only ever turns video off, so it needs no authorization.
350
+ * @param conversationBranchId Optional fork (`MJ: Conversation Branches.ID`) the session starts on;
351
+ * omitted is Main. It must be a fork of `conversationId`, else the start fails and no session is
352
+ * created. The session's history is read from that fork's path and its transcript is written there.
310
353
  *
311
354
  * @returns The ephemeral config + session linkage the browser needs to open its socket.
312
355
  */
313
- async StartRealtimeClientSession(targetAgentId, { userPayload, providers }, conversationId, lastSessionId, preferredModelId, clientToolsJson, coAgentId, configOverridesJson, recordingStartedAt, recordingConsent, mediaCollectionId, applicationId, appContextJson) {
356
+ async StartRealtimeClientSession(targetAgentId, { userPayload, providers }, conversationId, lastSessionId, preferredModelId, clientToolsJson, coAgentId, configOverridesJson, recordingStartedAt, recordingConsent, mediaCollectionId, applicationId, appContextJson, channelCandidatesJson, showsAgentVideo, conversationBranchId) {
314
357
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
315
358
  const coAgentID = await ResolveRealtimeCoAgentID(targetAgentId, coAgentId, contextUser, provider);
316
359
  // PAIRING CONSTRAINTS: a co-agent with pairing rows is restricted to that target list
@@ -344,16 +387,24 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
344
387
  'using the agent default media kit.');
345
388
  }
346
389
  }
347
- const session = await this.sessionManager.CreateSession({
390
+ const sessionBranchId = await this.resolveSessionBranch(conversationId, conversationBranchId, contextUser, provider);
391
+ if (sessionBranchId) {
392
+ config.conversationBranchID = sessionBranchId;
393
+ }
394
+ // The initial Config carries `maxSessionDeadlineIso` for capped sessions — a server-authoritative key
395
+ // that `MJAIAgentSessionEntityServer` refuses to let the session OWNER write, so this write (and only
396
+ // this one, built entirely from server-decided values) runs in the trusted scope.
397
+ const session = await RunWithTrustedSessionConfigWrites(() => this.sessionManager.CreateSession({
348
398
  agentID: coAgentID,
349
399
  userID: contextUser.ID,
350
400
  conversationID: conversationId,
351
401
  lastSessionID: lastSessionId,
352
402
  config: JSON.stringify(config),
353
- }, contextUser, provider);
403
+ }, contextUser, provider));
354
404
  // Best-effort: stamp recording-start metadata when the browser captured (with consent) at start.
355
405
  await this.stampRecordingStart(session, recordingConsent, recordingStartedAt);
356
406
  const clientTools = this.parseClientTools(clientToolsJson);
407
+ const channelCandidates = this.parseChannelCandidates(channelCandidatesJson);
357
408
  // Best-effort model-context hydration: the PRIOR session chain's transcript (ownership-
358
409
  // checked, capped) is framed into the system prompt so the model REMEMBERS the last leg.
359
410
  // Strictly tolerant — any problem yields no hydration, never a failed start.
@@ -362,19 +413,83 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
362
413
  // mid-thread knows what was already typed. The legs above are excluded: they are already
363
414
  // framed as the resumed transcript, and injecting the same turns twice puts two accounts of
364
415
  // the same exchange in one prompt.
365
- const conversationMessages = await this.loadConversationHistory(conversationId, prior?.LegIDs ?? [], contextUser, provider);
366
- const result = await this.prepareClientSessionOrClose(session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, prior?.Text, configOverridesJson, maxSessionSeconds, applicationId, this.parseAppContext(appContextJson), conversationMessages);
416
+ const conversationMessages = await this.loadConversationHistory(conversationId, prior?.LegIDs ?? [], contextUser, provider, sessionBranchId);
417
+ const result = await this.prepareClientSessionOrClose(session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, prior?.Text, configOverridesJson, maxSessionSeconds, applicationId, this.parseAppContext(appContextJson), conversationMessages, channelCandidates, showsAgentVideo);
418
+ await this.stampVerificationPolicy(session, result.EffectiveConfigJson, lastSessionId, contextUser, provider);
367
419
  // Best-effort restore of the PRIOR session's persisted channel states (e.g. the whiteboard
368
420
  // board). Strictly tolerant — any problem yields a null field, never a failed start.
369
421
  result.PriorChannelStatesJson = (await this.loadPriorChannelStatesJson(lastSessionId, contextUser, provider)) ?? undefined;
370
422
  return result;
371
423
  }
424
+ /**
425
+ * The fork a session starts on: null (Main) when none is named; else the named fork, which must
426
+ * belong to the session's conversation. Returns the fork's stored id.
427
+ * @throws when a fork is named without a conversation, or is not a fork of it, or the fork rows cannot be read
428
+ */
429
+ async resolveSessionBranch(conversationId, branchId, contextUser, provider) {
430
+ const named = branchId?.trim();
431
+ if (!named) {
432
+ return null;
433
+ }
434
+ if (!conversationId) {
435
+ throw new Error(`StartRealtimeClientSession: fork ${named} needs a conversation`);
436
+ }
437
+ try {
438
+ const scope = await ConversationEngine.LoadScope(conversationId, named, contextUser, provider);
439
+ return scope.BranchID;
440
+ }
441
+ catch (error) {
442
+ throw new Error(`StartRealtimeClientSession: fork ${named} is not a readable fork of conversation ${conversationId}: ${error.message}`);
443
+ }
444
+ }
445
+ /**
446
+ * Stamps the identity-verification policy snapshot, the configured unverified duration cap and any
447
+ * verified identity inherited from the resumed session onto the new session's `Config`.
448
+ *
449
+ * The effective config (config cascade) is only known after prepare, so this is a second, narrowly
450
+ * scoped write: it runs only when there is something to stamp. It **fails closed** — a cap that could
451
+ * not be applied would leave the session uncapped, so on a save failure the session is closed and the
452
+ * start fails rather than proceeding without its limits.
453
+ */
454
+ async stampVerificationPolicy(session, effectiveConfigJson, lastSessionId, contextUser, provider) {
455
+ const plan = BuildMintConfigStamps({
456
+ EffectiveRealtime: this.readEffectiveRealtime(effectiveConfigJson),
457
+ ExistingConfigRaw: session.Config_,
458
+ InheritedVerified: await LoadInheritedVerifiedIdentity(lastSessionId, contextUser, provider),
459
+ NowMs: Date.now(),
460
+ });
461
+ if (!plan) {
462
+ return;
463
+ }
464
+ session.Config_ = plan.NextConfigRaw;
465
+ const saved = await RunWithTrustedSessionConfigWrites(() => session.Save());
466
+ if (!saved) {
467
+ LogError(`RealtimeClientSessionResolver.stampVerificationPolicy save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
468
+ await this.sessionManager.CloseSession(session.ID, contextUser, provider, 'Error');
469
+ throw new Error('Failed to apply the session limits; the realtime session was not started.');
470
+ }
471
+ }
472
+ /** The `realtime` section of the resolved effective config JSON, or undefined when absent/unparseable. */
473
+ readEffectiveRealtime(effectiveConfigJson) {
474
+ if (!effectiveConfigJson) {
475
+ return undefined;
476
+ }
477
+ try {
478
+ return JSON.parse(effectiveConfigJson).realtime;
479
+ }
480
+ catch (error) {
481
+ LogError(`RealtimeClientSessionResolver: effective config JSON was unparseable while stamping verification policy: ${error.message}`);
482
+ return undefined;
483
+ }
484
+ }
372
485
  /**
373
486
  * Execute a single tool call the browser relayed from its provider socket and return the
374
487
  * serialized result for the browser to relay back to the model.
375
488
  *
376
- * Ownership-gated; the target agent id is read from the **session config** (authoritative), never
377
- * from the client. Heartbeats the session on success.
489
+ * Ownership-gated. The target agent id is read from the session config and `CanRun` on it is
490
+ * checked for the caller on every call, because the owner can edit the config. A direct-action
491
+ * call from a scope-limited caller is refused before any elevation. Heartbeats the session on
492
+ * success.
378
493
  *
379
494
  * @returns The serialized tool result JSON.
380
495
  */
@@ -382,12 +497,17 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
382
497
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
383
498
  const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
384
499
  const config = this.readSessionConfig(session);
500
+ await this.assertCanRunTarget(config.targetAgentID, contextUser, provider);
501
+ if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME && IsScopeLimitedPrincipal(contextUser)) {
502
+ return this.refuseDirectAction(agentSessionId, toolName);
503
+ }
385
504
  // SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
386
505
  // run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
387
506
  // (the caller's role deliberately holds no grants on the run entities). The lead
388
- // targetAgentID comes from the session config and was CanRun-gated at start; the colleague
389
- // union is gated just below, against the CALLER, so elevation never widens agent authority.
507
+ // targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
508
+ // just below, so elevation never widens agent authority.
390
509
  const runUser = ResolveScopedAnonymousRunUser(contextUser);
510
+ const runIDs = await this.runIDVerifier.Verify(session.ID, { CoAgentRunID: config.coAgentRunID, PendingFeedbackRunID: config.pendingFeedbackRunID }, runUser, provider);
391
511
  if (runUser !== contextUser) {
392
512
  LogStatus(`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
393
513
  'under the system user (scoped-anonymous caller).');
@@ -400,20 +520,27 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
400
520
  // Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
401
521
  // colleague in the call is validated against this; absent ⇒ single-target behavior.
402
522
  AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
403
- DirectActions: config.directActions,
523
+ // No DirectActions: the service takes them from the session's start-time projection
524
+ // or the target agent's metadata, never from the owner-editable config.
404
525
  // Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
405
526
  // row and its context-memory scope must stay the person's, not the system user's.
406
527
  AttributionUserID: contextUser.ID,
407
528
  // Nest the delegated target-agent run under the co-agent observability run (when present).
408
- ParentRunID: config.coAgentRunID,
529
+ ParentRunID: runIDs.CoAgentRunID,
530
+ // The client-tool channel is keyed on the browser session id, so the delegated run can call
531
+ // the surface's tools; the session-start app context gives its prompt the surface context.
532
+ BrowserSessionID: userPayload.sessionId,
533
+ // The app context is a snapshot the browser sent, so a run elevated to the system user
534
+ // does not get it in its prompt.
535
+ AppContext: runUser === contextUser ? config.appContext : undefined,
409
536
  Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
410
537
  OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
411
538
  // Resume a previously-paused delegated run (if any) with the user's answer.
412
- ResumeRunID: config.pendingFeedbackRunID,
539
+ ResumeRunID: runIDs.PendingFeedbackRunID,
413
540
  }, runUser, provider);
414
541
  const durationMs = Date.now() - startTime;
415
542
  LogStatus(`ExecuteRealtimeSessionTool: completed relayed tool '${toolName}' (callId: ${callId}) in ${durationMs}ms`);
416
- if (toolName !== 'invoke-target-agent') {
543
+ if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME) {
417
544
  await this.persistDirectActionTurn(session, callId, toolName, argsJson, ResultJson, durationMs, contextUser, provider);
418
545
  }
419
546
  // Roll the paused-run id forward in the session config: clear the one we just consumed, and
@@ -451,6 +578,18 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
451
578
  }), userPayload);
452
579
  };
453
580
  }
581
+ /**
582
+ * The tool result for a direct-action call from a scope-limited caller (see the authorization
583
+ * model above). It has the broker's result shape, so the model hears that the tool is not
584
+ * available and can ask the target agent through `invoke-target-agent` instead.
585
+ */
586
+ refuseDirectAction(agentSessionId, toolName) {
587
+ LogStatus(`ExecuteRealtimeSessionTool: refused direct action '${toolName}' for a scope-limited caller on session ${agentSessionId}.`);
588
+ return JSON.stringify({
589
+ success: false,
590
+ output: `The tool '${toolName}' is not available in this session. Ask the agent to do it with ${INVOKE_TARGET_AGENT_TOOL_NAME}.`,
591
+ });
592
+ }
454
593
  /**
455
594
  * Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
456
595
  * consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
@@ -462,17 +601,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
462
601
  if (!previous && !pausedRunID) {
463
602
  return;
464
603
  }
465
- const next = {
466
- targetAgentID: config.targetAgentID,
467
- coAgentRunID: config.coAgentRunID,
468
- promptRunID: config.promptRunID,
469
- coAgentRunStepID: config.coAgentRunStepID,
470
- pendingFeedbackRunID: pausedRunID,
471
- // Preserve the server-authoritative voice deadline across config rewrites.
472
- maxSessionDeadlineIso: config.maxSessionDeadlineIso,
473
- applicationID: config.applicationID,
474
- allowedAgents: config.allowedAgents,
475
- };
604
+ // Start from the RAW stored config so every key survives the rewrite — including the
605
+ // server-authoritative `maxSessionDeadlineIso` and `identityVerification` (dropping either would be
606
+ // refused by `MJAIAgentSessionEntityServer`, and silently losing a verification would be worse).
607
+ const next = { ...ParseConfigObject(session.Config_), targetAgentID: config.targetAgentID, pendingFeedbackRunID: pausedRunID };
476
608
  session.Config_ = JSON.stringify(next);
477
609
  if (!(await session.Save())) {
478
610
  LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
@@ -504,9 +636,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
504
636
  // conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
505
637
  // The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
506
638
  // Conversation Detail above deliberately stays on the caller.
507
- const promptRunID = this.readPromptRunID(session);
639
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
640
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
508
641
  if (promptRunID) {
509
- await this.clientSessionService.AppendPromptRunMessage(promptRunID, this.mapTranscriptRoleToChatRole(role), text, replacesPrevious ?? false, ResolveScopedAnonymousRunUser(contextUser), provider);
642
+ await this.clientSessionService.AppendPromptRunMessage(promptRunID, this.mapTranscriptRoleToChatRole(role), text, replacesPrevious ?? false, runUser, provider);
510
643
  }
511
644
  await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
512
645
  return true;
@@ -550,11 +683,11 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
550
683
  if (consent !== true) {
551
684
  return { Success: false, ErrorMessage: 'Recording consent was not granted.' };
552
685
  }
553
- // SCOPED-ANONYMOUS ELEVATION (issue #3371): past the ownership + consent gates, the store is
554
- // server-side plumbing over entities (MJ: AI Agents read, MJ: Files, the file-session link)
555
- // the caller's narrow relay role deliberately does not hold. Attribution flows through the
556
- // session link, so nothing here depends on the caller's identity.
557
- const runUser = ResolveScopedAnonymousRunUser(contextUser);
686
+ // ELEVATION (issue #3371, #5195): past the ownership + consent gates, the store is server-side
687
+ // plumbing over entities (MJ: AI Agents read, MJ: Files, the file-session link, the session's
688
+ // recording stamp) that neither a scoped-anonymous relay role nor the stock UI role holds.
689
+ // Attribution flows through the session link, so nothing here depends on the caller's identity.
690
+ const runUser = ResolveRecordingStoreUser(contextUser);
558
691
  try {
559
692
  const agent = await provider.GetEntityObject('MJ: AI Agents', runUser);
560
693
  if (!(await agent.Load(session.AgentID))) {
@@ -600,11 +733,14 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
600
733
  }
601
734
  /**
602
735
  * Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
603
- * folder (`realtime-recordings/<sessionId>/seg-NNNN.<ext>`) as a raw storage object. Durability
604
- * insurance during a live call so a browser/tab death loses at most the last window; the shards are
605
- * deleted once the canonical consolidated file lands via {@link UploadRealtimeRecording}. Ownership-
606
- * gated; consent was already established at session start. Best-effort — returns `false` (never
607
- * throws) on any problem so a failed shard never disrupts the live call.
736
+ * folder (`realtime-recordings/<sessionId>/seg-NNNN.r<rate>.pcm` for PCM with a rate in its MIME type,
737
+ * else the legacy `seg-NNNN.<ext>`) as a raw storage object. Durability insurance during a live call
738
+ * so a browser/tab death loses at most the last window; the shards are deleted once the canonical
739
+ * consolidated file lands via {@link UploadRealtimeRecording}, or by the session janitor once it has
740
+ * recovered a recording from them when that upload never arrives. Ownership-
741
+ * gated; consent was already established at session start. Refused (`false`) once the session has
742
+ * its consolidated recording, since nothing would delete a shard written after that. Best-effort —
743
+ * returns `false` (never throws) on any problem so a failed shard never disrupts the live call.
608
744
  *
609
745
  * @param agentSessionId The in-progress session (ownership-gated).
610
746
  * @param segmentIndex 0-based shard index within the session.
@@ -616,6 +752,12 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
616
752
  try {
617
753
  const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
618
754
  const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
755
+ // The consolidated recording already landed and its upload deleted the shards; a shard
756
+ // arriving now (one still on the wire at hang-up) would be an orphan nothing deletes.
757
+ if (session.RecordingFileID) {
758
+ LogStatus(`RealtimeClientSessionResolver.UploadRealtimeRecordingSegment: shard ${segmentIndex} for session ${agentSessionId} refused — the session already has its consolidated recording`);
759
+ return false;
760
+ }
619
761
  // Scoped-anonymous elevation (issue #3371) — same rationale as UploadRealtimeRecording.
620
762
  const runUser = ResolveScopedAnonymousRunUser(contextUser);
621
763
  const agent = await provider.GetEntityObject('MJ: AI Agents', runUser);
@@ -659,11 +801,12 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
659
801
  // Persist the tool turn facts into a hidden ConversationDetail turn so session review
660
802
  // can reconstruct the tool card dynamically on load (Option 1). Best-effort.
661
803
  await this.persistDirectActionTurn(session, undefined, toolName, argsJson, resultJson, undefined, contextUser, provider);
662
- const promptRunID = this.readPromptRunID(session);
804
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
805
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
663
806
  if (!promptRunID) {
664
807
  return false;
665
808
  }
666
- return this.clientSessionService.AppendPromptRunMessage(promptRunID, 'assistant', this.formatToolTurn(toolName, argsJson, resultJson), false, ResolveScopedAnonymousRunUser(contextUser), provider);
809
+ return this.clientSessionService.AppendPromptRunMessage(promptRunID, 'assistant', this.formatToolTurn(toolName, argsJson, resultJson), false, runUser, provider);
667
810
  }
668
811
  /**
669
812
  * Formats a co-agent tool call into a compact, human-readable run line: `🔧 <tool> <args> → <result>`,
@@ -726,31 +869,56 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
726
869
  * (observability creation was skipped), a failed load, or a failed save all log and return
727
870
  * `false` — usage relay must never break a live call.
728
871
  *
872
+ * The optional details are the per-modality usage since the last relay: input and output
873
+ * blocks shaped like Core's `RealtimeUsageModalityDetail`, avatar video seconds included. They
874
+ * add into the usage record on the run's `ModelSpecificResponseDetails`, which prices the avatar
875
+ * video at finalize. Only the fields a block defines, with values that are finite numbers of at
876
+ * least 0, are kept; a relay that carries only details (avatar seconds, no tokens) is stored.
877
+ *
729
878
  * @param inputTokens Input-token DELTA to add (negative/non-finite values are clamped to 0).
730
879
  * @param outputTokens Output-token DELTA to add (negative/non-finite values are clamped to 0).
880
+ * @param usageDetailsJson The per-modality usage since the last relay, as JSON (`{ Input, Output }`).
731
881
  * @returns `true` when the accumulated usage was persisted; `false` on any tolerated failure.
732
882
  */
733
- async RelayRealtimeUsage(agentSessionId, inputTokens, outputTokens, { userPayload, providers }) {
883
+ async RelayRealtimeUsage(agentSessionId, inputTokens, outputTokens, { userPayload, providers }, usageDetailsJson) {
734
884
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
735
885
  const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
736
886
  const inputDelta = this.clampTokenDelta(inputTokens);
737
887
  const outputDelta = this.clampTokenDelta(outputTokens);
738
- if (inputDelta === 0 && outputDelta === 0) {
888
+ const details = this.readUsageDetails(usageDetailsJson);
889
+ if (inputDelta === 0 && outputDelta === 0 && !details) {
739
890
  return true; // nothing to add — a no-op flush is a success, not a failure
740
891
  }
741
- const promptRunID = this.readPromptRunID(session);
892
+ // Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
893
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
894
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
742
895
  if (!promptRunID) {
743
896
  return false; // no observability prompt run for this session — logged in the helper
744
897
  }
745
898
  // Delegate to the service so usage writes share the per-run serialization with transcript-message
746
899
  // appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
747
- // Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
748
- return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, ResolveScopedAnonymousRunUser(contextUser), provider);
900
+ return this.clientSessionService.AccumulatePromptRunUsage(promptRunID, inputDelta, outputDelta, runUser, provider, details ?? undefined);
749
901
  }
750
902
  /** Clamps a relayed token delta: negative / non-finite values become 0. */
751
903
  clampTokenDelta(value) {
752
904
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
753
905
  }
906
+ /**
907
+ * Reads the relayed per-modality usage, or `null` when there is none worth storing: absent,
908
+ * malformed, longer than a relay ever needs (a client cannot make the server parse an
909
+ * arbitrarily large string), or holding no amount above 0.
910
+ */
911
+ readUsageDetails(usageDetailsJson) {
912
+ if (typeof usageDetailsJson !== 'string') {
913
+ return null;
914
+ }
915
+ if (usageDetailsJson.length > MAX_USAGE_DETAILS_JSON_LENGTH) {
916
+ LogError(`RelayRealtimeUsage: usage details of ${usageDetailsJson.length} characters exceed ${MAX_USAGE_DETAILS_JSON_LENGTH} — details dropped.`);
917
+ return null;
918
+ }
919
+ const details = ParseRealtimeUsageRecord(usageDetailsJson);
920
+ return HasRealtimeUsage(details) ? details : null;
921
+ }
754
922
  /**
755
923
  * Sanitizes client-supplied waveform peaks before they're persisted: drops anything that isn't a
756
924
  * finite number, clamps each to `[0, 1]`, and caps the array length so a hostile client can't bloat
@@ -769,23 +937,29 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
769
937
  }
770
938
  /**
771
939
  * Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
772
- * throw-on-missing-target semantics — usage relay is best-effort, so a missing/malformed
773
- * config or absent id just logs and returns `null`.
940
+ * throw-on-missing-target semantics, and keeps it only when it is the prompt run of the
941
+ * session's co-agent run (the owner can edit the config). Usage relay is best-effort, so a
942
+ * missing/malformed config, an absent id, or an id of another session just logs and returns `null`.
943
+ *
944
+ * @param runUser The user the prompt-run write runs as; the check runs as the same user.
774
945
  */
775
- readPromptRunID(session) {
776
- try {
777
- const config = this.readSessionConfig(session);
778
- if (config.promptRunID) {
779
- return config.promptRunID;
780
- }
946
+ async readPromptRunID(session, runUser, provider) {
947
+ const config = this.tryReadSessionConfig(session);
948
+ if (!config) {
949
+ LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
950
+ return null;
951
+ }
952
+ if (!config.promptRunID) {
781
953
  LogStatus(`RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
782
954
  'creation was skipped or failed) — usage delta dropped.');
783
955
  return null;
784
956
  }
785
- catch {
786
- LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
957
+ const verified = await this.runIDVerifier.Verify(session.ID, { CoAgentRunID: config.coAgentRunID, PromptRunID: config.promptRunID }, runUser, provider);
958
+ if (!verified.PromptRunID) {
959
+ LogError(`RelayRealtimeUsage: session ${session.ID} names a promptRunID that is not its co-agent prompt run — ignored.`);
787
960
  return null;
788
961
  }
962
+ return verified.PromptRunID;
789
963
  }
790
964
  /**
791
965
  * Persist an interactive channel's state of record (e.g. the live whiteboard's serialized
@@ -811,6 +985,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
811
985
  async SaveSessionChannelState(agentSessionId, channelName, stateJson, { userPayload, providers }) {
812
986
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
813
987
  const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
988
+ if (!this.isChannelInSessionScope(session, channelName)) {
989
+ LogError(`SaveSessionChannelState: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — state not saved.`);
990
+ return false;
991
+ }
814
992
  if (stateJson.length > MAX_CHANNEL_STATE_CHARS) {
815
993
  LogError(`SaveSessionChannelState: rejected oversized state for session ${agentSessionId} / channel '${channelName}' ` +
816
994
  `(${stateJson.length} chars > ${MAX_CHANNEL_STATE_CHARS}).`);
@@ -873,6 +1051,11 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
873
1051
  async SaveSessionChannelArtifact(agentSessionId, channelName, name, contentJson, { userPayload, providers }) {
874
1052
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
875
1053
  const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1054
+ if (!this.isChannelInSessionScope(session, channelName)) {
1055
+ const message = `SaveSessionChannelArtifact: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — nothing saved.`;
1056
+ LogError(message);
1057
+ return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
1058
+ }
876
1059
  if (contentJson.length > MAX_CHANNEL_STATE_CHARS) {
877
1060
  const message = `SaveSessionChannelArtifact: rejected oversized content for session ${agentSessionId} / channel '${channelName}' ` +
878
1061
  `(${contentJson.length} chars > ${MAX_CHANNEL_STATE_CHARS}).`;
@@ -900,8 +1083,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
900
1083
  return { contextUser, provider: GetReadWriteProvider(providers) };
901
1084
  }
902
1085
  /**
903
- * Authorization gate for {@link RealtimeClientSessionResolver.StartRealtimeClientSession}: the
904
- * caller must be able to run the **target** agent. Denial throws (no session is created).
1086
+ * Authorization gate for the **target** agent: the caller must be able to run it. Denial throws.
1087
+ * {@link RealtimeClientSessionResolver.StartRealtimeClientSession} runs it before any session is
1088
+ * created, and {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} runs it on every
1089
+ * relayed call, because the owner can edit the target in the session config.
905
1090
  */
906
1091
  async assertCanRunTarget(targetAgentId, contextUser, _provider) {
907
1092
  const canRun = await AIAgentPermissionHelper.HasPermission(targetAgentId, contextUser, 'run');
@@ -1123,8 +1308,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1123
1308
  * resumed session remembers the previous leg(s).
1124
1309
  * @param conversationMessages Optional capped history of the TEXT conversation this session is
1125
1310
  * starting from (from {@link loadConversationHistory}) — framed as "Conversation so far".
1311
+ * @param showsAgentVideo `false` when the app shows no agent video: the session asks for no avatar (`host`).
1126
1312
  */
1127
- async prepareClientSessionOrClose(session, coAgentID, targetAgentId, contextUser, provider, preferredModelId, clientTools, priorTranscript, configOverridesJson, maxSessionSeconds, applicationId, appContext, conversationMessages) {
1313
+ async prepareClientSessionOrClose(session, coAgentID, targetAgentId, contextUser, provider, preferredModelId, clientTools, priorTranscript, configOverridesJson, maxSessionSeconds, applicationId, appContext, conversationMessages, channelCandidates, showsAgentVideo) {
1128
1314
  const prep = await this.clientSessionService.PrepareClientSession({
1129
1315
  CoAgentID: coAgentID,
1130
1316
  TargetAgentID: targetAgentId,
@@ -1150,6 +1336,14 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1150
1336
  // (allowed-agent union); the snapshot is injected into the system prompt at mint.
1151
1337
  ApplicationID: applicationId,
1152
1338
  AppContext: appContext,
1339
+ // Scope-limited callers get no direct actions (see the authorization model above), so
1340
+ // the model is not given tools that the relay refuses.
1341
+ ExcludeDirectActions: IsScopeLimitedPrincipal(contextUser),
1342
+ // The channels the browser could mount: scoped here (registry + cascade) and handed back
1343
+ // as the resolved policy; the declared tools are narrowed to match.
1344
+ ChannelCandidates: channelCandidates,
1345
+ // An app with no channel that shows the agent's video: no avatar is asked for (reason `host`).
1346
+ ShowsAgentVideo: showsAgentVideo ?? undefined,
1153
1347
  },
1154
1348
  // SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
1155
1349
  // observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
@@ -1162,7 +1356,8 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1162
1356
  await this.sessionManager.CloseSession(session.ID, contextUser, provider, 'Error');
1163
1357
  throw new Error(prep.ErrorMessage ?? 'Failed to prepare the client realtime session.');
1164
1358
  }
1165
- await this.persistObservabilityRunIDs(session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID, applicationId, prep.EffectiveConfig?.realtime?.allowedAgents, prep.EffectiveConfig?.realtime?.directActions);
1359
+ await this.persistObservabilityRunIDs(session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID, applicationId, prep.EffectiveConfig?.realtime?.allowedAgents, prep.EffectiveConfig?.realtime?.directActions, prep.ClientPolicy?.Channels.map((c) => c.Key), appContext);
1360
+ this.pruneExcludedServerChannels(session.ID, prep.ClientPolicy);
1166
1361
  const cfg = prep.ClientConfig;
1167
1362
  return {
1168
1363
  AgentSessionId: session.ID,
@@ -1171,40 +1366,91 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1171
1366
  Model: cfg.Model,
1172
1367
  EphemeralToken: cfg.EphemeralToken,
1173
1368
  ExpiresAt: cfg.ExpiresAt,
1369
+ Transport: cfg.Transport,
1370
+ RelayUrl: cfg.RelayUrl,
1174
1371
  SessionConfigJson: JSON.stringify(cfg.SessionConfig),
1175
1372
  ModelName: prep.ModelName,
1176
1373
  DriverClass: prep.DriverClass,
1177
1374
  NarrationInstructionsTemplate: prep.NarrationInstructionsTemplate,
1178
1375
  NarrationPaceMs: prep.NarrationPaceMs,
1179
1376
  EffectiveConfigJson: prep.EffectiveConfig ? JSON.stringify(prep.EffectiveConfig) : undefined,
1377
+ ClientPolicyJson: prep.ClientPolicy ? JSON.stringify(prep.ClientPolicy) : undefined,
1378
+ AvatarStatusJson: prep.AvatarStatus ? JSON.stringify(prep.AvatarStatus) : undefined,
1180
1379
  };
1181
1380
  }
1381
+ /**
1382
+ * Drops the server-side channel plugins of channels the resolved scope left out. Server plugins
1383
+ * start when the session row is created — before the scope is known — so an excluded channel's
1384
+ * server half would otherwise keep running. A no-op when the caller reported no candidates.
1385
+ */
1386
+ pruneExcludedServerChannels(agentSessionID, policy) {
1387
+ const excluded = (policy?.ExcludedChannels ?? []).map((e) => e.Key);
1388
+ if (excluded.length === 0) {
1389
+ return;
1390
+ }
1391
+ const dropped = RealtimeChannelServerHost.Instance.PruneSessionChannels(agentSessionID, excluded);
1392
+ if (dropped.length > 0) {
1393
+ LogStatus(`StartRealtimeClientSession: dropped the server plugins of out-of-scope channel(s) [${dropped.join(', ')}] for session ${agentSessionID}.`);
1394
+ }
1395
+ }
1182
1396
  /**
1183
1397
  * Writes the co-agent observability run ids into the session's `Config_` alongside the
1184
- * authoritative `targetAgentID`, then saves the session. These ids are read back on close to
1185
- * finalize the runs (and on relay to nest delegated runs). Best-effort: a save failure is logged,
1186
- * not thrown — the voice session still proceeds, it just won't carry the run ids.
1187
- */
1188
- async persistObservabilityRunIDs(session, targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID, applicationID, allowedAgents, directActions) {
1189
- // Preserve any server-authoritative voice deadline stamped at session start (this rebuilds the
1190
- // full config, so read the existing value forward rather than dropping it).
1191
- const existing = this.tryReadSessionConfig(session);
1398
+ * `targetAgentID`, then saves the session. These ids are read back on close to
1399
+ * finalize the runs (and on relay to nest delegated runs). The app awareness (application id,
1400
+ * allowed agents, direct actions, app-context snapshot) is written with them; relays read the
1401
+ * allowed agents and the app context, never the direct actions.
1402
+ * Best-effort: a save failure is logged, not thrown — the voice session still proceeds, it just
1403
+ * won't carry the run ids.
1404
+ */
1405
+ async persistObservabilityRunIDs(session, targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID, applicationID, allowedAgents, directActions, channels, appContext) {
1406
+ // Start from the RAW stored config so every key set at session start survives — the
1407
+ // server-authoritative voice deadline (`maxSessionDeadlineIso`) and the per-session media-kit
1408
+ // override among them — rather than rebuilding from a whitelist that silently drops the rest.
1192
1409
  const config = {
1410
+ ...ParseConfigObject(session.Config_),
1193
1411
  targetAgentID,
1194
1412
  coAgentRunID,
1195
1413
  promptRunID,
1196
1414
  coAgentRunStepID,
1197
- maxSessionDeadlineIso: existing?.maxSessionDeadlineIso,
1198
1415
  applicationID,
1416
+ appContext: this.appContextToStore(appContext, session.ID),
1199
1417
  allowedAgents: allowedAgents && allowedAgents.length > 0 ? allowedAgents : undefined,
1200
1418
  directActions,
1419
+ channels,
1201
1420
  };
1202
1421
  session.Config_ = JSON.stringify(config);
1203
- const saved = await session.Save();
1422
+ // Every value written here is server-decided (run ids, the resolved target, the in-scope channel
1423
+ // set), and `channels` is a server-authoritative key the owner may not write — so this save runs in
1424
+ // the trusted scope. Protected keys already in the stored config are carried forward unchanged.
1425
+ const saved = await RunWithTrustedSessionConfigWrites(() => session.Save());
1204
1426
  if (!saved) {
1205
1427
  LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
1206
1428
  }
1207
1429
  }
1430
+ /**
1431
+ * Returns the app-context snapshot to store on the session config. A snapshot whose JSON is longer
1432
+ * than {@link MAX_CLIENT_TOOLS_JSON_CHARS} is not stored: a warning is logged and `undefined` is
1433
+ * returned, so the session still starts and its delegated runs get no app context.
1434
+ *
1435
+ * @param appContext The snapshot the browser sent at session start, or undefined.
1436
+ * @param sessionID The session the snapshot belongs to (for the warning).
1437
+ * @returns The snapshot to store, or `undefined`.
1438
+ */
1439
+ appContextToStore(appContext, sessionID) {
1440
+ if (!appContext) {
1441
+ return undefined;
1442
+ }
1443
+ const chars = JSON.stringify(appContext).length;
1444
+ if (chars > MAX_CLIENT_TOOLS_JSON_CHARS) {
1445
+ LogErrorEx({
1446
+ message: `RealtimeClientSessionResolver: app context for session ${sessionID} not stored — ${chars} chars ` +
1447
+ `exceeds the ${MAX_CLIENT_TOOLS_JSON_CHARS} cap; delegated runs get no app context.`,
1448
+ severity: 'warning',
1449
+ });
1450
+ return undefined;
1451
+ }
1452
+ return appContext;
1453
+ }
1208
1454
  /**
1209
1455
  * Loads a session, enforcing inbound ownership (`UserID === contextUser.ID`) and that it is not
1210
1456
  * `Closed`. Throws on any violation. Returns the loaded session for the caller to read from.
@@ -1399,6 +1645,10 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1399
1645
  * as the resumed transcript are removed, so the prompt never carries two accounts of one
1400
1646
  * exchange.
1401
1647
  *
1648
+ * Only the rows on the session's path that no rerun replaced are read: the fork it started on,
1649
+ * or Main, in `Sequence` order. When that path cannot be built — its branch rows are missing or invalid — the session
1650
+ * starts with no history, never with every branch's rows.
1651
+ *
1402
1652
  * Strictly tolerant — no conversation, a failed read, or any throw yields `[]`, never a failed
1403
1653
  * session start. The call still works; it just starts cold.
1404
1654
  *
@@ -1406,19 +1656,28 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1406
1656
  * @param coveredSessionIDs Session ids already framed via `PriorTranscript`.
1407
1657
  * @param contextUser The calling user (RunView scope — the read is row-level-secured).
1408
1658
  * @param provider The request-scoped metadata provider.
1659
+ * @param branchId The fork the session started on; null is Main.
1409
1660
  * @returns The capped history, oldest first.
1410
1661
  */
1411
- async loadConversationHistory(conversationId, coveredSessionIDs, contextUser, provider) {
1662
+ async loadConversationHistory(conversationId, coveredSessionIDs, contextUser, provider, branchId) {
1412
1663
  if (!conversationId) {
1413
1664
  return [];
1414
1665
  }
1666
+ const rv = RunView.FromMetadataProvider(provider);
1667
+ let pathFilter;
1668
+ try {
1669
+ pathFilter = await ConversationEngine.BranchPathFilterFresh(conversationId, branchId, contextUser, provider);
1670
+ }
1671
+ catch (error) {
1672
+ LogError(`StartRealtimeClientSession: could not build the path of the session's view for conversation ${conversationId}: ${error.message}`);
1673
+ return [];
1674
+ }
1415
1675
  try {
1416
- const rv = RunView.FromMetadataProvider(provider);
1417
1676
  const result = await rv.RunView({
1418
1677
  EntityName: CONVERSATION_DETAIL_ENTITY,
1419
- ExtraFilter: `ConversationID='${conversationId.replace(/'/g, "''")}'`,
1678
+ ExtraFilter: ConversationEngine.LiveRowsFilter(pathFilter),
1420
1679
  Fields: ['ID', 'Role', 'Message', 'HiddenToUser', 'AgentSessionID', '__mj_CreatedAt'],
1421
- OrderBy: '__mj_CreatedAt ASC',
1680
+ OrderBy: 'Sequence ASC',
1422
1681
  ResultType: 'simple',
1423
1682
  }, contextUser);
1424
1683
  if (!result.Success) {
@@ -1707,6 +1966,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1707
1966
  const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, callerUser);
1708
1967
  detail.NewRecord();
1709
1968
  detail.ConversationID = session.ConversationID;
1969
+ detail.BranchID = this.sessionBranchIdOf(session);
1710
1970
  detail.Role = 'AI';
1711
1971
  detail.HiddenToUser = true;
1712
1972
  detail.Message = 'Artifacts produced during a realtime session (system anchor).';
@@ -1835,18 +2095,50 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1835
2095
  }
1836
2096
  /** Shape check for one client tool declaration: Name/Description non-empty strings, ParametersSchema a plain object. */
1837
2097
  isValidClientTool(candidate) {
1838
- if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
2098
+ return IsValidRealtimeToolDefinition(candidate);
2099
+ }
2100
+ /**
2101
+ * Tolerantly parses the channel candidates the browser reported at mint (the same caps and
2102
+ * tolerance as {@link parseClientTools}: capped, never throws, anything wrong is dropped and
2103
+ * logged). A caller that sent none yields `undefined` — an unscoped session, exactly as before.
2104
+ */
2105
+ parseChannelCandidates(channelCandidatesJson) {
2106
+ const parsed = ParseRealtimeChannelCandidates(channelCandidatesJson);
2107
+ for (const reason of parsed.Rejected) {
2108
+ LogError(`StartRealtimeClientSession: ${reason}.`);
2109
+ }
2110
+ return parsed.Candidates.length > 0 ? parsed.Candidates : undefined;
2111
+ }
2112
+ /**
2113
+ * Whether a session's persisted scope allows a server-side call to name `channelName`. A session
2114
+ * with no persisted scope (it reported no candidates) is unscoped and allows everything; a scoped
2115
+ * one allows only the channels its policy put in the session.
2116
+ */
2117
+ isChannelInSessionScope(session, channelName) {
2118
+ // Read the RAW config: an absent `channels` key means an unscoped session, but anything present
2119
+ // and malformed fails CLOSED — a scope that cannot be read must not widen to "everything".
2120
+ // (`channels` is a server-authoritative key, so the owner cannot delete it to unscope a session.)
2121
+ const channels = ParseConfigObject(session.Config_)['channels'];
2122
+ if (channels === undefined) {
2123
+ return true;
2124
+ }
2125
+ if (!Array.isArray(channels)) {
2126
+ LogError(`RealtimeClientSessionResolver: session ${session.ID} has a malformed channel scope; refusing channel '${channelName}'.`);
1839
2127
  return false;
1840
2128
  }
1841
- const tool = candidate;
1842
- return (typeof tool.Name === 'string' && tool.Name.trim().length > 0 && tool.Name.length <= 128 &&
1843
- typeof tool.Description === 'string' && tool.Description.trim().length > 0 &&
1844
- tool.ParametersSchema !== null && typeof tool.ParametersSchema === 'object' && !Array.isArray(tool.ParametersSchema));
2129
+ const wanted = NormalizeChannelKey(channelName);
2130
+ return channels.some((key) => typeof key === 'string' && NormalizeChannelKey(key) === wanted);
2131
+ }
2132
+ /** The fork the session was started on (its config's `conversationBranchID`); null is Main. */
2133
+ sessionBranchIdOf(session) {
2134
+ const branchId = ParseConfigObject(session.Config_)['conversationBranchID'];
2135
+ return typeof branchId === 'string' && branchId.length > 0 ? branchId : null;
1845
2136
  }
1846
2137
  /**
1847
- * Parses the session's persisted config, returning the authoritative `targetAgentID` plus any
1848
- * observability run ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed
1849
- * or carries no target — a relay cannot proceed without a target.
2138
+ * Parses the session's persisted config, returning its `targetAgentID` plus any observability run
2139
+ * ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed or carries no
2140
+ * target — a relay cannot proceed without a target. The owner can edit the config, so callers
2141
+ * re-check what they act on.
1850
2142
  */
1851
2143
  readSessionConfig(session) {
1852
2144
  const raw = session.Config_;
@@ -1862,10 +2154,16 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1862
2154
  pendingFeedbackRunID: typeof parsed.pendingFeedbackRunID === 'string' ? parsed.pendingFeedbackRunID : undefined,
1863
2155
  maxSessionDeadlineIso: typeof parsed.maxSessionDeadlineIso === 'string' ? parsed.maxSessionDeadlineIso : undefined,
1864
2156
  applicationID: typeof parsed.applicationID === 'string' ? parsed.applicationID : undefined,
2157
+ appContext: parsed.appContext && typeof parsed.appContext === 'object' && !Array.isArray(parsed.appContext)
2158
+ ? parsed.appContext
2159
+ : undefined,
1865
2160
  allowedAgents: Array.isArray(parsed.allowedAgents) ? parsed.allowedAgents : undefined,
1866
2161
  directActions: parsed.directActions && typeof parsed.directActions === 'object' && typeof parsed.directActions.enabled === 'boolean'
1867
2162
  ? parsed.directActions
1868
2163
  : undefined,
2164
+ conversationBranchID: typeof parsed.conversationBranchID === 'string' ? parsed.conversationBranchID : undefined,
2165
+ mediaCollectionID: typeof parsed.mediaCollectionID === 'string' ? parsed.mediaCollectionID : undefined,
2166
+ channels: Array.isArray(parsed.channels) ? parsed.channels.filter((k) => typeof k === 'string') : undefined,
1869
2167
  };
1870
2168
  }
1871
2169
  }
@@ -1931,6 +2229,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
1931
2229
  const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
1932
2230
  detail.NewRecord();
1933
2231
  detail.ConversationID = session.ConversationID;
2232
+ detail.BranchID = this.sessionBranchIdOf(session);
1934
2233
  detail.Role = this.mapTranscriptRole(role);
1935
2234
  detail.Message = text;
1936
2235
  detail.AgentSessionID = session.ID;
@@ -2068,6 +2367,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
2068
2367
  const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
2069
2368
  detail.NewRecord();
2070
2369
  detail.ConversationID = session.ConversationID;
2370
+ detail.BranchID = this.sessionBranchIdOf(session);
2071
2371
  detail.Role = 'AI';
2072
2372
  detail.HiddenToUser = true;
2073
2373
  detail.AgentSessionID = session.ID;
@@ -2129,8 +2429,11 @@ __decorate([
2129
2429
  __param(10, Arg('mediaCollectionId', () => String, { nullable: true })),
2130
2430
  __param(11, Arg('applicationId', () => String, { nullable: true })),
2131
2431
  __param(12, Arg('appContextJson', () => String, { nullable: true })),
2432
+ __param(13, Arg('channelCandidatesJson', () => String, { nullable: true })),
2433
+ __param(14, Arg('showsAgentVideo', () => Boolean, { nullable: true })),
2434
+ __param(15, Arg('conversationBranchId', () => String, { nullable: true })),
2132
2435
  __metadata("design:type", Function),
2133
- __metadata("design:paramtypes", [String, Object, String, String, String, String, String, String, String, Boolean, String, String, String]),
2436
+ __metadata("design:paramtypes", [String, Object, String, String, String, String, String, String, String, Boolean, String, String, String, String, Boolean, String]),
2134
2437
  __metadata("design:returntype", Promise)
2135
2438
  ], RealtimeClientSessionResolver.prototype, "StartRealtimeClientSession", null);
2136
2439
  __decorate([
@@ -2208,8 +2511,9 @@ __decorate([
2208
2511
  __param(1, Arg('inputTokens', () => Int)),
2209
2512
  __param(2, Arg('outputTokens', () => Int)),
2210
2513
  __param(3, Ctx()),
2514
+ __param(4, Arg('usageDetailsJson', () => String, { nullable: true })),
2211
2515
  __metadata("design:type", Function),
2212
- __metadata("design:paramtypes", [String, Number, Number, Object]),
2516
+ __metadata("design:paramtypes", [String, Number, Number, Object, String]),
2213
2517
  __metadata("design:returntype", Promise)
2214
2518
  ], RealtimeClientSessionResolver.prototype, "RelayRealtimeUsage", null);
2215
2519
  __decorate([