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

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