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

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 (543) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  4. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  5. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  6. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  7. package/dist/agentSessions/HostInstance.d.ts +75 -11
  8. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  9. package/dist/agentSessions/HostInstance.js +99 -28
  10. package/dist/agentSessions/HostInstance.js.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  12. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  13. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  14. package/dist/agentSessions/SessionJanitor.d.ts +236 -27
  15. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  16. package/dist/agentSessions/SessionJanitor.js +682 -54
  17. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  18. package/dist/agentSessions/SessionManager.d.ts +48 -3
  19. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  20. package/dist/agentSessions/SessionManager.js +112 -9
  21. package/dist/agentSessions/SessionManager.js.map +1 -1
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  25. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  26. package/dist/agentSessions/index.d.ts +2 -0
  27. package/dist/agentSessions/index.d.ts.map +1 -1
  28. package/dist/agentSessions/index.js +10 -3
  29. package/dist/agentSessions/index.js.map +1 -1
  30. package/dist/apiRoot.d.ts +32 -0
  31. package/dist/apiRoot.d.ts.map +1 -0
  32. package/dist/apiRoot.js +67 -0
  33. package/dist/apiRoot.js.map +1 -0
  34. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  35. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  36. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  37. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  38. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  39. package/dist/auth/LoginScreenEngine.js +57 -0
  40. package/dist/auth/LoginScreenEngine.js.map +1 -0
  41. package/dist/auth/actionAuthorization.d.ts +40 -0
  42. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  43. package/dist/auth/actionAuthorization.js +94 -0
  44. package/dist/auth/actionAuthorization.js.map +1 -0
  45. package/dist/auth/index.d.ts +1 -0
  46. package/dist/auth/index.d.ts.map +1 -1
  47. package/dist/auth/index.js +4 -10
  48. package/dist/auth/index.js.map +1 -1
  49. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  50. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  51. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  52. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  53. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  54. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  55. package/dist/auth/newUsers.d.ts +10 -0
  56. package/dist/auth/newUsers.d.ts.map +1 -1
  57. package/dist/auth/newUsers.js +80 -11
  58. package/dist/auth/newUsers.js.map +1 -1
  59. package/dist/config.d.ts +2514 -22
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +166 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/context.d.ts +8 -0
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +14 -11
  66. package/dist/context.js.map +1 -1
  67. package/dist/coreRoutes.d.ts +29 -0
  68. package/dist/coreRoutes.d.ts.map +1 -0
  69. package/dist/coreRoutes.js +142 -0
  70. package/dist/coreRoutes.js.map +1 -0
  71. package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
  72. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  73. package/dist/generated/graphql-schemas/__mj.js +2456 -7
  74. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  75. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  76. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  77. package/dist/generic/CacheInvalidationResolver.js +59 -19
  78. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  79. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  80. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  81. package/dist/generic/PushStatusResolver.js +1 -1
  82. package/dist/generic/PushStatusResolver.js.map +1 -1
  83. package/dist/generic/ResolverBase.d.ts +123 -48
  84. package/dist/generic/ResolverBase.d.ts.map +1 -1
  85. package/dist/generic/ResolverBase.js +293 -305
  86. package/dist/generic/ResolverBase.js.map +1 -1
  87. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  88. package/dist/generic/RunViewResolver.js +16 -0
  89. package/dist/generic/RunViewResolver.js.map +1 -1
  90. package/dist/globalCors.d.ts +33 -0
  91. package/dist/globalCors.d.ts.map +1 -0
  92. package/dist/globalCors.js +58 -0
  93. package/dist/globalCors.js.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +219 -278
  97. package/dist/index.js.map +1 -1
  98. package/dist/isolatedProvider.d.ts +20 -0
  99. package/dist/isolatedProvider.d.ts.map +1 -0
  100. package/dist/isolatedProvider.js +22 -0
  101. package/dist/isolatedProvider.js.map +1 -0
  102. package/dist/multiTenancy/index.d.ts.map +1 -1
  103. package/dist/multiTenancy/index.js +22 -2
  104. package/dist/multiTenancy/index.js.map +1 -1
  105. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  106. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  107. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  108. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  109. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  110. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  111. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  112. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  113. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  114. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  115. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  116. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  117. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  118. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  119. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +139 -0
  120. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  121. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  122. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  123. package/dist/realtimeSessions/RealtimeVerifyRouter.js +99 -0
  124. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  125. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  126. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  127. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  128. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  129. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  130. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  131. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  132. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  133. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  134. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  135. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  136. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  137. package/dist/realtimeSessions/index.d.ts +19 -0
  138. package/dist/realtimeSessions/index.d.ts.map +1 -0
  139. package/dist/realtimeSessions/index.js +19 -0
  140. package/dist/realtimeSessions/index.js.map +1 -0
  141. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  142. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  143. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  144. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  145. package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
  146. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  147. package/dist/realtimeSessions/sessionAccess.js +94 -0
  148. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  149. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  150. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  151. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  152. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  153. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  154. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  155. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  156. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  157. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  158. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  159. package/dist/realtimeSessions/verificationCore.js +356 -0
  160. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  161. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  162. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  163. package/dist/realtimeSessions/verificationPages.js +110 -0
  164. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  165. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  166. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  167. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  168. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  169. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  170. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  171. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  172. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  173. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  174. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  175. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  176. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  177. package/dist/resolvers/ActionResolver.d.ts +31 -5
  178. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  179. package/dist/resolvers/ActionResolver.js +75 -14
  180. package/dist/resolvers/ActionResolver.js.map +1 -1
  181. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  182. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  183. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  184. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  185. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  186. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  187. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  188. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  189. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  190. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  191. package/dist/resolvers/CacheStatsResolver.js +113 -2
  192. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  193. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  194. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  195. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  196. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  197. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  198. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  199. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  200. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  201. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  202. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  203. package/dist/resolvers/DatasetResolver.js +57 -8
  204. package/dist/resolvers/DatasetResolver.js.map +1 -1
  205. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  206. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  207. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  208. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  209. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
  210. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  211. package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
  212. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  213. package/dist/resolvers/FileResolver.d.ts +82 -11
  214. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  215. package/dist/resolvers/FileResolver.js +279 -101
  216. package/dist/resolvers/FileResolver.js.map +1 -1
  217. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  218. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  219. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  220. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  221. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  222. package/dist/resolvers/GetDataResolver.js +6 -3
  223. package/dist/resolvers/GetDataResolver.js.map +1 -1
  224. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  225. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  226. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  227. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  228. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  229. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  230. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  231. package/dist/resolvers/MCPResolver.js +9 -4
  232. package/dist/resolvers/MCPResolver.js.map +1 -1
  233. package/dist/resolvers/QueryResolver.d.ts +2 -0
  234. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  235. package/dist/resolvers/QueryResolver.js +13 -0
  236. package/dist/resolvers/QueryResolver.js.map +1 -1
  237. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  238. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  239. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  240. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  241. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +157 -33
  242. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  243. package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
  244. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  245. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  246. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  247. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  248. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  249. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  250. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  251. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  252. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  253. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  254. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  255. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  256. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  257. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  258. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  259. package/dist/resolvers/RunAIAgentResolver.js +100 -14
  260. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  261. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  262. package/dist/resolvers/RunTemplateResolver.js +12 -11
  263. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  264. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  265. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  266. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  267. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  268. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  269. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  270. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  271. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  272. package/dist/resolvers/SyncDataResolver.js +6 -2
  273. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  274. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  275. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  276. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  277. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  278. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  279. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  280. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  281. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  282. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  283. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  284. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  285. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  286. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  287. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  288. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  289. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  290. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  291. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  292. package/dist/resolvers/UserAvatarResolver.js +124 -0
  293. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  294. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  295. package/dist/resolvers/UserResolver.js +2 -17
  296. package/dist/resolvers/UserResolver.js.map +1 -1
  297. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  298. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  299. package/dist/resolvers/agent-run-data-guard.js +72 -0
  300. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  301. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  302. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  303. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  304. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  305. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  306. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  307. package/dist/resolvers/avatarInputValidation.js +97 -0
  308. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  309. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  310. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  311. package/dist/resolvers/currentUserRoles.js +30 -0
  312. package/dist/resolvers/currentUserRoles.js.map +1 -1
  313. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  314. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  315. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  316. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  317. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  318. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  319. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  320. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  321. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  322. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  323. package/dist/rest/ViewOperationsHandler.js +21 -12
  324. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  325. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  326. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  327. package/dist/rest/setupRESTEndpoints.js +6 -0
  328. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  329. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  330. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  331. package/dist/serverExtensionReservedRoots.js +14 -17
  332. package/dist/serverExtensionReservedRoots.js.map +1 -1
  333. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  334. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  335. package/dist/services/MJStorageBlobStore.js +25 -19
  336. package/dist/services/MJStorageBlobStore.js.map +1 -1
  337. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  338. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  339. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  340. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  341. package/dist/services/TaskGraphActionRunner.js +27 -5
  342. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  343. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  344. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  345. package/dist/services/TaskGraphAgentRunner.js +157 -9
  346. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  347. package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
  348. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  349. package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
  350. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  351. package/dist/sharedCache.d.ts +105 -0
  352. package/dist/sharedCache.d.ts.map +1 -0
  353. package/dist/sharedCache.js +279 -0
  354. package/dist/sharedCache.js.map +1 -0
  355. package/dist/types.d.ts +6 -0
  356. package/dist/types.d.ts.map +1 -1
  357. package/dist/types.js.map +1 -1
  358. package/package.json +99 -96
  359. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  360. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  361. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  362. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  363. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  364. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  365. package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
  366. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  367. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  368. package/src/__tests__/HostInstance.test.ts +84 -0
  369. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  370. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  371. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  372. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  373. package/src/__tests__/MeetingResolver.test.ts +75 -1
  374. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  375. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  376. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  377. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  378. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  379. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  380. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  381. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  382. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  383. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  384. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  385. package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
  386. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  387. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  388. package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
  389. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
  390. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  391. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  392. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  393. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  394. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  395. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  396. package/src/__tests__/SessionJanitor.test.ts +1203 -21
  397. package/src/__tests__/SessionManager.test.ts +240 -17
  398. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  399. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  400. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  401. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  402. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
  403. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  404. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  405. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  406. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  407. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  408. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  409. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  410. package/src/__tests__/actionAuthorization.test.ts +130 -0
  411. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  412. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  413. package/src/__tests__/apiRootBoot.test.ts +202 -0
  414. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  415. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  416. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  417. package/src/__tests__/globalCors.test.ts +217 -0
  418. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  419. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  420. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  421. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  422. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  423. package/src/__tests__/multiTenancy.test.ts +1 -1
  424. package/src/__tests__/newUsers.template.test.ts +151 -0
  425. package/src/__tests__/newUsers.test.ts +74 -5
  426. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  427. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  428. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  429. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  430. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  431. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  432. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  433. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  434. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  435. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  436. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  437. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  438. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  439. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  440. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  441. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  442. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  443. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  444. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  445. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
  446. package/src/__tests__/sharedCache.test.ts +312 -0
  447. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  448. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  449. package/src/agentSessions/HostInstance.ts +124 -28
  450. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  451. package/src/agentSessions/SessionJanitor.ts +821 -59
  452. package/src/agentSessions/SessionManager.ts +138 -26
  453. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  454. package/src/agentSessions/index.ts +10 -3
  455. package/src/apiRoot.ts +78 -0
  456. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  457. package/src/auth/LoginScreenEngine.ts +50 -0
  458. package/src/auth/actionAuthorization.ts +116 -0
  459. package/src/auth/index.ts +4 -8
  460. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  461. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  462. package/src/auth/newUsers.ts +99 -11
  463. package/src/config.ts +171 -5
  464. package/src/context.ts +15 -10
  465. package/src/coreRoutes.ts +166 -0
  466. package/src/generated/graphql-schemas/__mj.ts +1815 -57
  467. package/src/generic/CacheInvalidationResolver.ts +73 -18
  468. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  469. package/src/generic/PushStatusResolver.ts +1 -1
  470. package/src/generic/ResolverBase.ts +335 -326
  471. package/src/generic/RunViewResolver.ts +21 -0
  472. package/src/globalCors.ts +71 -0
  473. package/src/index.ts +231 -300
  474. package/src/isolatedProvider.ts +22 -0
  475. package/src/multiTenancy/index.ts +24 -2
  476. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  477. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  478. package/src/realtimeSessions/README.md +179 -0
  479. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  480. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +168 -0
  481. package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
  482. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  483. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  484. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  485. package/src/realtimeSessions/index.ts +18 -0
  486. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  487. package/src/realtimeSessions/sessionAccess.ts +145 -0
  488. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  489. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  490. package/src/realtimeSessions/verificationCore.ts +502 -0
  491. package/src/realtimeSessions/verificationPages.ts +149 -0
  492. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  493. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  494. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  495. package/src/resolvers/ActionResolver.ts +84 -21
  496. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  497. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  498. package/src/resolvers/CacheStatsResolver.ts +89 -2
  499. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  500. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  501. package/src/resolvers/DatasetResolver.ts +70 -10
  502. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  503. package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
  504. package/src/resolvers/FileResolver.ts +321 -106
  505. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  506. package/src/resolvers/GetDataResolver.ts +6 -3
  507. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  508. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  509. package/src/resolvers/MCPResolver.ts +8 -5
  510. package/src/resolvers/QueryResolver.ts +11 -0
  511. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  512. package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
  513. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  514. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  515. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  516. package/src/resolvers/RunAIAgentResolver.ts +116 -14
  517. package/src/resolvers/RunTemplateResolver.ts +13 -12
  518. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  519. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  520. package/src/resolvers/SyncDataResolver.ts +6 -2
  521. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  522. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  523. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  524. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  525. package/src/resolvers/UserAvatarResolver.ts +111 -0
  526. package/src/resolvers/UserResolver.ts +2 -16
  527. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  528. package/src/resolvers/agent-run-data-guard.ts +89 -0
  529. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  530. package/src/resolvers/avatarInputValidation.ts +123 -0
  531. package/src/resolvers/currentUserRoles.ts +32 -1
  532. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  533. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  534. package/src/rest/ViewOperationsHandler.ts +29 -13
  535. package/src/rest/setupRESTEndpoints.ts +6 -0
  536. package/src/serverExtensionReservedRoots.ts +14 -17
  537. package/src/services/MJStorageBlobStore.ts +31 -30
  538. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  539. package/src/services/TaskGraphActionRunner.ts +25 -5
  540. package/src/services/TaskGraphAgentRunner.ts +188 -11
  541. package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
  542. package/src/sharedCache.ts +305 -0
  543. package/src/types.ts +7 -0
@@ -11,9 +11,9 @@
11
11
  * and **executes** every tool call the browser relays back. The three mutations here cover the MVP:
12
12
  *
13
13
  * 1. {@link RealtimeClientSessionResolver.StartRealtimeClientSession} — authorize, create the
14
- * session (storing the target agent id server-side, authoritatively), mint the ephemeral config.
15
- * 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call,
16
- * reading the target agent id from the session (NOT the client) so the browser can't swap targets.
14
+ * session (storing the target agent id on it), mint the ephemeral config.
15
+ * 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
16
+ * for the target agent stored on the session, re-authorized for the caller on every call.
17
17
  * 3. {@link RealtimeClientSessionResolver.RelayRealtimeTranscript} — persist a transcript turn as a
18
18
  * `Conversation Detail`.
19
19
  *
@@ -21,12 +21,19 @@
21
21
  * - **Start**: the caller must have `CanRun` on the *target* agent (the Realtime Co-Agent is an internal
22
22
  * orchestration agent, so the meaningful gate is the agent doing the real work).
23
23
  * - **Execute / Relay**: inbound ownership — the session's `UserID` must equal `contextUser.ID`.
24
+ * - **Session `Config`**: the owner can edit it through the generic entity API, so nothing in it is
25
+ * trusted. Execute re-checks `CanRun` on its target, direct actions never come from it, and its
26
+ * run ids are used only when {@link SessionRunIDVerifier} finds them to be records of the session.
27
+ * - **Scope-limited callers** (anonymous and resource-scoped magic-link sessions, including public
28
+ * web-widget guests): no direct actions. Their scope exists only as row-level filters, which an
29
+ * action can read around, and a scoped anonymous caller's relay runs as the system user. They
30
+ * reach the target agent only through `invoke-target-agent`.
24
31
  *
25
32
  * @module @memberjunction/server
26
33
  */
27
34
  import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
28
35
  import { AppContext, UserPayload } from '../types.js';
29
- import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView, BaseEntity } from '@memberjunction/core';
36
+ import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogErrorEx, LogStatus, RunView, BaseEntity } from '@memberjunction/core';
30
37
  import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
31
38
  import {
32
39
  MJAIAgentEntity,
@@ -36,9 +43,10 @@ import {
36
43
  MJArtifactVersionEntity,
37
44
  MJConversationDetailArtifactEntity,
38
45
  MJConversationDetailEntity,
46
+ ConversationEngine,
39
47
  } from '@memberjunction/core-entities';
40
48
  import { AIEngine } from '@memberjunction/aiengine';
41
- import { AIAgentPermissionHelper, AIEngineBase } from '@memberjunction/ai-engine-base';
49
+ import { AIAgentPermissionHelper, AIEngineBase, HasRealtimeUsage, ParseRealtimeUsageRecord, type RealtimeUsageRecord } from '@memberjunction/ai-engine-base';
42
50
  import {
43
51
  RealtimeClientSessionService,
44
52
  DelegatedRunArtifact,
@@ -50,6 +58,7 @@ import {
50
58
  ResolveEffectiveRealtimeConfig,
51
59
  ResolveRealtimeCoAgentID,
52
60
  FilterAllowedAgentsByCanRun,
61
+ INVOKE_TARGET_AGENT_TOOL_NAME,
53
62
  RealtimeAllowedAgent,
54
63
  RealtimeDirectActionsConfig,
55
64
  REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION,
@@ -58,13 +67,27 @@ import {
58
67
  writeRealtimeRecordingSegment,
59
68
  deleteRealtimeRecordingSegments,
60
69
  } from '@memberjunction/ai-agents';
61
- import { AgentExecutionProgressCallback, AppContextSnapshot } from '@memberjunction/ai-core-plus';
70
+ import {
71
+ AgentExecutionProgressCallback,
72
+ AppContextSnapshot,
73
+ IsValidRealtimeToolDefinition,
74
+ NormalizeChannelKey,
75
+ ParseRealtimeChannelCandidates,
76
+ type RealtimeChannelCandidate,
77
+ type RealtimeSessionClientPolicy
78
+ } from '@memberjunction/ai-core-plus';
62
79
  import { ChatMessage, RealtimeToolDefinition } from '@memberjunction/ai';
63
80
  import { ResolverBase } from '../generic/ResolverBase.js';
64
81
  import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
65
82
  import { GetReadWriteProvider } from '../util.js';
66
83
  import { SessionManager } from '../agentSessions/index.js';
67
- import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../realtimeWidget/widgetGuestElevation.js';
84
+ import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
85
+ import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
86
+ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser, ResolveRecordingStoreUser } from '../realtimeWidget/widgetGuestElevation.js';
87
+ import { RunWithTrustedSessionConfigWrites } from '@memberjunction/core-entities-server';
88
+ import { BuildMintConfigStamps, LoadInheritedVerifiedIdentity } from '../realtimeSessions/sessionConfigStamps.js';
89
+ import { ParseConfigObject } from '../realtimeSessions/verificationCore.js';
90
+ import { LoadOwnedRealtimeSession } from '../realtimeSessions/ownedRealtimeSession.js';
68
91
 
69
92
  /**
70
93
  * Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
@@ -73,6 +96,12 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../
73
96
  */
74
97
  const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
75
98
 
99
+ /**
100
+ * The longest `usageDetailsJson` a usage relay may carry. Every field of both detail blocks fits in a few hundred
101
+ * characters, so anything longer is not a relay the browser runtime sends.
102
+ */
103
+ const MAX_USAGE_DETAILS_JSON_LENGTH = 4096;
104
+
76
105
  /** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
77
106
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
78
107
  const CO_AGENT_ENTITY = 'MJ: AI Agent Co Agents';
@@ -100,7 +129,8 @@ const WHITEBOARD_ARTIFACT_TYPE_NAME = 'Whiteboard';
100
129
  */
101
130
  const MAX_CLIENT_TOOLS = 64;
102
131
  /** Maximum accepted size (chars) of the serialized client tool declarations. Raised in step with
103
- * {@link MAX_CLIENT_TOOLS} — multi-channel tool sets with verbose descriptions + JSON schemas run large. */
132
+ * {@link MAX_CLIENT_TOOLS} — multi-channel tool sets with verbose descriptions + JSON schemas run large.
133
+ * Also the largest serialized app-context snapshot stored on the session config. */
104
134
  const MAX_CLIENT_TOOLS_JSON_CHARS = 256_000;
105
135
  /** Maximum accepted size (chars) of a persisted channel state blob. */
106
136
  const MAX_CHANNEL_STATE_CHARS = 2_000_000;
@@ -118,9 +148,9 @@ const MAX_CONVERSATION_HISTORY_TURNS = 30;
118
148
  const MAX_CONVERSATION_HISTORY_CHARS = 8_000;
119
149
 
120
150
  /**
121
- * Authoritative shape persisted in `AIAgentSession.Config_` for a client-direct voice session.
122
- * The target agent id is stored here at start and read back on every relay — the browser never
123
- * re-supplies it, so it cannot swap targets mid-session.
151
+ * Shape persisted in `AIAgentSession.Config_` for a client-direct voice session. The server writes
152
+ * it, but the session owner can also edit it through the generic entity API, so relays re-check
153
+ * what they read from it (see the authorization model above).
124
154
  */
125
155
  interface RealtimeSessionConfig {
126
156
  /** The top-level target agent the co-agent voices on behalf of. */
@@ -157,6 +187,12 @@ interface RealtimeSessionConfig {
157
187
  * for observability / continuity; absent when the session carries no app context.
158
188
  */
159
189
  applicationID?: string;
190
+ /**
191
+ * The app-context snapshot the browser sent at session start. Passed into each delegated
192
+ * target-agent run so its prompt sees the surface context and tools. Absent when the start carried
193
+ * none, or when its JSON is longer than {@link MAX_CLIENT_TOOLS_JSON_CHARS}.
194
+ */
195
+ appContext?: AppContextSnapshot;
160
196
  /**
161
197
  * The session's effective allowed delegation targets (union from the config cascade, incl. the app's
162
198
  * `RelevantAgents`). Persisted at start so each relayed `invoke-target-agent` call can validate a
@@ -175,6 +211,24 @@ interface RealtimeSessionConfig {
175
211
  * agent default kit is used (the common case).
176
212
  */
177
213
  mediaCollectionID?: string;
214
+ /**
215
+ * The fork (`MJ: Conversation Branches.ID`) the session was started on, fixed at start; absent means
216
+ * Main. Every transcript, direct-action and anchor row the session writes carries it.
217
+ */
218
+ conversationBranchID?: string;
219
+ /**
220
+ * Server-authoritative identity-verification state (policy snapshot taken at mint, pending/verified
221
+ * state written by `RealtimeSessionVerificationService`). Protected from owner writes by
222
+ * `MJAIAgentSessionEntityServer`; opaque to this resolver beyond preserving it across config rewrites.
223
+ */
224
+ identityVerification?: Record<string, unknown>;
225
+ /**
226
+ * The keys of the interactive channels the session's resolved scope put IN the session (registry
227
+ * kill switch, agent and app configuration applied). Persisted at start so a later server-side
228
+ * call naming a channel — a state or artifact save — can check it is actually in scope. Absent
229
+ * (an unscoped session: a client that reported no channel candidates) ⇒ no channel check.
230
+ */
231
+ channels?: string[];
178
232
  }
179
233
 
180
234
  /**
@@ -199,14 +253,32 @@ export class StartRealtimeClientSessionResult {
199
253
  @Field(() => String)
200
254
  Model: string;
201
255
 
202
- /** The short-lived client secret the browser presents to the provider to authenticate. */
256
+ /**
257
+ * The short-lived client secret the browser presents to the provider to authenticate. Empty on a relay session
258
+ * ({@link StartRealtimeClientSessionResult.Transport} `relay`), whose {@link StartRealtimeClientSessionResult.RelayUrl}
259
+ * carries the relay's ticket instead.
260
+ */
203
261
  @Field(() => String)
204
262
  EphemeralToken: string;
205
263
 
206
- /** ISO-8601 timestamp at which {@link StartRealtimeClientSessionResult.EphemeralToken} expires. */
264
+ /** ISO-8601 timestamp at which {@link StartRealtimeClientSessionResult.EphemeralToken} (or a relay session's ticket) expires. */
207
265
  @Field(() => String)
208
266
  ExpiresAt: string;
209
267
 
268
+ /**
269
+ * How the browser reaches the provider: `direct` (with the ephemeral token) or `relay` (through MJAPI's realtime
270
+ * relay at {@link StartRealtimeClientSessionResult.RelayUrl}). Null means direct.
271
+ */
272
+ @Field(() => String, { nullable: true })
273
+ Transport?: string;
274
+
275
+ /**
276
+ * Where a relay session's browser connects (MJAPI's realtime relay). Null on a direct session. The URL carries the
277
+ * session's ticket, so it is a credential and is never logged.
278
+ */
279
+ @Field(() => String, { nullable: true })
280
+ RelayUrl?: string;
281
+
210
282
  /** JSON string of the provider-native session config the browser applies verbatim. */
211
283
  @Field(() => String)
212
284
  SessionConfigJson: string;
@@ -263,6 +335,25 @@ export class StartRealtimeClientSessionResult {
263
335
  */
264
336
  @Field(() => String, { nullable: true })
265
337
  EffectiveConfigJson?: string;
338
+
339
+ /**
340
+ * JSON of the server's resolved channel policy for this session (`RealtimeSessionClientPolicy`):
341
+ * which of the channel candidates the browser reported are in the session (registry kill switch,
342
+ * agent and app `channels` configuration applied), with each one's resolved display and config,
343
+ * plus the app/static client-tool tiers. Null when the caller reported no channel candidates (a
344
+ * client that predates channel scoping) — the browser then resolves the scope locally.
345
+ */
346
+ @Field(() => String, { nullable: true })
347
+ ClientPolicyJson?: string;
348
+
349
+ /**
350
+ * JSON of the session's live-avatar status (`RealtimeAvatarStatus` from `@memberjunction/ai`): whether the voiced
351
+ * agent asked for an avatar, whether the model renders it, and why not. The browser reads it after connecting and
352
+ * tells the user once when the call is audio only. Null when the agent asked for no avatar (its video setting is
353
+ * off).
354
+ */
355
+ @Field(() => String, { nullable: true })
356
+ AvatarStatusJson?: string;
266
357
  }
267
358
 
268
359
  /**
@@ -325,7 +416,7 @@ export class CancelRealtimeSessionToolResult {
325
416
  * Result of {@link RealtimeClientSessionResolver.UploadRealtimeRecording} — a structured
326
417
  * success/failure envelope. A recording-storage failure must never throw to the browser (the
327
418
  * 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.
419
+ * human-readable reason; only ownership/authn violations (from `LoadOwnedRealtimeSession`) still throw.
329
420
  */
330
421
  @ObjectType()
331
422
  export class UploadRealtimeRecordingResult {
@@ -356,6 +447,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
356
447
  // state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
357
448
  private readonly clientSessionService = new RealtimeClientSessionService();
358
449
  private readonly sessionManager = new SessionManager(this.clientSessionService);
450
+ private readonly runIDVerifier = new SessionRunIDVerifier();
359
451
 
360
452
  /**
361
453
  * The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
@@ -392,7 +484,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
392
484
  * agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
393
485
  * see {@link ResolveRealtimeCoAgentID} for the full contract.
394
486
  * 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
395
- * config server-side — this is the authoritative target for all later relays.
487
+ * config — the target later relays use, re-authorized for the caller on each relay.
396
488
  * 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
397
489
  * On failure the just-created session is closed so no half-open session leaks.
398
490
  *
@@ -418,6 +510,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
418
510
  * `TypeConfiguration` ← this). **Authorization-gated**: requires the
419
511
  * `Realtime: Advanced Session Controls` authorization — unauthorized callers receive a
420
512
  * structured rejection (never a silent ignore). Must be a JSON object.
513
+ * @param showsAgentVideo `false` from an app that shows no agent video (no channel of it shows it: the embeddable
514
+ * widgets, the mobile app): the session asks the model for no avatar, and `AvatarStatusJson` says `host`. Absent or
515
+ * `true`: the app may show it. It only ever turns video off, so it needs no authorization.
516
+ * @param conversationBranchId Optional fork (`MJ: Conversation Branches.ID`) the session starts on;
517
+ * omitted is Main. It must be a fork of `conversationId`, else the start fails and no session is
518
+ * created. The session's history is read from that fork's path and its transcript is written there.
421
519
  *
422
520
  * @returns The ephemeral config + session linkage the browser needs to open its socket.
423
521
  */
@@ -436,6 +534,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
436
534
  @Arg('mediaCollectionId', () => String, { nullable: true }) mediaCollectionId?: string,
437
535
  @Arg('applicationId', () => String, { nullable: true }) applicationId?: string,
438
536
  @Arg('appContextJson', () => String, { nullable: true }) appContextJson?: string,
537
+ @Arg('channelCandidatesJson', () => String, { nullable: true }) channelCandidatesJson?: string,
538
+ @Arg('showsAgentVideo', () => Boolean, { nullable: true }) showsAgentVideo?: boolean,
539
+ @Arg('conversationBranchId', () => String, { nullable: true }) conversationBranchId?: string,
439
540
  ): Promise<StartRealtimeClientSessionResult> {
440
541
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
441
542
 
@@ -473,22 +574,32 @@ export class RealtimeClientSessionResolver extends ResolverBase {
473
574
  );
474
575
  }
475
576
  }
476
- const session = await this.sessionManager.CreateSession(
477
- {
478
- agentID: coAgentID,
479
- userID: contextUser.ID,
480
- conversationID: conversationId,
481
- lastSessionID: lastSessionId,
482
- config: JSON.stringify(config),
483
- },
484
- contextUser,
485
- provider,
577
+ const sessionBranchId = await this.resolveSessionBranch(conversationId, conversationBranchId, contextUser, provider);
578
+ if (sessionBranchId) {
579
+ config.conversationBranchID = sessionBranchId;
580
+ }
581
+ // The initial Config carries `maxSessionDeadlineIso` for capped sessions — a server-authoritative key
582
+ // that `MJAIAgentSessionEntityServer` refuses to let the session OWNER write, so this write (and only
583
+ // this one, built entirely from server-decided values) runs in the trusted scope.
584
+ const session = await RunWithTrustedSessionConfigWrites(() =>
585
+ this.sessionManager.CreateSession(
586
+ {
587
+ agentID: coAgentID,
588
+ userID: contextUser.ID,
589
+ conversationID: conversationId,
590
+ lastSessionID: lastSessionId,
591
+ config: JSON.stringify(config),
592
+ },
593
+ contextUser,
594
+ provider,
595
+ ),
486
596
  );
487
597
 
488
598
  // Best-effort: stamp recording-start metadata when the browser captured (with consent) at start.
489
599
  await this.stampRecordingStart(session, recordingConsent, recordingStartedAt);
490
600
 
491
601
  const clientTools = this.parseClientTools(clientToolsJson);
602
+ const channelCandidates = this.parseChannelCandidates(channelCandidatesJson);
492
603
  // Best-effort model-context hydration: the PRIOR session chain's transcript (ownership-
493
604
  // checked, capped) is framed into the system prompt so the model REMEMBERS the last leg.
494
605
  // Strictly tolerant — any problem yields no hydration, never a failed start.
@@ -498,24 +609,103 @@ export class RealtimeClientSessionResolver extends ResolverBase {
498
609
  // framed as the resumed transcript, and injecting the same turns twice puts two accounts of
499
610
  // the same exchange in one prompt.
500
611
  const conversationMessages = await this.loadConversationHistory(
501
- conversationId, prior?.LegIDs ?? [], contextUser, provider,
612
+ conversationId, prior?.LegIDs ?? [], contextUser, provider, sessionBranchId,
502
613
  );
503
614
  const result = await this.prepareClientSessionOrClose(
504
615
  session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, prior?.Text,
505
616
  configOverridesJson, maxSessionSeconds, applicationId, this.parseAppContext(appContextJson), conversationMessages,
617
+ channelCandidates, showsAgentVideo,
506
618
  );
619
+ await this.stampVerificationPolicy(session, result.EffectiveConfigJson, lastSessionId, contextUser, provider);
507
620
  // Best-effort restore of the PRIOR session's persisted channel states (e.g. the whiteboard
508
621
  // board). Strictly tolerant — any problem yields a null field, never a failed start.
509
622
  result.PriorChannelStatesJson = (await this.loadPriorChannelStatesJson(lastSessionId, contextUser, provider)) ?? undefined;
510
623
  return result;
511
624
  }
512
625
 
626
+ /**
627
+ * The fork a session starts on: null (Main) when none is named; else the named fork, which must
628
+ * belong to the session's conversation. Returns the fork's stored id.
629
+ * @throws when a fork is named without a conversation, or is not a fork of it, or the fork rows cannot be read
630
+ */
631
+ private async resolveSessionBranch(
632
+ conversationId: string | undefined,
633
+ branchId: string | undefined,
634
+ contextUser: UserInfo,
635
+ provider: IMetadataProvider,
636
+ ): Promise<string | null> {
637
+ const named = branchId?.trim();
638
+ if (!named) {
639
+ return null;
640
+ }
641
+ if (!conversationId) {
642
+ throw new Error(`StartRealtimeClientSession: fork ${named} needs a conversation`);
643
+ }
644
+ try {
645
+ const scope = await ConversationEngine.LoadScope(conversationId, named, contextUser, provider);
646
+ return scope.BranchID;
647
+ } catch (error) {
648
+ throw new Error(`StartRealtimeClientSession: fork ${named} is not a readable fork of conversation ${conversationId}: ${(error as Error).message}`);
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Stamps the identity-verification policy snapshot, the configured unverified duration cap and any
654
+ * verified identity inherited from the resumed session onto the new session's `Config`.
655
+ *
656
+ * The effective config (config cascade) is only known after prepare, so this is a second, narrowly
657
+ * scoped write: it runs only when there is something to stamp. It **fails closed** — a cap that could
658
+ * not be applied would leave the session uncapped, so on a save failure the session is closed and the
659
+ * start fails rather than proceeding without its limits.
660
+ */
661
+ private async stampVerificationPolicy(
662
+ session: MJAIAgentSessionEntity,
663
+ effectiveConfigJson: string | undefined,
664
+ lastSessionId: string | undefined,
665
+ contextUser: UserInfo,
666
+ provider: IMetadataProvider,
667
+ ): Promise<void> {
668
+ const plan = BuildMintConfigStamps({
669
+ EffectiveRealtime: this.readEffectiveRealtime(effectiveConfigJson),
670
+ ExistingConfigRaw: session.Config_,
671
+ InheritedVerified: await LoadInheritedVerifiedIdentity(lastSessionId, contextUser, provider),
672
+ NowMs: Date.now(),
673
+ });
674
+ if (!plan) {
675
+ return;
676
+ }
677
+ session.Config_ = plan.NextConfigRaw;
678
+ const saved = await RunWithTrustedSessionConfigWrites(() => session.Save());
679
+ if (!saved) {
680
+ LogError(
681
+ `RealtimeClientSessionResolver.stampVerificationPolicy save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
682
+ );
683
+ await this.sessionManager.CloseSession(session.ID, contextUser, provider, 'Error');
684
+ throw new Error('Failed to apply the session limits; the realtime session was not started.');
685
+ }
686
+ }
687
+
688
+ /** The `realtime` section of the resolved effective config JSON, or undefined when absent/unparseable. */
689
+ private readEffectiveRealtime(effectiveConfigJson: string | undefined): unknown {
690
+ if (!effectiveConfigJson) {
691
+ return undefined;
692
+ }
693
+ try {
694
+ return (JSON.parse(effectiveConfigJson) as { realtime?: unknown }).realtime;
695
+ } catch (error) {
696
+ LogError(`RealtimeClientSessionResolver: effective config JSON was unparseable while stamping verification policy: ${(error as Error).message}`);
697
+ return undefined;
698
+ }
699
+ }
700
+
513
701
  /**
514
702
  * Execute a single tool call the browser relayed from its provider socket and return the
515
703
  * serialized result for the browser to relay back to the model.
516
704
  *
517
- * Ownership-gated; the target agent id is read from the **session config** (authoritative), never
518
- * from the client. Heartbeats the session on success.
705
+ * Ownership-gated. The target agent id is read from the session config and `CanRun` on it is
706
+ * checked for the caller on every call, because the owner can edit the config. A direct-action
707
+ * call from a scope-limited caller is refused before any elevation. Heartbeats the session on
708
+ * success.
519
709
  *
520
710
  * @returns The serialized tool result JSON.
521
711
  */
@@ -531,13 +721,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
531
721
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
532
722
  const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
533
723
  const config = this.readSessionConfig(session);
724
+ await this.assertCanRunTarget(config.targetAgentID, contextUser, provider);
725
+ if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME && IsScopeLimitedPrincipal(contextUser)) {
726
+ return this.refuseDirectAction(agentSessionId, toolName);
727
+ }
534
728
 
535
729
  // SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
536
730
  // run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
537
731
  // (the caller's role deliberately holds no grants on the run entities). The lead
538
- // targetAgentID comes from the session config and was CanRun-gated at start; the colleague
539
- // union is gated just below, against the CALLER, so elevation never widens agent authority.
732
+ // targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
733
+ // just below, so elevation never widens agent authority.
540
734
  const runUser = ResolveScopedAnonymousRunUser(contextUser);
735
+ const runIDs = await this.runIDVerifier.Verify(
736
+ session.ID,
737
+ { CoAgentRunID: config.coAgentRunID, PendingFeedbackRunID: config.pendingFeedbackRunID },
738
+ runUser,
739
+ provider,
740
+ );
541
741
  if (runUser !== contextUser) {
542
742
  LogStatus(
543
743
  `ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
@@ -555,16 +755,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
555
755
  // Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
556
756
  // colleague in the call is validated against this; absent ⇒ single-target behavior.
557
757
  AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
558
- DirectActions: config.directActions,
758
+ // No DirectActions: the service takes them from the session's start-time projection
759
+ // or the target agent's metadata, never from the owner-editable config.
559
760
  // Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
560
761
  // row and its context-memory scope must stay the person's, not the system user's.
561
762
  AttributionUserID: contextUser.ID,
562
763
  // Nest the delegated target-agent run under the co-agent observability run (when present).
563
- ParentRunID: config.coAgentRunID,
764
+ ParentRunID: runIDs.CoAgentRunID,
765
+ // The client-tool channel is keyed on the browser session id, so the delegated run can call
766
+ // the surface's tools; the session-start app context gives its prompt the surface context.
767
+ BrowserSessionID: userPayload.sessionId,
768
+ // The app context is a snapshot the browser sent, so a run elevated to the system user
769
+ // does not get it in its prompt.
770
+ AppContext: runUser === contextUser ? config.appContext : undefined,
564
771
  Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
565
772
  OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
566
773
  // Resume a previously-paused delegated run (if any) with the user's answer.
567
- ResumeRunID: config.pendingFeedbackRunID,
774
+ ResumeRunID: runIDs.PendingFeedbackRunID,
568
775
  },
569
776
  runUser,
570
777
  provider,
@@ -574,7 +781,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
574
781
  `ExecuteRealtimeSessionTool: completed relayed tool '${toolName}' (callId: ${callId}) in ${durationMs}ms`,
575
782
  );
576
783
 
577
- if (toolName !== 'invoke-target-agent') {
784
+ if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME) {
578
785
  await this.persistDirectActionTurn(
579
786
  session,
580
787
  callId,
@@ -631,6 +838,19 @@ export class RealtimeClientSessionResolver extends ResolverBase {
631
838
  };
632
839
  }
633
840
 
841
+ /**
842
+ * The tool result for a direct-action call from a scope-limited caller (see the authorization
843
+ * model above). It has the broker's result shape, so the model hears that the tool is not
844
+ * available and can ask the target agent through `invoke-target-agent` instead.
845
+ */
846
+ private refuseDirectAction(agentSessionId: string, toolName: string): string {
847
+ LogStatus(`ExecuteRealtimeSessionTool: refused direct action '${toolName}' for a scope-limited caller on session ${agentSessionId}.`);
848
+ return JSON.stringify({
849
+ success: false,
850
+ output: `The tool '${toolName}' is not available in this session. Ask the agent to do it with ${INVOKE_TARGET_AGENT_TOOL_NAME}.`,
851
+ });
852
+ }
853
+
634
854
  /**
635
855
  * Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
636
856
  * consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
@@ -646,17 +866,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
646
866
  if (!previous && !pausedRunID) {
647
867
  return;
648
868
  }
649
- const next: RealtimeSessionConfig = {
650
- targetAgentID: config.targetAgentID,
651
- coAgentRunID: config.coAgentRunID,
652
- promptRunID: config.promptRunID,
653
- coAgentRunStepID: config.coAgentRunStepID,
654
- pendingFeedbackRunID: pausedRunID,
655
- // Preserve the server-authoritative voice deadline across config rewrites.
656
- maxSessionDeadlineIso: config.maxSessionDeadlineIso,
657
- applicationID: config.applicationID,
658
- allowedAgents: config.allowedAgents,
659
- };
869
+ // Start from the RAW stored config so every key survives the rewrite — including the
870
+ // server-authoritative `maxSessionDeadlineIso` and `identityVerification` (dropping either would be
871
+ // refused by `MJAIAgentSessionEntityServer`, and silently losing a verification would be worse).
872
+ const next = { ...ParseConfigObject(session.Config_), targetAgentID: config.targetAgentID, pendingFeedbackRunID: pausedRunID };
660
873
  session.Config_ = JSON.stringify(next);
661
874
  if (!(await session.Save())) {
662
875
  LogError(
@@ -701,14 +914,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
701
914
  // conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
702
915
  // The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
703
916
  // Conversation Detail above deliberately stays on the caller.
704
- const promptRunID = this.readPromptRunID(session);
917
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
918
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
705
919
  if (promptRunID) {
706
920
  await this.clientSessionService.AppendPromptRunMessage(
707
921
  promptRunID,
708
922
  this.mapTranscriptRoleToChatRole(role),
709
923
  text,
710
924
  replacesPrevious ?? false,
711
- ResolveScopedAnonymousRunUser(contextUser),
925
+ runUser,
712
926
  provider,
713
927
  );
714
928
  }
@@ -731,7 +945,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
731
945
  * the call and uploads the assembled blob (base64) once the session ends.
732
946
  *
733
947
  * Hard gates (in order):
734
- * 1. Ownership — `loadOwnedSession` enforces `UserID === contextUser.ID` (throws on violation).
948
+ * 1. Ownership — `LoadOwnedRealtimeSession` enforces `UserID === contextUser.ID` (throws on violation).
735
949
  * 2. Consent — `consent !== true` is a hard refusal: no audio is ever stored without it.
736
950
  * 3. Storage configuration — the session's agent must resolve a recording storage account (the
737
951
  * agent's `RecordingStorageProviderID`, else `AttachmentStorageProviderID`).
@@ -760,18 +974,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
760
974
  @Arg('peaks', () => [Float], { nullable: true }) peaks?: number[],
761
975
  ): Promise<UploadRealtimeRecordingResult> {
762
976
  const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
763
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
977
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
764
978
 
765
979
  // HARD consent gate — no audio is ever stored without an explicit grant.
766
980
  if (consent !== true) {
767
981
  return { Success: false, ErrorMessage: 'Recording consent was not granted.' };
768
982
  }
769
983
 
770
- // SCOPED-ANONYMOUS ELEVATION (issue #3371): past the ownership + consent gates, the store is
771
- // server-side plumbing over entities (MJ: AI Agents read, MJ: Files, the file-session link)
772
- // the caller's narrow relay role deliberately does not hold. Attribution flows through the
773
- // session link, so nothing here depends on the caller's identity.
774
- const runUser = ResolveScopedAnonymousRunUser(contextUser);
984
+ // ELEVATION (issue #3371, #5195): past the ownership + consent gates, the store is server-side
985
+ // plumbing over entities (MJ: AI Agents read, MJ: Files, the file-session link, the session's
986
+ // recording stamp) that neither a scoped-anonymous relay role nor the stock UI role holds.
987
+ // Attribution flows through the session link, so nothing here depends on the caller's identity.
988
+ const runUser = ResolveRecordingStoreUser(contextUser);
775
989
  try {
776
990
  const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
777
991
  if (!(await agent.Load(session.AgentID))) {
@@ -822,11 +1036,14 @@ export class RealtimeClientSessionResolver extends ResolverBase {
822
1036
 
823
1037
  /**
824
1038
  * Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
825
- * folder (`realtime-recordings/<sessionId>/seg-NNNN.<ext>`) as a raw storage object. Durability
826
- * insurance during a live call so a browser/tab death loses at most the last window; the shards are
827
- * deleted once the canonical consolidated file lands via {@link UploadRealtimeRecording}. Ownership-
828
- * gated; consent was already established at session start. Best-effort — returns `false` (never
829
- * throws) on any problem so a failed shard never disrupts the live call.
1039
+ * folder (`realtime-recordings/<sessionId>/seg-NNNN.r<rate>.pcm` for PCM with a rate in its MIME type,
1040
+ * else the legacy `seg-NNNN.<ext>`) as a raw storage object. Durability insurance during a live call
1041
+ * so a browser/tab death loses at most the last window; the shards are deleted once the canonical
1042
+ * consolidated file lands via {@link UploadRealtimeRecording}, or by the session janitor once it has
1043
+ * recovered a recording from them when that upload never arrives. Ownership-
1044
+ * gated; consent was already established at session start. Refused (`false`) once the session has
1045
+ * its consolidated recording, since nothing would delete a shard written after that. Best-effort —
1046
+ * returns `false` (never throws) on any problem so a failed shard never disrupts the live call.
830
1047
  *
831
1048
  * @param agentSessionId The in-progress session (ownership-gated).
832
1049
  * @param segmentIndex 0-based shard index within the session.
@@ -844,7 +1061,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
844
1061
  ): Promise<boolean> {
845
1062
  try {
846
1063
  const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
847
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1064
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1065
+ // The consolidated recording already landed and its upload deleted the shards; a shard
1066
+ // arriving now (one still on the wire at hang-up) would be an orphan nothing deletes.
1067
+ if (session.RecordingFileID) {
1068
+ LogStatus(`RealtimeClientSessionResolver.UploadRealtimeRecordingSegment: shard ${segmentIndex} for session ${agentSessionId} refused — the session already has its consolidated recording`);
1069
+ return false;
1070
+ }
848
1071
  // Scoped-anonymous elevation (issue #3371) — same rationale as UploadRealtimeRecording.
849
1072
  const runUser = ResolveScopedAnonymousRunUser(contextUser);
850
1073
  const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
@@ -906,7 +1129,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
906
1129
  provider,
907
1130
  );
908
1131
 
909
- const promptRunID = this.readPromptRunID(session);
1132
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
1133
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
910
1134
  if (!promptRunID) {
911
1135
  return false;
912
1136
  }
@@ -915,7 +1139,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
915
1139
  'assistant',
916
1140
  this.formatToolTurn(toolName, argsJson, resultJson),
917
1141
  false,
918
- ResolveScopedAnonymousRunUser(contextUser),
1142
+ runUser,
919
1143
  provider,
920
1144
  );
921
1145
  }
@@ -960,7 +1184,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
960
1184
  @Arg('callId', () => String, { nullable: true }) callId?: string,
961
1185
  ): Promise<CancelRealtimeSessionToolResult> {
962
1186
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
963
- await this.loadOwnedSession(agentSessionId, contextUser, provider);
1187
+ await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
964
1188
  try {
965
1189
  const aborted = this.clientSessionService.CancelInFlightDelegations(agentSessionId, callId);
966
1190
  return { AbortedCount: aborted, Success: true };
@@ -987,8 +1211,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
987
1211
  * (observability creation was skipped), a failed load, or a failed save all log and return
988
1212
  * `false` — usage relay must never break a live call.
989
1213
  *
1214
+ * The optional details are the per-modality usage since the last relay: input and output
1215
+ * blocks shaped like Core's `RealtimeUsageModalityDetail`, avatar video seconds included. They
1216
+ * add into the usage record on the run's `ModelSpecificResponseDetails`, which prices the avatar
1217
+ * video at finalize. Only the fields a block defines, with values that are finite numbers of at
1218
+ * least 0, are kept; a relay that carries only details (avatar seconds, no tokens) is stored.
1219
+ *
990
1220
  * @param inputTokens Input-token DELTA to add (negative/non-finite values are clamped to 0).
991
1221
  * @param outputTokens Output-token DELTA to add (negative/non-finite values are clamped to 0).
1222
+ * @param usageDetailsJson The per-modality usage since the last relay, as JSON (`{ Input, Output }`).
992
1223
  * @returns `true` when the accumulated usage was persisted; `false` on any tolerated failure.
993
1224
  */
994
1225
  @Mutation(() => Boolean)
@@ -997,25 +1228,28 @@ export class RealtimeClientSessionResolver extends ResolverBase {
997
1228
  @Arg('inputTokens', () => Int) inputTokens: number,
998
1229
  @Arg('outputTokens', () => Int) outputTokens: number,
999
1230
  @Ctx() { userPayload, providers }: AppContext,
1231
+ @Arg('usageDetailsJson', () => String, { nullable: true }) usageDetailsJson?: string,
1000
1232
  ): Promise<boolean> {
1001
1233
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
1002
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1234
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1003
1235
 
1004
1236
  const inputDelta = this.clampTokenDelta(inputTokens);
1005
1237
  const outputDelta = this.clampTokenDelta(outputTokens);
1006
- if (inputDelta === 0 && outputDelta === 0) {
1238
+ const details = this.readUsageDetails(usageDetailsJson);
1239
+ if (inputDelta === 0 && outputDelta === 0 && !details) {
1007
1240
  return true; // nothing to add — a no-op flush is a success, not a failure
1008
1241
  }
1009
1242
 
1010
- const promptRunID = this.readPromptRunID(session);
1243
+ // Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
1244
+ const runUser = ResolveScopedAnonymousRunUser(contextUser);
1245
+ const promptRunID = await this.readPromptRunID(session, runUser, provider);
1011
1246
  if (!promptRunID) {
1012
1247
  return false; // no observability prompt run for this session — logged in the helper
1013
1248
  }
1014
1249
  // Delegate to the service so usage writes share the per-run serialization with transcript-message
1015
1250
  // appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
1016
- // Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
1017
1251
  return this.clientSessionService.AccumulatePromptRunUsage(
1018
- promptRunID, inputDelta, outputDelta, ResolveScopedAnonymousRunUser(contextUser), provider,
1252
+ promptRunID, inputDelta, outputDelta, runUser, provider, details ?? undefined,
1019
1253
  );
1020
1254
  }
1021
1255
 
@@ -1024,6 +1258,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1024
1258
  return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
1025
1259
  }
1026
1260
 
1261
+ /**
1262
+ * Reads the relayed per-modality usage, or `null` when there is none worth storing: absent,
1263
+ * malformed, longer than a relay ever needs (a client cannot make the server parse an
1264
+ * arbitrarily large string), or holding no amount above 0.
1265
+ */
1266
+ private readUsageDetails(usageDetailsJson: string | undefined): RealtimeUsageRecord | null {
1267
+ if (typeof usageDetailsJson !== 'string') {
1268
+ return null;
1269
+ }
1270
+ if (usageDetailsJson.length > MAX_USAGE_DETAILS_JSON_LENGTH) {
1271
+ LogError(`RelayRealtimeUsage: usage details of ${usageDetailsJson.length} characters exceed ${MAX_USAGE_DETAILS_JSON_LENGTH} — details dropped.`);
1272
+ return null;
1273
+ }
1274
+ const details = ParseRealtimeUsageRecord(usageDetailsJson);
1275
+ return HasRealtimeUsage(details) ? details : null;
1276
+ }
1277
+
1027
1278
  /**
1028
1279
  * Sanitizes client-supplied waveform peaks before they're persisted: drops anything that isn't a
1029
1280
  * finite number, clamps each to `[0, 1]`, and caps the array length so a hostile client can't bloat
@@ -1043,24 +1294,40 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1043
1294
 
1044
1295
  /**
1045
1296
  * Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
1046
- * throw-on-missing-target semantics — usage relay is best-effort, so a missing/malformed
1047
- * config or absent id just logs and returns `null`.
1297
+ * throw-on-missing-target semantics, and keeps it only when it is the prompt run of the
1298
+ * session's co-agent run (the owner can edit the config). Usage relay is best-effort, so a
1299
+ * missing/malformed config, an absent id, or an id of another session just logs and returns `null`.
1300
+ *
1301
+ * @param runUser The user the prompt-run write runs as; the check runs as the same user.
1048
1302
  */
1049
- private readPromptRunID(session: MJAIAgentSessionEntity): string | null {
1050
- try {
1051
- const config = this.readSessionConfig(session);
1052
- if (config.promptRunID) {
1053
- return config.promptRunID;
1054
- }
1303
+ private async readPromptRunID(
1304
+ session: MJAIAgentSessionEntity,
1305
+ runUser: UserInfo,
1306
+ provider: IMetadataProvider,
1307
+ ): Promise<string | null> {
1308
+ const config = this.tryReadSessionConfig(session);
1309
+ if (!config) {
1310
+ LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
1311
+ return null;
1312
+ }
1313
+ if (!config.promptRunID) {
1055
1314
  LogStatus(
1056
1315
  `RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
1057
1316
  'creation was skipped or failed) — usage delta dropped.',
1058
1317
  );
1059
1318
  return null;
1060
- } catch {
1061
- LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
1319
+ }
1320
+ const verified = await this.runIDVerifier.Verify(
1321
+ session.ID,
1322
+ { CoAgentRunID: config.coAgentRunID, PromptRunID: config.promptRunID },
1323
+ runUser,
1324
+ provider,
1325
+ );
1326
+ if (!verified.PromptRunID) {
1327
+ LogError(`RelayRealtimeUsage: session ${session.ID} names a promptRunID that is not its co-agent prompt run — ignored.`);
1062
1328
  return null;
1063
1329
  }
1330
+ return verified.PromptRunID;
1064
1331
  }
1065
1332
 
1066
1333
  /**
@@ -1092,8 +1359,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1092
1359
  @Ctx() { userPayload, providers }: AppContext,
1093
1360
  ): Promise<boolean> {
1094
1361
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
1095
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1362
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1096
1363
 
1364
+ if (!this.isChannelInSessionScope(session, channelName)) {
1365
+ LogError(`SaveSessionChannelState: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — state not saved.`);
1366
+ return false;
1367
+ }
1097
1368
  if (stateJson.length > MAX_CHANNEL_STATE_CHARS) {
1098
1369
  LogError(
1099
1370
  `SaveSessionChannelState: rejected oversized state for session ${agentSessionId} / channel '${channelName}' ` +
@@ -1171,8 +1442,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1171
1442
  @Ctx() { userPayload, providers }: AppContext,
1172
1443
  ): Promise<SaveSessionChannelArtifactResult> {
1173
1444
  const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
1174
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1445
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1175
1446
 
1447
+ if (!this.isChannelInSessionScope(session, channelName)) {
1448
+ const message = `SaveSessionChannelArtifact: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — nothing saved.`;
1449
+ LogError(message);
1450
+ return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
1451
+ }
1176
1452
  if (contentJson.length > MAX_CHANNEL_STATE_CHARS) {
1177
1453
  const message =
1178
1454
  `SaveSessionChannelArtifact: rejected oversized content for session ${agentSessionId} / channel '${channelName}' ` +
@@ -1210,8 +1486,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1210
1486
  }
1211
1487
 
1212
1488
  /**
1213
- * Authorization gate for {@link RealtimeClientSessionResolver.StartRealtimeClientSession}: the
1214
- * caller must be able to run the **target** agent. Denial throws (no session is created).
1489
+ * Authorization gate for the **target** agent: the caller must be able to run it. Denial throws.
1490
+ * {@link RealtimeClientSessionResolver.StartRealtimeClientSession} runs it before any session is
1491
+ * created, and {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} runs it on every
1492
+ * relayed call, because the owner can edit the target in the session config.
1215
1493
  */
1216
1494
  private async assertCanRunTarget(
1217
1495
  targetAgentId: string,
@@ -1479,6 +1757,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1479
1757
  * resumed session remembers the previous leg(s).
1480
1758
  * @param conversationMessages Optional capped history of the TEXT conversation this session is
1481
1759
  * starting from (from {@link loadConversationHistory}) — framed as "Conversation so far".
1760
+ * @param showsAgentVideo `false` when the app shows no agent video: the session asks for no avatar (`host`).
1482
1761
  */
1483
1762
  private async prepareClientSessionOrClose(
1484
1763
  session: MJAIAgentSessionEntity,
@@ -1494,6 +1773,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1494
1773
  applicationId?: string,
1495
1774
  appContext?: AppContextSnapshot,
1496
1775
  conversationMessages?: ChatMessage[],
1776
+ channelCandidates?: RealtimeChannelCandidate[],
1777
+ showsAgentVideo?: boolean,
1497
1778
  ): Promise<StartRealtimeClientSessionResult> {
1498
1779
  const prep = await this.clientSessionService.PrepareClientSession(
1499
1780
  {
@@ -1521,6 +1802,14 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1521
1802
  // (allowed-agent union); the snapshot is injected into the system prompt at mint.
1522
1803
  ApplicationID: applicationId,
1523
1804
  AppContext: appContext,
1805
+ // Scope-limited callers get no direct actions (see the authorization model above), so
1806
+ // the model is not given tools that the relay refuses.
1807
+ ExcludeDirectActions: IsScopeLimitedPrincipal(contextUser),
1808
+ // The channels the browser could mount: scoped here (registry + cascade) and handed back
1809
+ // as the resolved policy; the declared tools are narrowed to match.
1810
+ ChannelCandidates: channelCandidates,
1811
+ // An app with no channel that shows the agent's video: no avatar is asked for (reason `host`).
1812
+ ShowsAgentVideo: showsAgentVideo ?? undefined,
1524
1813
  },
1525
1814
  // SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
1526
1815
  // observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
@@ -1541,7 +1830,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1541
1830
  session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID,
1542
1831
  applicationId, prep.EffectiveConfig?.realtime?.allowedAgents,
1543
1832
  prep.EffectiveConfig?.realtime?.directActions,
1833
+ prep.ClientPolicy?.Channels.map((c) => c.Key),
1834
+ appContext,
1544
1835
  );
1836
+ this.pruneExcludedServerChannels(session.ID, prep.ClientPolicy);
1545
1837
 
1546
1838
  const cfg = prep.ClientConfig;
1547
1839
  return {
@@ -1551,20 +1843,43 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1551
1843
  Model: cfg.Model,
1552
1844
  EphemeralToken: cfg.EphemeralToken,
1553
1845
  ExpiresAt: cfg.ExpiresAt,
1846
+ Transport: cfg.Transport,
1847
+ RelayUrl: cfg.RelayUrl,
1554
1848
  SessionConfigJson: JSON.stringify(cfg.SessionConfig),
1555
1849
  ModelName: prep.ModelName,
1556
1850
  DriverClass: prep.DriverClass,
1557
1851
  NarrationInstructionsTemplate: prep.NarrationInstructionsTemplate,
1558
1852
  NarrationPaceMs: prep.NarrationPaceMs,
1559
1853
  EffectiveConfigJson: prep.EffectiveConfig ? JSON.stringify(prep.EffectiveConfig) : undefined,
1854
+ ClientPolicyJson: prep.ClientPolicy ? JSON.stringify(prep.ClientPolicy) : undefined,
1855
+ AvatarStatusJson: prep.AvatarStatus ? JSON.stringify(prep.AvatarStatus) : undefined,
1560
1856
  };
1561
1857
  }
1562
1858
 
1859
+ /**
1860
+ * Drops the server-side channel plugins of channels the resolved scope left out. Server plugins
1861
+ * start when the session row is created — before the scope is known — so an excluded channel's
1862
+ * server half would otherwise keep running. A no-op when the caller reported no candidates.
1863
+ */
1864
+ private pruneExcludedServerChannels(agentSessionID: string, policy: RealtimeSessionClientPolicy | undefined): void {
1865
+ const excluded = (policy?.ExcludedChannels ?? []).map((e) => e.Key);
1866
+ if (excluded.length === 0) {
1867
+ return;
1868
+ }
1869
+ const dropped = RealtimeChannelServerHost.Instance.PruneSessionChannels(agentSessionID, excluded);
1870
+ if (dropped.length > 0) {
1871
+ LogStatus(`StartRealtimeClientSession: dropped the server plugins of out-of-scope channel(s) [${dropped.join(', ')}] for session ${agentSessionID}.`);
1872
+ }
1873
+ }
1874
+
1563
1875
  /**
1564
1876
  * Writes the co-agent observability run ids into the session's `Config_` alongside the
1565
- * authoritative `targetAgentID`, then saves the session. These ids are read back on close to
1566
- * finalize the runs (and on relay to nest delegated runs). Best-effort: a save failure is logged,
1567
- * not thrown — the voice session still proceeds, it just won't carry the run ids.
1877
+ * `targetAgentID`, then saves the session. These ids are read back on close to
1878
+ * finalize the runs (and on relay to nest delegated runs). The app awareness (application id,
1879
+ * allowed agents, direct actions, app-context snapshot) is written with them; relays read the
1880
+ * allowed agents and the app context, never the direct actions.
1881
+ * Best-effort: a save failure is logged, not thrown — the voice session still proceeds, it just
1882
+ * won't carry the run ids.
1568
1883
  */
1569
1884
  private async persistObservabilityRunIDs(
1570
1885
  session: MJAIAgentSessionEntity,
@@ -1575,22 +1890,29 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1575
1890
  applicationID?: string,
1576
1891
  allowedAgents?: RealtimeAllowedAgent[],
1577
1892
  directActions?: RealtimeDirectActionsConfig,
1893
+ channels?: string[],
1894
+ appContext?: AppContextSnapshot,
1578
1895
  ): Promise<void> {
1579
- // Preserve any server-authoritative voice deadline stamped at session start (this rebuilds the
1580
- // full config, so read the existing value forward rather than dropping it).
1581
- const existing = this.tryReadSessionConfig(session);
1582
- const config: RealtimeSessionConfig = {
1896
+ // Start from the RAW stored config so every key set at session start survives — the
1897
+ // server-authoritative voice deadline (`maxSessionDeadlineIso`) and the per-session media-kit
1898
+ // override among them — rather than rebuilding from a whitelist that silently drops the rest.
1899
+ const config = {
1900
+ ...ParseConfigObject(session.Config_),
1583
1901
  targetAgentID,
1584
1902
  coAgentRunID,
1585
1903
  promptRunID,
1586
1904
  coAgentRunStepID,
1587
- maxSessionDeadlineIso: existing?.maxSessionDeadlineIso,
1588
1905
  applicationID,
1906
+ appContext: this.appContextToStore(appContext, session.ID),
1589
1907
  allowedAgents: allowedAgents && allowedAgents.length > 0 ? allowedAgents : undefined,
1590
1908
  directActions,
1909
+ channels,
1591
1910
  };
1592
1911
  session.Config_ = JSON.stringify(config);
1593
- const saved = await session.Save();
1912
+ // Every value written here is server-decided (run ids, the resolved target, the in-scope channel
1913
+ // set), and `channels` is a server-authoritative key the owner may not write — so this save runs in
1914
+ // the trusted scope. Protected keys already in the stored config are carried forward unchanged.
1915
+ const saved = await RunWithTrustedSessionConfigWrites(() => session.Save());
1594
1916
  if (!saved) {
1595
1917
  LogError(
1596
1918
  `RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
@@ -1599,41 +1921,43 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1599
1921
  }
1600
1922
 
1601
1923
  /**
1602
- * Loads a session, enforcing inbound ownership (`UserID === contextUser.ID`) and that it is not
1603
- * `Closed`. Throws on any violation. Returns the loaded session for the caller to read from.
1924
+ * Returns the app-context snapshot to store on the session config. A snapshot whose JSON is longer
1925
+ * than {@link MAX_CLIENT_TOOLS_JSON_CHARS} is not stored: a warning is logged and `undefined` is
1926
+ * returned, so the session still starts and its delegated runs get no app context.
1927
+ *
1928
+ * @param appContext The snapshot the browser sent at session start, or undefined.
1929
+ * @param sessionID The session the snapshot belongs to (for the warning).
1930
+ * @returns The snapshot to store, or `undefined`.
1604
1931
  */
1605
- private async loadOwnedActiveSession(
1606
- agentSessionId: string,
1607
- contextUser: UserInfo,
1608
- provider: IMetadataProvider,
1609
- ): Promise<MJAIAgentSessionEntity> {
1610
- const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
1611
- if (session.Status === 'Closed') {
1612
- throw new Error(`Realtime session ${agentSessionId} is closed`);
1932
+ private appContextToStore(appContext: AppContextSnapshot | undefined, sessionID: string): AppContextSnapshot | undefined {
1933
+ if (!appContext) {
1934
+ return undefined;
1613
1935
  }
1614
- return session;
1936
+ const chars = JSON.stringify(appContext).length;
1937
+ if (chars > MAX_CLIENT_TOOLS_JSON_CHARS) {
1938
+ LogErrorEx({
1939
+ message:
1940
+ `RealtimeClientSessionResolver: app context for session ${sessionID} not stored — ${chars} chars ` +
1941
+ `exceeds the ${MAX_CLIENT_TOOLS_JSON_CHARS} cap; delegated runs get no app context.`,
1942
+ severity: 'warning',
1943
+ });
1944
+ return undefined;
1945
+ }
1946
+ return appContext;
1615
1947
  }
1616
1948
 
1617
1949
  /**
1618
- * Loads a session and enforces inbound ownership (`UserID === contextUser.ID`) WITHOUT
1619
- * rejecting `Closed` sessions — {@link SaveSessionChannelState}'s final on-end flush
1620
- * legitimately arrives after the session closed. Throws when not found / not owned.
1950
+ * Loads a session, enforcing inbound ownership (`UserID === contextUser.ID`) and that it is not
1951
+ * `Closed`. Throws on any violation. Returns the loaded session for the caller to read from.
1621
1952
  */
1622
- private async loadOwnedSession(
1953
+ private async loadOwnedActiveSession(
1623
1954
  agentSessionId: string,
1624
1955
  contextUser: UserInfo,
1625
1956
  provider: IMetadataProvider,
1626
1957
  ): Promise<MJAIAgentSessionEntity> {
1627
- const session = await provider.GetEntityObject<MJAIAgentSessionEntity>(SESSION_ENTITY, contextUser);
1628
- const loaded = await session.Load(agentSessionId);
1629
- if (!loaded) {
1630
- throw new Error(`Realtime session ${agentSessionId} not found`);
1631
- }
1632
- if (!UUIDsEqual(session.UserID, contextUser.ID)) {
1633
- LogError(
1634
- `RealtimeClientSessionResolver: ownership check failed — user ${contextUser.ID} attempted to operate on session ${agentSessionId} owned by ${session.UserID}`,
1635
- );
1636
- throw new Error('Not authorized: you do not own this realtime session');
1958
+ const session = await LoadOwnedRealtimeSession(agentSessionId, contextUser, provider);
1959
+ if (session.Status === 'Closed') {
1960
+ throw new Error(`Realtime session ${agentSessionId} is closed`);
1637
1961
  }
1638
1962
  return session;
1639
1963
  }
@@ -1843,6 +2167,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1843
2167
  * as the resumed transcript are removed, so the prompt never carries two accounts of one
1844
2168
  * exchange.
1845
2169
  *
2170
+ * Only the rows on the session's path that no rerun replaced are read: the fork it started on,
2171
+ * or Main, in `Sequence` order. When that path cannot be built — its branch rows are missing or invalid — the session
2172
+ * starts with no history, never with every branch's rows.
2173
+ *
1846
2174
  * Strictly tolerant — no conversation, a failed read, or any throw yields `[]`, never a failed
1847
2175
  * session start. The call still works; it just starts cold.
1848
2176
  *
@@ -1850,6 +2178,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1850
2178
  * @param coveredSessionIDs Session ids already framed via `PriorTranscript`.
1851
2179
  * @param contextUser The calling user (RunView scope — the read is row-level-secured).
1852
2180
  * @param provider The request-scoped metadata provider.
2181
+ * @param branchId The fork the session started on; null is Main.
1853
2182
  * @returns The capped history, oldest first.
1854
2183
  */
1855
2184
  private async loadConversationHistory(
@@ -1857,12 +2186,22 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1857
2186
  coveredSessionIDs: string[],
1858
2187
  contextUser: UserInfo,
1859
2188
  provider: IMetadataProvider,
2189
+ branchId: string | null,
1860
2190
  ): Promise<ChatMessage[]> {
1861
2191
  if (!conversationId) {
1862
2192
  return [];
1863
2193
  }
2194
+ const rv = RunView.FromMetadataProvider(provider);
2195
+ let pathFilter: string;
2196
+ try {
2197
+ pathFilter = await ConversationEngine.BranchPathFilterFresh(conversationId, branchId, contextUser, provider);
2198
+ } catch (error) {
2199
+ LogError(
2200
+ `StartRealtimeClientSession: could not build the path of the session's view for conversation ${conversationId}: ${(error as Error).message}`,
2201
+ );
2202
+ return [];
2203
+ }
1864
2204
  try {
1865
- const rv = RunView.FromMetadataProvider(provider);
1866
2205
  const result = await rv.RunView<{
1867
2206
  Role: string;
1868
2207
  Message: string | null;
@@ -1871,9 +2210,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
1871
2210
  }>(
1872
2211
  {
1873
2212
  EntityName: CONVERSATION_DETAIL_ENTITY,
1874
- ExtraFilter: `ConversationID='${conversationId.replace(/'/g, "''")}'`,
2213
+ ExtraFilter: ConversationEngine.LiveRowsFilter(pathFilter),
1875
2214
  Fields: ['ID', 'Role', 'Message', 'HiddenToUser', 'AgentSessionID', '__mj_CreatedAt'],
1876
- OrderBy: '__mj_CreatedAt ASC',
2215
+ OrderBy: 'Sequence ASC',
1877
2216
  ResultType: 'simple',
1878
2217
  },
1879
2218
  contextUser,
@@ -2240,6 +2579,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2240
2579
  const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY, callerUser);
2241
2580
  detail.NewRecord();
2242
2581
  detail.ConversationID = session.ConversationID;
2582
+ detail.BranchID = this.sessionBranchIdOf(session);
2243
2583
  detail.Role = 'AI';
2244
2584
  detail.HiddenToUser = true;
2245
2585
  detail.Message = 'Artifacts produced during a realtime session (system anchor).';
@@ -2392,21 +2732,54 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2392
2732
 
2393
2733
  /** Shape check for one client tool declaration: Name/Description non-empty strings, ParametersSchema a plain object. */
2394
2734
  private isValidClientTool(candidate: unknown): boolean {
2395
- if (candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
2735
+ return IsValidRealtimeToolDefinition(candidate);
2736
+ }
2737
+
2738
+ /**
2739
+ * Tolerantly parses the channel candidates the browser reported at mint (the same caps and
2740
+ * tolerance as {@link parseClientTools}: capped, never throws, anything wrong is dropped and
2741
+ * logged). A caller that sent none yields `undefined` — an unscoped session, exactly as before.
2742
+ */
2743
+ private parseChannelCandidates(channelCandidatesJson?: string): RealtimeChannelCandidate[] | undefined {
2744
+ const parsed = ParseRealtimeChannelCandidates(channelCandidatesJson);
2745
+ for (const reason of parsed.Rejected) {
2746
+ LogError(`StartRealtimeClientSession: ${reason}.`);
2747
+ }
2748
+ return parsed.Candidates.length > 0 ? parsed.Candidates : undefined;
2749
+ }
2750
+
2751
+ /**
2752
+ * Whether a session's persisted scope allows a server-side call to name `channelName`. A session
2753
+ * with no persisted scope (it reported no candidates) is unscoped and allows everything; a scoped
2754
+ * one allows only the channels its policy put in the session.
2755
+ */
2756
+ private isChannelInSessionScope(session: MJAIAgentSessionEntity, channelName: string): boolean {
2757
+ // Read the RAW config: an absent `channels` key means an unscoped session, but anything present
2758
+ // and malformed fails CLOSED — a scope that cannot be read must not widen to "everything".
2759
+ // (`channels` is a server-authoritative key, so the owner cannot delete it to unscope a session.)
2760
+ const channels = ParseConfigObject(session.Config_)['channels'];
2761
+ if (channels === undefined) {
2762
+ return true;
2763
+ }
2764
+ if (!Array.isArray(channels)) {
2765
+ LogError(`RealtimeClientSessionResolver: session ${session.ID} has a malformed channel scope; refusing channel '${channelName}'.`);
2396
2766
  return false;
2397
2767
  }
2398
- const tool = candidate as Partial<RealtimeToolDefinition>;
2399
- return (
2400
- typeof tool.Name === 'string' && tool.Name.trim().length > 0 && tool.Name.length <= 128 &&
2401
- typeof tool.Description === 'string' && tool.Description.trim().length > 0 &&
2402
- tool.ParametersSchema !== null && typeof tool.ParametersSchema === 'object' && !Array.isArray(tool.ParametersSchema)
2403
- );
2768
+ const wanted = NormalizeChannelKey(channelName);
2769
+ return channels.some((key) => typeof key === 'string' && NormalizeChannelKey(key) === wanted);
2770
+ }
2771
+
2772
+ /** The fork the session was started on (its config's `conversationBranchID`); null is Main. */
2773
+ private sessionBranchIdOf(session: MJAIAgentSessionEntity): string | null {
2774
+ const branchId = ParseConfigObject(session.Config_)['conversationBranchID'];
2775
+ return typeof branchId === 'string' && branchId.length > 0 ? branchId : null;
2404
2776
  }
2405
2777
 
2406
2778
  /**
2407
- * Parses the session's persisted config, returning the authoritative `targetAgentID` plus any
2408
- * observability run ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed
2409
- * or carries no target — a relay cannot proceed without a target.
2779
+ * Parses the session's persisted config, returning its `targetAgentID` plus any observability run
2780
+ * ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed or carries no
2781
+ * target — a relay cannot proceed without a target. The owner can edit the config, so callers
2782
+ * re-check what they act on.
2410
2783
  */
2411
2784
  private readSessionConfig(session: MJAIAgentSessionEntity): RealtimeSessionConfig {
2412
2785
  const raw = session.Config_;
@@ -2424,11 +2797,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2424
2797
  maxSessionDeadlineIso:
2425
2798
  typeof parsed.maxSessionDeadlineIso === 'string' ? parsed.maxSessionDeadlineIso : undefined,
2426
2799
  applicationID: typeof parsed.applicationID === 'string' ? parsed.applicationID : undefined,
2800
+ appContext:
2801
+ parsed.appContext && typeof parsed.appContext === 'object' && !Array.isArray(parsed.appContext)
2802
+ ? parsed.appContext
2803
+ : undefined,
2427
2804
  allowedAgents: Array.isArray(parsed.allowedAgents) ? parsed.allowedAgents : undefined,
2428
2805
  directActions:
2429
2806
  parsed.directActions && typeof parsed.directActions === 'object' && typeof parsed.directActions.enabled === 'boolean'
2430
2807
  ? (parsed.directActions as RealtimeDirectActionsConfig)
2431
2808
  : undefined,
2809
+ conversationBranchID: typeof parsed.conversationBranchID === 'string' ? parsed.conversationBranchID : undefined,
2810
+ mediaCollectionID: typeof parsed.mediaCollectionID === 'string' ? parsed.mediaCollectionID : undefined,
2811
+ channels: Array.isArray(parsed.channels) ? parsed.channels.filter((k): k is string => typeof k === 'string') : undefined,
2432
2812
  };
2433
2813
  }
2434
2814
  } catch {
@@ -2516,6 +2896,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2516
2896
  );
2517
2897
  detail.NewRecord();
2518
2898
  detail.ConversationID = session.ConversationID;
2899
+ detail.BranchID = this.sessionBranchIdOf(session);
2519
2900
  detail.Role = this.mapTranscriptRole(role);
2520
2901
  detail.Message = text;
2521
2902
  detail.AgentSessionID = session.ID;
@@ -2689,6 +3070,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
2689
3070
  );
2690
3071
  detail.NewRecord();
2691
3072
  detail.ConversationID = session.ConversationID;
3073
+ detail.BranchID = this.sessionBranchIdOf(session);
2692
3074
  detail.Role = 'AI';
2693
3075
  detail.HiddenToUser = true;
2694
3076
  detail.AgentSessionID = session.ID;