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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (543) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  4. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  5. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  6. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  7. package/dist/agentSessions/HostInstance.d.ts +75 -11
  8. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  9. package/dist/agentSessions/HostInstance.js +99 -28
  10. package/dist/agentSessions/HostInstance.js.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  12. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  13. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  14. package/dist/agentSessions/SessionJanitor.d.ts +236 -27
  15. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  16. package/dist/agentSessions/SessionJanitor.js +682 -54
  17. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  18. package/dist/agentSessions/SessionManager.d.ts +48 -3
  19. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  20. package/dist/agentSessions/SessionManager.js +112 -9
  21. package/dist/agentSessions/SessionManager.js.map +1 -1
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  25. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  26. package/dist/agentSessions/index.d.ts +2 -0
  27. package/dist/agentSessions/index.d.ts.map +1 -1
  28. package/dist/agentSessions/index.js +10 -3
  29. package/dist/agentSessions/index.js.map +1 -1
  30. package/dist/apiRoot.d.ts +32 -0
  31. package/dist/apiRoot.d.ts.map +1 -0
  32. package/dist/apiRoot.js +67 -0
  33. package/dist/apiRoot.js.map +1 -0
  34. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  35. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  36. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  37. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  38. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  39. package/dist/auth/LoginScreenEngine.js +57 -0
  40. package/dist/auth/LoginScreenEngine.js.map +1 -0
  41. package/dist/auth/actionAuthorization.d.ts +40 -0
  42. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  43. package/dist/auth/actionAuthorization.js +94 -0
  44. package/dist/auth/actionAuthorization.js.map +1 -0
  45. package/dist/auth/index.d.ts +1 -0
  46. package/dist/auth/index.d.ts.map +1 -1
  47. package/dist/auth/index.js +4 -10
  48. package/dist/auth/index.js.map +1 -1
  49. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  50. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  51. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  52. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  53. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  54. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  55. package/dist/auth/newUsers.d.ts +10 -0
  56. package/dist/auth/newUsers.d.ts.map +1 -1
  57. package/dist/auth/newUsers.js +80 -11
  58. package/dist/auth/newUsers.js.map +1 -1
  59. package/dist/config.d.ts +2514 -22
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +166 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/context.d.ts +8 -0
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +14 -11
  66. package/dist/context.js.map +1 -1
  67. package/dist/coreRoutes.d.ts +29 -0
  68. package/dist/coreRoutes.d.ts.map +1 -0
  69. package/dist/coreRoutes.js +142 -0
  70. package/dist/coreRoutes.js.map +1 -0
  71. package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
  72. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  73. package/dist/generated/graphql-schemas/__mj.js +2456 -7
  74. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  75. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  76. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  77. package/dist/generic/CacheInvalidationResolver.js +59 -19
  78. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  79. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  80. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  81. package/dist/generic/PushStatusResolver.js +1 -1
  82. package/dist/generic/PushStatusResolver.js.map +1 -1
  83. package/dist/generic/ResolverBase.d.ts +123 -48
  84. package/dist/generic/ResolverBase.d.ts.map +1 -1
  85. package/dist/generic/ResolverBase.js +293 -305
  86. package/dist/generic/ResolverBase.js.map +1 -1
  87. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  88. package/dist/generic/RunViewResolver.js +16 -0
  89. package/dist/generic/RunViewResolver.js.map +1 -1
  90. package/dist/globalCors.d.ts +33 -0
  91. package/dist/globalCors.d.ts.map +1 -0
  92. package/dist/globalCors.js +58 -0
  93. package/dist/globalCors.js.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +219 -278
  97. package/dist/index.js.map +1 -1
  98. package/dist/isolatedProvider.d.ts +20 -0
  99. package/dist/isolatedProvider.d.ts.map +1 -0
  100. package/dist/isolatedProvider.js +22 -0
  101. package/dist/isolatedProvider.js.map +1 -0
  102. package/dist/multiTenancy/index.d.ts.map +1 -1
  103. package/dist/multiTenancy/index.js +22 -2
  104. package/dist/multiTenancy/index.js.map +1 -1
  105. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  106. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  107. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  108. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  109. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  110. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  111. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  112. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  113. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  114. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  115. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  116. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  117. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  118. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  119. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +139 -0
  120. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  121. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  122. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  123. package/dist/realtimeSessions/RealtimeVerifyRouter.js +99 -0
  124. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  125. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  126. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  127. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  128. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  129. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  130. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  131. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  132. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  133. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  134. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  135. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  136. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  137. package/dist/realtimeSessions/index.d.ts +19 -0
  138. package/dist/realtimeSessions/index.d.ts.map +1 -0
  139. package/dist/realtimeSessions/index.js +19 -0
  140. package/dist/realtimeSessions/index.js.map +1 -0
  141. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  142. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  143. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  144. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  145. package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
  146. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  147. package/dist/realtimeSessions/sessionAccess.js +94 -0
  148. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  149. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  150. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  151. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  152. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  153. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  154. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  155. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  156. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  157. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  158. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  159. package/dist/realtimeSessions/verificationCore.js +356 -0
  160. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  161. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  162. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  163. package/dist/realtimeSessions/verificationPages.js +110 -0
  164. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  165. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  166. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  167. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  168. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  169. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  170. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  171. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  172. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  173. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  174. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  175. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  176. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  177. package/dist/resolvers/ActionResolver.d.ts +31 -5
  178. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  179. package/dist/resolvers/ActionResolver.js +75 -14
  180. package/dist/resolvers/ActionResolver.js.map +1 -1
  181. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  182. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  183. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  184. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  185. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  186. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  187. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  188. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  189. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  190. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  191. package/dist/resolvers/CacheStatsResolver.js +113 -2
  192. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  193. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  194. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  195. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  196. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  197. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  198. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  199. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  200. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  201. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  202. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  203. package/dist/resolvers/DatasetResolver.js +57 -8
  204. package/dist/resolvers/DatasetResolver.js.map +1 -1
  205. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  206. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  207. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  208. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  209. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
  210. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  211. package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
  212. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  213. package/dist/resolvers/FileResolver.d.ts +82 -11
  214. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  215. package/dist/resolvers/FileResolver.js +279 -101
  216. package/dist/resolvers/FileResolver.js.map +1 -1
  217. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  218. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  219. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  220. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  221. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  222. package/dist/resolvers/GetDataResolver.js +6 -3
  223. package/dist/resolvers/GetDataResolver.js.map +1 -1
  224. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  225. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  226. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  227. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  228. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  229. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  230. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  231. package/dist/resolvers/MCPResolver.js +9 -4
  232. package/dist/resolvers/MCPResolver.js.map +1 -1
  233. package/dist/resolvers/QueryResolver.d.ts +2 -0
  234. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  235. package/dist/resolvers/QueryResolver.js +13 -0
  236. package/dist/resolvers/QueryResolver.js.map +1 -1
  237. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  238. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  239. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  240. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  241. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +157 -33
  242. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  243. package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
  244. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  245. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  246. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  247. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  248. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  249. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  250. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  251. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  252. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  253. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  254. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  255. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  256. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  257. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  258. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  259. package/dist/resolvers/RunAIAgentResolver.js +100 -14
  260. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  261. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  262. package/dist/resolvers/RunTemplateResolver.js +12 -11
  263. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  264. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  265. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  266. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  267. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  268. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  269. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  270. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  271. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  272. package/dist/resolvers/SyncDataResolver.js +6 -2
  273. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  274. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  275. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  276. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  277. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  278. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  279. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  280. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  281. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  282. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  283. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  284. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  285. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  286. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  287. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  288. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  289. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  290. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  291. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  292. package/dist/resolvers/UserAvatarResolver.js +124 -0
  293. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  294. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  295. package/dist/resolvers/UserResolver.js +2 -17
  296. package/dist/resolvers/UserResolver.js.map +1 -1
  297. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  298. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  299. package/dist/resolvers/agent-run-data-guard.js +72 -0
  300. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  301. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  302. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  303. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  304. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  305. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  306. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  307. package/dist/resolvers/avatarInputValidation.js +97 -0
  308. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  309. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  310. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  311. package/dist/resolvers/currentUserRoles.js +30 -0
  312. package/dist/resolvers/currentUserRoles.js.map +1 -1
  313. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  314. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  315. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  316. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  317. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  318. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  319. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  320. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  321. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  322. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  323. package/dist/rest/ViewOperationsHandler.js +21 -12
  324. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  325. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  326. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  327. package/dist/rest/setupRESTEndpoints.js +6 -0
  328. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  329. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  330. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  331. package/dist/serverExtensionReservedRoots.js +14 -17
  332. package/dist/serverExtensionReservedRoots.js.map +1 -1
  333. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  334. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  335. package/dist/services/MJStorageBlobStore.js +25 -19
  336. package/dist/services/MJStorageBlobStore.js.map +1 -1
  337. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  338. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  339. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  340. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  341. package/dist/services/TaskGraphActionRunner.js +27 -5
  342. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  343. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  344. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  345. package/dist/services/TaskGraphAgentRunner.js +157 -9
  346. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  347. package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
  348. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  349. package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
  350. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  351. package/dist/sharedCache.d.ts +105 -0
  352. package/dist/sharedCache.d.ts.map +1 -0
  353. package/dist/sharedCache.js +279 -0
  354. package/dist/sharedCache.js.map +1 -0
  355. package/dist/types.d.ts +6 -0
  356. package/dist/types.d.ts.map +1 -1
  357. package/dist/types.js.map +1 -1
  358. package/package.json +99 -96
  359. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  360. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  361. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  362. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  363. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  364. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  365. package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
  366. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  367. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  368. package/src/__tests__/HostInstance.test.ts +84 -0
  369. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  370. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  371. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  372. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  373. package/src/__tests__/MeetingResolver.test.ts +75 -1
  374. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  375. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  376. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  377. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  378. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  379. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  380. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  381. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  382. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  383. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  384. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  385. package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
  386. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  387. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  388. package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
  389. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
  390. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  391. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  392. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  393. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  394. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  395. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  396. package/src/__tests__/SessionJanitor.test.ts +1203 -21
  397. package/src/__tests__/SessionManager.test.ts +240 -17
  398. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  399. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  400. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  401. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  402. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
  403. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  404. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  405. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  406. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  407. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  408. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  409. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  410. package/src/__tests__/actionAuthorization.test.ts +130 -0
  411. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  412. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  413. package/src/__tests__/apiRootBoot.test.ts +202 -0
  414. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  415. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  416. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  417. package/src/__tests__/globalCors.test.ts +217 -0
  418. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  419. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  420. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  421. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  422. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  423. package/src/__tests__/multiTenancy.test.ts +1 -1
  424. package/src/__tests__/newUsers.template.test.ts +151 -0
  425. package/src/__tests__/newUsers.test.ts +74 -5
  426. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  427. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  428. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  429. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  430. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  431. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  432. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  433. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  434. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  435. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  436. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  437. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  438. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  439. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  440. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  441. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  442. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  443. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  444. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  445. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
  446. package/src/__tests__/sharedCache.test.ts +312 -0
  447. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  448. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  449. package/src/agentSessions/HostInstance.ts +124 -28
  450. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  451. package/src/agentSessions/SessionJanitor.ts +821 -59
  452. package/src/agentSessions/SessionManager.ts +138 -26
  453. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  454. package/src/agentSessions/index.ts +10 -3
  455. package/src/apiRoot.ts +78 -0
  456. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  457. package/src/auth/LoginScreenEngine.ts +50 -0
  458. package/src/auth/actionAuthorization.ts +116 -0
  459. package/src/auth/index.ts +4 -8
  460. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  461. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  462. package/src/auth/newUsers.ts +99 -11
  463. package/src/config.ts +171 -5
  464. package/src/context.ts +15 -10
  465. package/src/coreRoutes.ts +166 -0
  466. package/src/generated/graphql-schemas/__mj.ts +1815 -57
  467. package/src/generic/CacheInvalidationResolver.ts +73 -18
  468. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  469. package/src/generic/PushStatusResolver.ts +1 -1
  470. package/src/generic/ResolverBase.ts +335 -326
  471. package/src/generic/RunViewResolver.ts +21 -0
  472. package/src/globalCors.ts +71 -0
  473. package/src/index.ts +231 -300
  474. package/src/isolatedProvider.ts +22 -0
  475. package/src/multiTenancy/index.ts +24 -2
  476. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  477. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  478. package/src/realtimeSessions/README.md +179 -0
  479. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  480. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +168 -0
  481. package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
  482. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  483. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  484. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  485. package/src/realtimeSessions/index.ts +18 -0
  486. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  487. package/src/realtimeSessions/sessionAccess.ts +145 -0
  488. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  489. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  490. package/src/realtimeSessions/verificationCore.ts +502 -0
  491. package/src/realtimeSessions/verificationPages.ts +149 -0
  492. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  493. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  494. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  495. package/src/resolvers/ActionResolver.ts +84 -21
  496. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  497. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  498. package/src/resolvers/CacheStatsResolver.ts +89 -2
  499. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  500. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  501. package/src/resolvers/DatasetResolver.ts +70 -10
  502. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  503. package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
  504. package/src/resolvers/FileResolver.ts +321 -106
  505. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  506. package/src/resolvers/GetDataResolver.ts +6 -3
  507. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  508. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  509. package/src/resolvers/MCPResolver.ts +8 -5
  510. package/src/resolvers/QueryResolver.ts +11 -0
  511. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  512. package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
  513. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  514. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  515. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  516. package/src/resolvers/RunAIAgentResolver.ts +116 -14
  517. package/src/resolvers/RunTemplateResolver.ts +13 -12
  518. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  519. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  520. package/src/resolvers/SyncDataResolver.ts +6 -2
  521. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  522. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  523. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  524. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  525. package/src/resolvers/UserAvatarResolver.ts +111 -0
  526. package/src/resolvers/UserResolver.ts +2 -16
  527. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  528. package/src/resolvers/agent-run-data-guard.ts +89 -0
  529. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  530. package/src/resolvers/avatarInputValidation.ts +123 -0
  531. package/src/resolvers/currentUserRoles.ts +32 -1
  532. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  533. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  534. package/src/rest/ViewOperationsHandler.ts +29 -13
  535. package/src/rest/setupRESTEndpoints.ts +6 -0
  536. package/src/serverExtensionReservedRoots.ts +14 -17
  537. package/src/services/MJStorageBlobStore.ts +31 -30
  538. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  539. package/src/services/TaskGraphActionRunner.ts +25 -5
  540. package/src/services/TaskGraphAgentRunner.ts +188 -11
  541. package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
  542. package/src/sharedCache.ts +305 -0
  543. package/src/types.ts +7 -0
@@ -29,7 +29,10 @@ interface FakeChannelRow {
29
29
  const engineCoAgentsMock = vi.fn((): FakeCoAgentRow[] => []);
30
30
  const engineChannelsMock = vi.fn((): FakeChannelRow[] => []);
31
31
  const engineConfigMock = vi.fn(async (): Promise<void> => undefined);
32
- vi.mock('@memberjunction/ai-engine-base', () => ({
32
+ // PARTIAL mock: the engine and the permission helper are stubbed; the pure usage-record functions (read, merge)
33
+ // stay real, so the usage relay's details tests assert what is actually stored.
34
+ vi.mock('@memberjunction/ai-engine-base', async (importOriginal) => ({
35
+ ...(await importOriginal<Record<string, unknown>>()),
33
36
  AIAgentPermissionHelper: {
34
37
  HasPermission: (...args: unknown[]) => hasPermissionMock(...(args as [])),
35
38
  },
@@ -87,13 +90,14 @@ const cancelInFlightMock = vi.fn((_agentSessionID: string, _callID?: string): nu
87
90
  // class-property/getter references above).
88
91
  const { resolveStorageMock, storeRecordingMock, writeSegmentMock, deleteSegmentsMock } = vi.hoisted(() => ({
89
92
  resolveStorageMock: vi.fn(async (): Promise<string | null> => 'storage-acct-1'),
90
- storeRecordingMock: vi.fn(async (): Promise<StoreRealtimeRecordingResult> => ({ FileID: 'file-1', ErrorMessage: null })),
93
+ storeRecordingMock: vi.fn(async (): Promise<StoreRealtimeRecordingResult> => ({ FileID: 'file-1', ErrorMessage: null, Superseded: false })),
91
94
  writeSegmentMock: vi.fn(async (): Promise<boolean> => true),
92
95
  deleteSegmentsMock: vi.fn(async (): Promise<number> => 0),
93
96
  }));
94
97
  const onChannelStateSaveMock = vi.fn(
95
98
  async (_agentSessionID: string, _channelName: string, stateJson: string): Promise<string> => stateJson,
96
99
  );
100
+ const pruneSessionChannelsMock = vi.fn((_agentSessionID: string, _channelNames: ReadonlyArray<string>): string[] => []);
97
101
  // PARTIAL mock: the service + channel host are stubbed, but the PURE co-agent config module
98
102
  // (DeepMergeConfigs / ResolveEffectiveRealtimeConfig / EvaluateRuntimeOverrideAuthorization /
99
103
  // ParseRealtimeTypeConfiguration) stays REAL — the resolver's pairing/override gate is under test.
@@ -117,7 +121,7 @@ vi.mock('@memberjunction/ai-agents', async (importOriginal) => {
117
121
  },
118
122
  RealtimeChannelServerHost: {
119
123
  get Instance() {
120
- return { OnChannelStateSave: onChannelStateSaveMock };
124
+ return { OnChannelStateSave: onChannelStateSaveMock, PruneSessionChannels: pruneSessionChannelsMock };
121
125
  },
122
126
  },
123
127
  resolveRecordingStorageAccountID: resolveStorageMock,
@@ -213,6 +217,8 @@ interface FakeSession {
213
217
  LatestResult?: { CompleteMessage?: string; Success?: boolean };
214
218
  /** Only set on the fakes exercising `describeSaveFailure`'s "no failure detail recorded" branches. */
215
219
  ResultHistory?: unknown[];
220
+ /** The fork a conversation-detail fake is written on (null is Main). */
221
+ BranchID?: string | null;
216
222
  }
217
223
 
218
224
  /**
@@ -242,13 +248,44 @@ function makeSessionEntity(overrides: Partial<FakeSession> = {}): FakeSession {
242
248
  };
243
249
  }
244
250
 
245
- /** Provider whose GetEntityObject returns a fixed entity per entity name. */
246
- function makeProvider(factory: (entityName: string) => FakeSession): unknown {
247
- return { GetEntityObject: vi.fn(async (name: string) => factory(name)) };
251
+ /** Co-agent observability and paused-run ids, as a voice session's Config holds them. */
252
+ const CO_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000001';
253
+ const PROMPT_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000002';
254
+ const PAUSED_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000003';
255
+
256
+ /** `RunView`/`RunViews` stubs for a provider. */
257
+ interface RunLookups {
258
+ RunView: ReturnType<typeof vi.fn>;
259
+ RunViews: ReturnType<typeof vi.fn>;
260
+ }
261
+
262
+ /**
263
+ * Answers the session run-id lookups: each id in `runIDs` is a run of the session, and
264
+ * `promptRunID` (when given) is the TargetLogID of the co-agent run's Prompt step.
265
+ */
266
+ function sessionRunLookups(runIDs: string[], promptRunID?: string): RunLookups {
267
+ const runView = vi.fn(async (params: { EntityName?: string }) => {
268
+ if (params.EntityName === 'MJ: AI Agent Runs') {
269
+ return { Success: true, Results: runIDs.map((ID) => ({ ID })) };
270
+ }
271
+ if (params.EntityName === 'MJ: AI Agent Run Steps' && promptRunID) {
272
+ return { Success: true, Results: [{ ID: 'c0c0c0c0-0000-4000-8000-0000000000a1', StepType: 'Prompt', TargetLogID: promptRunID }] };
273
+ }
274
+ return { Success: true, Results: [] };
275
+ });
276
+ const runViews = vi.fn(async (paramsList: Array<{ EntityName?: string }>) => Promise.all(paramsList.map((params) => runView(params))));
277
+ return { RunView: runView, RunViews: runViews };
278
+ }
279
+
280
+ /** Provider whose GetEntityObject returns a fixed entity per entity name, plus optional run-id lookups. */
281
+ function makeProvider(factory: (entityName: string) => FakeSession, lookups?: RunLookups): unknown {
282
+ return { GetEntityObject: vi.fn(async (name: string) => factory(name)), ...lookups };
248
283
  }
249
284
 
250
285
  beforeEach(() => {
251
286
  hasPermissionMock.mockReset();
287
+ // Callers may run the agents in play unless a test says otherwise; relays re-check CanRun on the target.
288
+ hasPermissionMock.mockResolvedValue(true);
252
289
  engineCoAgentsMock.mockReset();
253
290
  engineCoAgentsMock.mockReturnValue([]);
254
291
  engineChannelsMock.mockReset();
@@ -390,6 +427,38 @@ describe('RealtimeClientSessionResolver.StartRealtimeClientSession', () => {
390
427
  expect(prepArg.PreferredModelID).toBeUndefined();
391
428
  expect(result.ModelName).toBeUndefined();
392
429
  expect(result.NarrationInstructionsTemplate).toBeUndefined();
430
+ // A direct session says nothing about the transport.
431
+ expect(result.Transport).toBeUndefined();
432
+ expect(result.RelayUrl).toBeUndefined();
433
+ });
434
+
435
+ it("returns a relay session's transport and relay URL as minted, with no token, and logs neither", async () => {
436
+ const ticket = '7d1e0c2a-5555-4666-8777-988899990000';
437
+ const relayUrl = `wss://mjapi.example.test/realtime/relay/${ticket}`;
438
+ hasPermissionMock.mockResolvedValue(true);
439
+ currentProvider = makeProvider(() => makeSessionEntity());
440
+ createSessionMock.mockResolvedValue(makeSessionEntity({ ID: 'session-relay' }));
441
+ prepareClientSessionMock.mockResolvedValue({
442
+ Success: true,
443
+ ClientConfig: {
444
+ Provider: 'gemini-enterprise',
445
+ Model: 'gemini-3.8-live',
446
+ EphemeralToken: '',
447
+ ExpiresAt: '2026-01-01T00:00:00Z',
448
+ Transport: 'relay',
449
+ RelayUrl: relayUrl,
450
+ SessionConfig: { model: 'gemini-3.8-live' },
451
+ },
452
+ });
453
+ const consoleSpies = (['log', 'info', 'warn', 'error', 'debug'] as const).map((method) => vi.spyOn(console, method));
454
+
455
+ const result = await makeResolver().StartRealtimeClientSession('target-1', makeCtx());
456
+
457
+ expect(result).toMatchObject({ Provider: 'gemini-enterprise', EphemeralToken: '', Transport: 'relay', RelayUrl: relayUrl });
458
+ expect(result.SessionConfigJson).not.toContain(ticket);
459
+ const logged = consoleSpies.flatMap((spy) => spy.mock.calls.map((call: unknown[]) => call.map(String).join(' ')));
460
+ expect(logged.filter((line) => line.includes(ticket))).toEqual([]);
461
+ consoleSpies.forEach((spy) => spy.mockRestore());
393
462
  });
394
463
 
395
464
  it('propagates the preferred-model failure (and closes the session) — no silent fallback', async () => {
@@ -685,10 +754,12 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
685
754
  });
686
755
 
687
756
  it('reads the target + co-agent run id from the session config and returns ResultJson', async () => {
688
- currentProvider = makeProvider(() =>
689
- makeSessionEntity({
690
- Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: 'co-run-77' }),
691
- }),
757
+ currentProvider = makeProvider(
758
+ () =>
759
+ makeSessionEntity({
760
+ Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: CO_RUN_ID }),
761
+ }),
762
+ sessionRunLookups([CO_RUN_ID]),
692
763
  );
693
764
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
694
765
  const resolver = makeResolver();
@@ -711,7 +782,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
711
782
  // Target comes from the session, NOT the client.
712
783
  expect(relayArg.TargetAgentID).toBe('target-from-session');
713
784
  // Delegated run nests under the co-agent observability run from the session config.
714
- expect(relayArg.ParentRunID).toBe('co-run-77');
785
+ expect(relayArg.ParentRunID).toBe(CO_RUN_ID);
715
786
  expect(relayArg.Call).toEqual({ CallID: 'call-1', ToolName: 'invoke-target-agent', Arguments: '{"request":"do it"}' });
716
787
  expect(heartbeatMock).toHaveBeenCalledWith('session-1', USER, currentProvider);
717
788
  });
@@ -775,9 +846,9 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
775
846
 
776
847
  it('consumes pendingFeedbackRunID as ResumeRunID and clears it on the next call', async () => {
777
848
  const session = makeSessionEntity({
778
- Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: 'paused-1' }),
849
+ Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
779
850
  });
780
- currentProvider = makeProvider(() => session);
851
+ currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
781
852
  // Resumed run completes (no new pause).
782
853
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"done":1}', Success: true });
783
854
  const resolver = makeResolver();
@@ -786,7 +857,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
786
857
 
787
858
  // The paused id was passed in to resume the run...
788
859
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
789
- expect(relayArg.ResumeRunID).toBe('paused-1');
860
+ expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
790
861
  // ...and cleared from the session config afterward.
791
862
  expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBeUndefined();
792
863
  expect(session.Save).toHaveBeenCalled();
@@ -794,16 +865,16 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
794
865
 
795
866
  it('re-stores a new pendingFeedbackRunID when a resumed run pauses again', async () => {
796
867
  const session = makeSessionEntity({
797
- Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: 'paused-1' }),
868
+ Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
798
869
  });
799
- currentProvider = makeProvider(() => session);
870
+ currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
800
871
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":2}', Success: true, PausedRunID: 'paused-2' });
801
872
  const resolver = makeResolver();
802
873
 
803
874
  await resolver.ExecuteRealtimeSessionTool('session-1', 'call-3', 'invoke-target-agent', '{"request":"yes"}', makeCtx(), makePubSub());
804
875
 
805
876
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
806
- expect(relayArg.ResumeRunID).toBe('paused-1');
877
+ expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
807
878
  expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBe('paused-2');
808
879
  });
809
880
 
@@ -908,6 +979,26 @@ describe('RealtimeClientSessionResolver.RelayRealtimeTranscript', () => {
908
979
  expect(heartbeatMock).toHaveBeenCalledWith('session-1', USER, currentProvider);
909
980
  });
910
981
 
982
+ it("writes the turn on the session's fork", async () => {
983
+ const detail = makeSessionEntity({ ID: 'detail-t' });
984
+ const session = makeSessionEntity({ ConversationID: 'conv-7', Config_: JSON.stringify({ targetAgentID: 'target-1', conversationBranchID: 'b1' }) });
985
+ currentProvider = makeProvider((name) => (name === 'MJ: Conversation Details' ? detail : session));
986
+
987
+ await makeResolver().RelayRealtimeTranscript('session-1', 'user', 'hello', makeCtx());
988
+
989
+ expect(detail.BranchID).toBe('b1');
990
+ });
991
+
992
+ it('writes the turn on Main when the session has no fork', async () => {
993
+ const detail = makeSessionEntity({ ID: 'detail-m' });
994
+ const session = makeSessionEntity({ ConversationID: 'conv-7' });
995
+ currentProvider = makeProvider((name) => (name === 'MJ: Conversation Details' ? detail : session));
996
+
997
+ await makeResolver().RelayRealtimeTranscript('session-1', 'user', 'hello', makeCtx());
998
+
999
+ expect(detail.BranchID).toBeNull();
1000
+ });
1001
+
911
1002
  it("maps a non-user role to 'AI'", async () => {
912
1003
  const detail = makeSessionEntity({ ID: 'detail-2' });
913
1004
  const session = makeSessionEntity();
@@ -2292,10 +2383,11 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2292
2383
  }): { provider: unknown; promptRun: FakeSession } {
2293
2384
  const session =
2294
2385
  opts.session ??
2295
- makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }) });
2296
- const promptRun = opts.promptRun ?? makeSessionEntity({ ID: 'prompt-run-1', TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2386
+ makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }) });
2387
+ const promptRun = opts.promptRun ?? makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2297
2388
  const provider = {
2298
2389
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
2390
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2299
2391
  };
2300
2392
  return { provider, promptRun };
2301
2393
  }
@@ -2321,7 +2413,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2321
2413
  const ok = await resolver.RelayRealtimeUsage('session-1', 30, 5, makeCtx());
2322
2414
 
2323
2415
  expect(ok).toBe(true);
2324
- expect(promptRun.Load).toHaveBeenCalledWith('prompt-run-1');
2416
+ expect(promptRun.Load).toHaveBeenCalledWith(PROMPT_RUN_ID);
2325
2417
  expect(promptRun.TokensPrompt).toBe(130);
2326
2418
  expect(promptRun.TokensCompletion).toBe(45);
2327
2419
  expect(promptRun.TokensUsed).toBe(175);
@@ -2418,7 +2510,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2418
2510
  const { provider, promptRun } = makeUsageProvider({
2419
2511
  session: makeSessionEntity({
2420
2512
  Status: 'Closed',
2421
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
2513
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2422
2514
  }),
2423
2515
  });
2424
2516
  currentProvider = provider;
@@ -2431,6 +2523,69 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
2431
2523
  expect(promptRun.TokensCompletion).toBe(3);
2432
2524
  expect(promptRun.TokensUsed).toBe(12);
2433
2525
  });
2526
+
2527
+ describe('per-modality details (usageDetailsJson)', () => {
2528
+ /** The usage record the run's details hold after the relay. */
2529
+ function storedRecord(promptRun: FakeSession): unknown {
2530
+ const details = promptRun['ModelSpecificResponseDetails'];
2531
+ return typeof details === 'string' ? (JSON.parse(details) as Record<string, unknown>)['RealtimeUsage'] : undefined;
2532
+ }
2533
+
2534
+ it('adds the relayed blocks into the usage record on the prompt run', async () => {
2535
+ const { provider, promptRun } = makeUsageProvider({});
2536
+ currentProvider = provider;
2537
+ const details = JSON.stringify({ Input: { TextTokens: 60, AudioTokens: 40 }, Output: { AudioTokens: 100, VideoTokens: 6192, VideoSeconds: 1.5 } });
2538
+
2539
+ const ok = await makeResolver().RelayRealtimeUsage('session-1', 100, 6292, makeCtx(), details);
2540
+
2541
+ expect(ok).toBe(true);
2542
+ expect(promptRun.TokensCompletion).toBe(6292);
2543
+ expect(storedRecord(promptRun)).toEqual({ Input: { TextTokens: 60, AudioTokens: 40 }, Output: { AudioTokens: 100, VideoTokens: 6192, VideoSeconds: 1.5 } });
2544
+ });
2545
+
2546
+ it('stores a relay that carries only avatar video seconds, rather than treating it as a no-op', async () => {
2547
+ const { provider, promptRun } = makeUsageProvider({});
2548
+ currentProvider = provider;
2549
+
2550
+ const ok = await makeResolver().RelayRealtimeUsage('session-1', 0, 0, makeCtx(), JSON.stringify({ Output: { VideoSeconds: 2.25 } }));
2551
+
2552
+ expect(ok).toBe(true);
2553
+ expect(promptRun.Save).toHaveBeenCalled();
2554
+ expect(storedRecord(promptRun)).toEqual({ Output: { VideoSeconds: 2.25 } });
2555
+ });
2556
+
2557
+ it('is a no-op for details with nothing in them and no tokens', async () => {
2558
+ const { provider, promptRun } = makeUsageProvider({});
2559
+ currentProvider = provider;
2560
+
2561
+ for (const details of ['{"Output":{"VideoSeconds":0}}', '{broken', '{"Output":{"Bogus":5}}']) {
2562
+ await expect(makeResolver().RelayRealtimeUsage('session-1', 0, 0, makeCtx(), details)).resolves.toBe(true);
2563
+ }
2564
+ expect(promptRun.Load).not.toHaveBeenCalled();
2565
+ });
2566
+
2567
+ it('drops details longer than a relay ever needs, and still stores the tokens', async () => {
2568
+ const { provider, promptRun } = makeUsageProvider({});
2569
+ currentProvider = provider;
2570
+ const oversized = JSON.stringify({ Output: { VideoSeconds: 2 }, Padding: 'x'.repeat(5000) });
2571
+
2572
+ const ok = await makeResolver().RelayRealtimeUsage('session-1', 10, 5, makeCtx(), oversized);
2573
+
2574
+ expect(ok).toBe(true);
2575
+ expect(promptRun.TokensPrompt).toBe(10);
2576
+ expect(storedRecord(promptRun)).toBeUndefined();
2577
+ });
2578
+
2579
+ it('keeps only the fields a block defines, with usable values', async () => {
2580
+ const { provider, promptRun } = makeUsageProvider({});
2581
+ currentProvider = provider;
2582
+ const details = JSON.stringify({ Output: { VideoSeconds: -3, AudioTokens: 'lots', VideoTokens: 12, Injected: 1 }, Other: { TextTokens: 1 } });
2583
+
2584
+ await makeResolver().RelayRealtimeUsage('session-1', 1, 1, makeCtx(), details);
2585
+
2586
+ expect(storedRecord(promptRun)).toEqual({ Output: { VideoTokens: 12 } });
2587
+ });
2588
+ });
2434
2589
  });
2435
2590
 
2436
2591
  describe('RealtimeClientSessionResolver.StartRealtimeClientSession — pairing constraints (MJ: AI Agent Co Agents)', () => {
@@ -2815,7 +2970,7 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
2815
2970
  expect(relayArg.AllowedAgents?.map(a => a.agentId)).toEqual(['skip-1']);
2816
2971
  });
2817
2972
 
2818
- it('passes the persisted directActions from the session config into ExecuteRelayedTool', async () => {
2973
+ it('does not pass the directActions stored in the session config into ExecuteRelayedTool', async () => {
2819
2974
  currentProvider = makeProvider(() =>
2820
2975
  makeSessionEntity({
2821
2976
  Config_: JSON.stringify({
@@ -2832,7 +2987,167 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
2832
2987
  );
2833
2988
 
2834
2989
  const relayArg = executeRelayedToolMock.mock.calls[0][0] as { DirectActions?: { enabled: boolean; actionNames: string[] } };
2835
- expect(relayArg.DirectActions).toEqual({ enabled: true, actionNames: ['SendEmail'] });
2990
+ // The owner can edit Config, so the service takes direct actions from server-side state instead.
2991
+ expect(relayArg.DirectActions).toBeUndefined();
2992
+ });
2993
+ });
2994
+
2995
+ describe('RealtimeClientSessionResolver — delegated runs get the browser session and app context', () => {
2996
+ const appContext = {
2997
+ App: { Name: 'Dashboards', Description: '' },
2998
+ ActiveNavItem: { Name: 'Sales Overview' },
2999
+ OtherNavItems: [],
3000
+ User: { Name: 'A', Roles: [] },
3001
+ Capabilities: { Tools: [{ Name: 'AddPanel', Description: 'Adds a panel', InputSchema: {} }] },
3002
+ };
3003
+ /** Mirrors the resolver's MAX_CLIENT_TOOLS_JSON_CHARS, the cap on a stored app-context snapshot. */
3004
+ const APP_CONTEXT_CAP_CHARS = 256_000;
3005
+
3006
+ beforeEach(() => {
3007
+ // The resolver logs each relayed tool dispatch to the console; keep test output clean.
3008
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
3009
+ });
3010
+
3011
+ /** Starts a session whose browser sent `appContextJson` and returns the created session entity. */
3012
+ async function startWithAppContext(appContextJson: string): Promise<FakeSession> {
3013
+ hasPermissionMock.mockResolvedValue(true);
3014
+ currentProvider = makeProvider(() => makeSessionEntity());
3015
+ const createdSession = makeSessionEntity({ ID: 'session-app' });
3016
+ createSessionMock.mockResolvedValue(createdSession);
3017
+ prepareClientSessionMock.mockResolvedValue({
3018
+ Success: true,
3019
+ ClientConfig: { Provider: 'openai', Model: 'm', EphemeralToken: 'ek', ExpiresAt: '2099-01-01T00:00:00Z', SessionConfig: {} },
3020
+ });
3021
+ await makeResolver().StartRealtimeClientSession(
3022
+ 'target-1', makeCtx(),
3023
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
3024
+ 'app-9', appContextJson,
3025
+ );
3026
+ return createdSession;
3027
+ }
3028
+
3029
+ /** An app-context snapshot whose JSON is exactly `chars` long, padded through AdditionalContext. */
3030
+ function snapshotOfLength(chars: number): typeof appContext & { AdditionalContext: { Padding: string } } {
3031
+ const unpadded = { ...appContext, AdditionalContext: { Padding: '' } };
3032
+ return { ...appContext, AdditionalContext: { Padding: 'x'.repeat(chars - JSON.stringify(unpadded).length) } };
3033
+ }
3034
+
3035
+ it('stores the parsed appContext on the session config in the post-prepare write', async () => {
3036
+ const session = await startWithAppContext(JSON.stringify(appContext));
3037
+
3038
+ expect(JSON.parse(session.Config_ as string).appContext).toEqual(appContext);
3039
+ });
3040
+
3041
+ it('stores an appContext whose JSON is exactly at the size cap', async () => {
3042
+ const atCap = snapshotOfLength(APP_CONTEXT_CAP_CHARS);
3043
+ expect(JSON.stringify(atCap)).toHaveLength(APP_CONTEXT_CAP_CHARS);
3044
+
3045
+ const session = await startWithAppContext(JSON.stringify(atCap));
3046
+
3047
+ expect(JSON.parse(session.Config_ as string).appContext).toEqual(atCap);
3048
+ });
3049
+
3050
+ it('stores no appContext over the size cap and logs a warning', async () => {
3051
+ errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
3052
+
3053
+ const session = await startWithAppContext(JSON.stringify(snapshotOfLength(APP_CONTEXT_CAP_CHARS + 1)));
3054
+
3055
+ const persisted = JSON.parse(session.Config_ as string) as { targetAgentID?: string; appContext?: unknown };
3056
+ expect(persisted.targetAgentID).toBe('target-1');
3057
+ expect(persisted.appContext).toBeUndefined();
3058
+ const warning = errSpy.mock.calls.map((call) => String(call[0])).find((line) => line.includes('app context'));
3059
+ expect(warning).toContain('[WARNING]');
3060
+ expect(warning).toContain('session-app');
3061
+ expect(warning).toContain(String(APP_CONTEXT_CAP_CHARS));
3062
+ });
3063
+
3064
+ it('passes the browser session id and the stored appContext into ExecuteRelayedTool', async () => {
3065
+ currentProvider = makeProvider(() =>
3066
+ makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', appContext }) }),
3067
+ );
3068
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
3069
+
3070
+ await makeResolver().ExecuteRealtimeSessionTool(
3071
+ 'session-1', 'call-1', 'invoke-target-agent', '{"request":"x"}', makeCtx(), makePubSub(),
3072
+ );
3073
+
3074
+ const relayArg = executeRelayedToolMock.mock.calls[0][0] as { BrowserSessionID?: string; AppContext?: unknown };
3075
+ // The client-tool channel is keyed on the caller's GraphQL session id (x-session-id).
3076
+ expect(relayArg.BrowserSessionID).toBe('pubsub-session-1');
3077
+ expect(relayArg.AppContext).toEqual(appContext);
3078
+ });
3079
+
3080
+ it('passes no AppContext when the session config has none', async () => {
3081
+ currentProvider = makeProvider(() => makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1' }) }));
3082
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
3083
+
3084
+ await makeResolver().ExecuteRealtimeSessionTool('session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub());
3085
+
3086
+ const relayArg = executeRelayedToolMock.mock.calls[0][0] as { BrowserSessionID?: string; AppContext?: unknown };
3087
+ expect(relayArg.AppContext).toBeUndefined();
3088
+ expect(relayArg.BrowserSessionID).toBe('pubsub-session-1');
3089
+ });
3090
+
3091
+ it('ignores a stored appContext that is not an object', async () => {
3092
+ currentProvider = makeProvider(() =>
3093
+ makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', appContext: 'not-an-object' }) }),
3094
+ );
3095
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
3096
+
3097
+ await makeResolver().ExecuteRealtimeSessionTool('session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub());
3098
+
3099
+ const relayArg = executeRelayedToolMock.mock.calls[0][0] as { AppContext?: unknown };
3100
+ expect(relayArg.AppContext).toBeUndefined();
3101
+ });
3102
+
3103
+ it('passes no AppContext when the run is elevated to the system user for a scoped-anonymous caller', async () => {
3104
+ const systemUser = { ID: 'system-1', Email: 'system@system.org' } as UserInfo;
3105
+ const anonymousCaller = {
3106
+ ID: 'anon-1',
3107
+ Email: 'anonymous@magic-link.local',
3108
+ IsMagicLinkAnonymous: true,
3109
+ MagicLinkScope: { ResourceID: 'scope-res-1' },
3110
+ } as UserInfo;
3111
+ /** A resolver whose caller is a scoped anonymous magic-link visitor. */
3112
+ class AnonymousCallerResolver extends RealtimeClientSessionResolver {
3113
+ protected override GetUserFromPayload(): UserInfo | undefined {
3114
+ return anonymousCaller;
3115
+ }
3116
+ }
3117
+ getSystemUserMock.mockReturnValue(systemUser);
3118
+ currentProvider = makeProvider(() =>
3119
+ makeSessionEntity({ UserID: 'anon-1', Config_: JSON.stringify({ targetAgentID: 'target-1', appContext }) }),
3120
+ );
3121
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
3122
+
3123
+ await new AnonymousCallerResolver().ExecuteRealtimeSessionTool(
3124
+ 'session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub(),
3125
+ );
3126
+
3127
+ expect(executeRelayedToolMock.mock.calls[0][1]).toBe(systemUser);
3128
+ const relayArg = executeRelayedToolMock.mock.calls[0][0] as { BrowserSessionID?: string; AppContext?: unknown };
3129
+ expect(relayArg.AppContext).toBeUndefined();
3130
+ expect(relayArg.BrowserSessionID).toBe('pubsub-session-1');
3131
+ });
3132
+
3133
+ it('keeps appContext and directActions when a paused run rewrites the session config', async () => {
3134
+ const directActions = { enabled: true, actionNames: ['SendEmail'] };
3135
+ const session = makeSessionEntity({
3136
+ Config_: JSON.stringify({ targetAgentID: 'target-1', appContext, directActions }),
3137
+ });
3138
+ currentProvider = makeProvider(() => session);
3139
+ executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":1}', Success: true, PausedRunID: 'paused-1' });
3140
+
3141
+ await makeResolver().ExecuteRealtimeSessionTool('session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub());
3142
+
3143
+ const persisted = JSON.parse(session.Config_ as string) as {
3144
+ pendingFeedbackRunID?: string;
3145
+ appContext?: unknown;
3146
+ directActions?: unknown;
3147
+ };
3148
+ expect(persisted.pendingFeedbackRunID).toBe('paused-1');
3149
+ expect(persisted.appContext).toEqual(appContext);
3150
+ expect(persisted.directActions).toEqual(directActions);
2836
3151
  });
2837
3152
  });
2838
3153
 
@@ -2925,7 +3240,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2925
3240
  expect(executeRelayedToolMock.mock.calls[0][1]).toBe(ANON_USER);
2926
3241
  });
2927
3242
 
2928
- it('writes the hidden direct-action tool turn (ExecuteRealtimeSessionTool) as the CALLER, not the system user (#4791)', async () => {
3243
+ it('refuses a direct action from a scoped anonymous caller without dispatching it or writing a turn as the system user (#4791)', async () => {
2929
3244
  const detail = makeSessionEntity({ ID: 'detail-1' });
2930
3245
  currentProvider = {
2931
3246
  GetEntityObject: vi.fn(async (name: string) =>
@@ -2935,17 +3250,16 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2935
3250
  executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"success":true}', Success: true });
2936
3251
  const resolver = makeAnonResolver();
2937
3252
 
2938
- await resolver.ExecuteRealtimeSessionTool(
3253
+ const out = await resolver.ExecuteRealtimeSessionTool(
2939
3254
  'session-1', 'call-1', 'Some Direct Action', '{}', makeCtx(), makePubSub(),
2940
3255
  );
2941
3256
 
2942
- // The dispatch itself still runs elevated (SYSTEM_USER)…
2943
- expect(executeRelayedToolMock.mock.calls[0][1]).toBe(SYSTEM_USER);
2944
- // …but the hidden Conversation Detail turn is written as the CALLER, never the system user.
2945
- expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', ANON_USER);
3257
+ // Scope-limited callers get no direct actions, so nothing is dispatched, elevated or not…
3258
+ expect(JSON.parse(out)).toMatchObject({ success: false });
3259
+ expect(executeRelayedToolMock).not.toHaveBeenCalled();
3260
+ // …and no hidden Conversation Detail turn is written, least of all as the system user.
2946
3261
  expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
2947
- expect(detail.Save).toHaveBeenCalled();
2948
- expect(detail.UserID).toBe('anon-1');
3262
+ expect(detail.Save).not.toHaveBeenCalled();
2949
3263
  });
2950
3264
 
2951
3265
  it('writes the direct-action tool turn as the caller unchanged for a normal authenticated user', async () => {
@@ -2966,11 +3280,12 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2966
3280
  it('accumulates relayed usage onto the prompt run under the SYSTEM user', async () => {
2967
3281
  const session = makeSessionEntity({
2968
3282
  UserID: 'anon-1',
2969
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3283
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2970
3284
  });
2971
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
3285
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
2972
3286
  currentProvider = {
2973
3287
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
3288
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2974
3289
  };
2975
3290
  const resolver = makeAnonResolver();
2976
3291
 
@@ -2985,9 +3300,9 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2985
3300
  it('mirrors the transcript turn onto the prompt run as SYSTEM while the Conversation Detail stays the caller', async () => {
2986
3301
  const session = makeSessionEntity({
2987
3302
  UserID: 'anon-1',
2988
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3303
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
2989
3304
  });
2990
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3305
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
2991
3306
  const detail = makeSessionEntity({ ID: 'detail-1' });
2992
3307
  currentProvider = {
2993
3308
  GetEntityObject: vi.fn(async (name: string) => {
@@ -2995,6 +3310,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
2995
3310
  if (name === 'MJ: Conversation Details') return detail;
2996
3311
  return session;
2997
3312
  }),
3313
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
2998
3314
  };
2999
3315
  const resolver = makeAnonResolver();
3000
3316
 
@@ -3011,11 +3327,12 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3011
3327
  it('records the co-agent tool turn (RelayRealtimeToolTurn) under the SYSTEM user', async () => {
3012
3328
  const session = makeSessionEntity({
3013
3329
  UserID: 'anon-1',
3014
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3330
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3015
3331
  });
3016
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3332
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3017
3333
  currentProvider = {
3018
3334
  GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
3335
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3019
3336
  };
3020
3337
  const resolver = makeAnonResolver();
3021
3338
 
@@ -3028,9 +3345,9 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3028
3345
  it('writes the hidden co-agent tool turn (RelayRealtimeToolTurn) as the CALLER (#4791)', async () => {
3029
3346
  const session = makeSessionEntity({
3030
3347
  UserID: 'anon-1',
3031
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3348
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3032
3349
  });
3033
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3350
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3034
3351
  const detail = makeSessionEntity({ ID: 'detail-3' });
3035
3352
  currentProvider = {
3036
3353
  GetEntityObject: vi.fn(async (name: string) => {
@@ -3038,6 +3355,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3038
3355
  if (name === 'MJ: Conversation Details') return detail;
3039
3356
  return session;
3040
3357
  }),
3358
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3041
3359
  };
3042
3360
  const resolver = makeAnonResolver();
3043
3361
 
@@ -3080,6 +3398,30 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3080
3398
  expect(prepInput.UserID).toBe('anon-1');
3081
3399
  });
3082
3400
 
3401
+ /** Starts a session as `resolver`'s caller and returns the input the prepare service received. */
3402
+ async function startAndCapturePrepInput(resolver: RealtimeClientSessionResolver, userID: string): Promise<{ ExcludeDirectActions?: boolean }> {
3403
+ currentProvider = makeProvider(() => makeSessionEntity({ UserID: userID }));
3404
+ createSessionMock.mockResolvedValue(makeSessionEntity({ ID: 'session-start', UserID: userID }));
3405
+ prepareClientSessionMock.mockResolvedValue({
3406
+ Success: true,
3407
+ ClientConfig: { Provider: 'openai', Model: 'gpt-realtime', EphemeralToken: 'ek_abc', ExpiresAt: '2026-01-01T00:00:00Z', SessionConfig: {} },
3408
+ });
3409
+ await resolver.StartRealtimeClientSession('target-1', makeCtx());
3410
+ return prepareClientSessionMock.mock.calls[0][0] as { ExcludeDirectActions?: boolean };
3411
+ }
3412
+
3413
+ it('prepares a scope-limited caller\'s session with no direct actions', async () => {
3414
+ const prepInput = await startAndCapturePrepInput(makeAnonResolver(), 'anon-1');
3415
+
3416
+ expect(prepInput.ExcludeDirectActions).toBe(true);
3417
+ });
3418
+
3419
+ it('prepares a caller that is not scope-limited with direct actions', async () => {
3420
+ const prepInput = await startAndCapturePrepInput(makeResolver(), 'user-1');
3421
+
3422
+ expect(prepInput.ExcludeDirectActions).toBe(false);
3423
+ });
3424
+
3083
3425
  it('junction-links delegated artifacts under the SYSTEM user and stamps the hidden anchor with the SESSION owner', async () => {
3084
3426
  const session = makeSessionEntity({ UserID: 'anon-1' });
3085
3427
  const junctions: FakeSession[] = [];
@@ -3198,7 +3540,21 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3198
3540
  expect((writeSegmentMock.mock.calls[0][0] as { ContextUser: unknown }).ContextUser).toBe(SYSTEM_USER);
3199
3541
  });
3200
3542
 
3201
- it('recording writes stay entirely on the caller for a normal authenticated user', async () => {
3543
+ it('refuses a crash-recovery segment once the session has its consolidated recording (no orphan shard) (#5197)', async () => {
3544
+ // A shard upload still on the wire at hang-up can land after UploadRealtimeRecording stored the
3545
+ // file and deleted the shards; nothing deletes a shard written after that.
3546
+ const session = makeSessionEntity({ UserID: 'anon-1', RecordingFileID: 'file-1' });
3547
+ const agent = makeSessionEntity({ ID: 'co-agent-1' });
3548
+ currentProvider = { GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Agents' ? agent : session)) };
3549
+ const resolver = makeAnonResolver();
3550
+
3551
+ const ok = await resolver.UploadRealtimeRecordingSegment('session-1', 3, AUDIO_B64, 'audio/L16;rate=24000', makeCtx());
3552
+
3553
+ expect(ok).toBe(false);
3554
+ expect(writeSegmentMock).not.toHaveBeenCalled();
3555
+ });
3556
+
3557
+ it('a normal authenticated owner’s recording store runs as the SYSTEM user; ownership stays on the caller (#5195)', async () => {
3202
3558
  currentProvider = makeRecordingProvider();
3203
3559
  // A named caller owning the session (the recording provider stamps UserID 'anon-1', so re-stamp).
3204
3560
  const session = makeSessionEntity({ UserID: 'user-1' });
@@ -3211,24 +3567,29 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
3211
3567
  const result = await resolver.UploadRealtimeRecording('session-1', AUDIO_B64, 'audio/wav', makeCtx(), 1000, true);
3212
3568
 
3213
3569
  expect(result.Success).toBe(true);
3214
- expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: AI Agents', USER);
3215
- expect((storeRecordingMock.mock.calls[0][0] as { ContextUser: unknown }).ContextUser).toBe(USER);
3216
- expect(getSystemUserMock).not.toHaveBeenCalled();
3570
+ // Ownership is proven as the CALLER; the store (MJ: Files, the file link and the session stamp —
3571
+ // entities the stock UI role cannot create/update) runs as SYSTEM.
3572
+ expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: AI Agent Sessions', USER);
3573
+ expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: AI Agents', SYSTEM_USER);
3574
+ expect(resolveStorageMock.mock.calls[0][1]).toBe(SYSTEM_USER);
3575
+ expect((storeRecordingMock.mock.calls[0][0] as { ContextUser: unknown }).ContextUser).toBe(SYSTEM_USER);
3576
+ expect(deleteSegmentsMock).toHaveBeenCalledWith('session-1', 'storage-acct-1', SYSTEM_USER);
3217
3577
  });
3218
3578
 
3219
3579
  /** Provider routing a hidden-tool-turn RelayRealtimeToolTurn call: session, prompt run, detail. */
3220
3580
  function makeToolTurnProvider(detail: FakeSession): unknown {
3221
3581
  const session = makeSessionEntity({
3222
3582
  UserID: 'anon-1',
3223
- Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID: 'prompt-run-1' }),
3583
+ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
3224
3584
  });
3225
- const promptRun = makeSessionEntity({ ID: 'prompt-run-1', Messages: null });
3585
+ const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
3226
3586
  return {
3227
3587
  GetEntityObject: vi.fn(async (name: string) => {
3228
3588
  if (name === 'MJ: AI Prompt Runs') return promptRun;
3229
3589
  if (name === 'MJ: Conversation Details') return detail;
3230
3590
  return session;
3231
3591
  }),
3592
+ ...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
3232
3593
  };
3233
3594
  }
3234
3595
 
@@ -3388,7 +3749,8 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3388
3749
 
3389
3750
  it('excludes every colleague when the caller can run none of them', async () => {
3390
3751
  currentProvider = makeProvider(() => makeMultiTargetSession('user-1'));
3391
- hasPermissionMock.mockResolvedValue(false);
3752
+ // The caller can run the lead target, and none of the colleagues.
3753
+ hasPermissionMock.mockImplementation((...args: unknown[]) => Promise.resolve(args[0] === 'target-1'));
3392
3754
 
3393
3755
  await makeResolver().ExecuteRealtimeSessionTool(
3394
3756
  'session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub(),
@@ -3397,7 +3759,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3397
3759
  expect(dispatchedAgentIDs()).toEqual([]);
3398
3760
  });
3399
3761
 
3400
- it('passes a single-target session through with no permission checks', async () => {
3762
+ it('passes a single-target session through with only the target permission check', async () => {
3401
3763
  currentProvider = makeProvider(() => makeSessionEntity());
3402
3764
  hasPermissionMock.mockResolvedValue(true);
3403
3765
 
@@ -3407,7 +3769,8 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
3407
3769
 
3408
3770
  const input = executeRelayedToolMock.mock.calls[0][0] as { AllowedAgents?: unknown };
3409
3771
  expect(input.AllowedAgents).toBeUndefined();
3410
- expect(hasPermissionMock).not.toHaveBeenCalled();
3772
+ expect(hasPermissionMock).toHaveBeenCalledTimes(1);
3773
+ expect((hasPermissionMock.mock.calls[0] as unknown[])[0]).toBe('target-1');
3411
3774
  });
3412
3775
  });
3413
3776
 
@@ -3438,18 +3801,36 @@ describe('RealtimeClientSessionResolver — conversation-history hydration', ()
3438
3801
  return { ID: `h-${message ?? 'x'}`, Role: role, Message: message, HiddenToUser: false, AgentSessionID: null, ...over };
3439
3802
  }
3440
3803
 
3804
+ /** A `MJ: Conversation Branches` row as the branch-path read selects it. */
3805
+ interface BranchRow {
3806
+ ID: string;
3807
+ ConversationID: string;
3808
+ ParentBranchID: string | null;
3809
+ ForkFromSequence: number | null;
3810
+ Name: string | null;
3811
+ }
3812
+
3441
3813
  /**
3442
3814
  * Provider that answers BOTH Conversation Details queries separately: the prior-leg transcript
3443
- * (filtered by `AgentSessionID IN (…)`) and the conversation history (`ConversationID=…`).
3444
- * Routing on the filter is what keeps the two from being confused for one another.
3815
+ * (filtered by `AgentSessionID IN (…)`) and the conversation history (the session's path filter).
3816
+ * Routing on the filter is what keeps the two from being confused for one another. It also
3817
+ * serves the conversation's branch rows, which the start reads to check the named fork and
3818
+ * to build its path.
3445
3819
  */
3446
3820
  function makeProvider(opts: {
3447
3821
  chain?: Record<string, { UserID: string; LastSessionID?: string | null }>;
3448
3822
  legRows?: Array<{ ID: string; Role: string; Message: string | null; HiddenToUser: boolean }>;
3449
3823
  historyRows?: HistoryRow[];
3450
3824
  historyFails?: boolean;
3825
+ branchRows?: BranchRow[];
3826
+ branchesFail?: boolean;
3451
3827
  }): { provider: unknown; runView: ReturnType<typeof vi.fn> } {
3452
3828
  const runView = vi.fn(async (params: { EntityName: string; ExtraFilter?: string }) => {
3829
+ if (params.EntityName === 'MJ: Conversation Branches') {
3830
+ return opts.branchesFail
3831
+ ? { Success: false, ErrorMessage: 'db down', Results: [] }
3832
+ : { Success: true, Results: opts.branchRows ?? [] };
3833
+ }
3453
3834
  if (params.EntityName === 'MJ: Conversation Details') {
3454
3835
  if (params.ExtraFilter?.startsWith('AgentSessionID IN')) {
3455
3836
  return { Success: true, Results: opts.legRows ?? [] };
@@ -3478,6 +3859,15 @@ describe('RealtimeClientSessionResolver — conversation-history hydration', ()
3478
3859
  return { provider, runView };
3479
3860
  }
3480
3861
 
3862
+ /** The history read: the Conversation Details query that is not the prior-leg lookup. */
3863
+ function historyCall(runView: ReturnType<typeof vi.fn>): { ExtraFilter: string; OrderBy: string } | undefined {
3864
+ const call = runView.mock.calls.find((c) => {
3865
+ const params = c[0] as { EntityName: string; ExtraFilter?: string };
3866
+ return params.EntityName === 'MJ: Conversation Details' && !params.ExtraFilter?.startsWith('AgentSessionID IN');
3867
+ });
3868
+ return call?.[0] as { ExtraFilter: string; OrderBy: string } | undefined;
3869
+ }
3870
+
3481
3871
  function historyArg(): Array<{ role: string; content: unknown }> {
3482
3872
  return (prepareClientSessionMock.mock.calls[0][0] as { ConversationMessages: Array<{ role: string; content: unknown }> })
3483
3873
  .ConversationMessages;
@@ -3515,17 +3905,62 @@ describe('RealtimeClientSessionResolver — conversation-history hydration', ()
3515
3905
  ]);
3516
3906
  });
3517
3907
 
3518
- it('scopes the read to the session\'s conversation', async () => {
3908
+ /** Starts a session on `conversationId`, optionally on a fork (the mutation's last argument). */
3909
+ function start(conversationId: string, forkId?: string): Promise<{ AgentSessionId: string }> {
3910
+ return makeResolver().StartRealtimeClientSession(
3911
+ 'target-1', makeCtx(), conversationId,
3912
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
3913
+ undefined, undefined, forkId,
3914
+ );
3915
+ }
3916
+
3917
+ it('reads Main when the start names no fork, with the apostrophe doubled once', async () => {
3519
3918
  const { provider, runView } = makeProvider({ historyRows: [row('User', 'hi')] });
3520
3919
  currentProvider = provider;
3521
3920
 
3522
- await makeResolver().StartRealtimeClientSession('target-1', makeCtx(), "conv-o'brien");
3921
+ await start("conv-o'brien");
3523
3922
 
3524
- const call = runView.mock.calls.find(
3525
- (c) => (c[0] as { ExtraFilter?: string }).ExtraFilter?.startsWith('ConversationID='),
3526
- );
3527
- // The apostrophe must be doubled, not passed through — this string is concatenated into SQL.
3528
- expect((call![0] as { ExtraFilter: string }).ExtraFilter).toBe("ConversationID='conv-o''brien'");
3923
+ expect(historyCall(runView)).toMatchObject({ ExtraFilter: "[ConversationID]='conv-o''brien' AND [BranchID] IS NULL AND [ReplacedAt] IS NULL", OrderBy: 'Sequence ASC' });
3924
+ expect(runView.mock.calls.some((c) => (c[0] as { EntityName: string }).EntityName === 'MJ: Conversation Branches')).toBe(false);
3925
+ expect(historyArg()).toEqual([{ role: 'user', content: 'hi' }]);
3926
+ });
3927
+
3928
+ it('reads the path of the fork the start names, and stores the fork on the session', async () => {
3929
+ const { provider, runView } = makeProvider({
3930
+ historyRows: [row('User', 'in the fork')],
3931
+ branchRows: [{ ID: 'b1', ConversationID: 'conv-1', ParentBranchID: null, ForkFromSequence: 4, Name: null }],
3932
+ });
3933
+ currentProvider = provider;
3934
+
3935
+ await start('conv-1', 'B1');
3936
+
3937
+ expect(historyCall(runView)).toMatchObject({
3938
+ ExtraFilter: "[ConversationID]='conv-1' AND ([BranchID]='b1' OR ([BranchID] IS NULL AND [Sequence] <= 4)) AND [ReplacedAt] IS NULL",
3939
+ OrderBy: 'Sequence ASC',
3940
+ });
3941
+ const config = JSON.parse((createSessionMock.mock.calls.at(-1)![0] as { config: string }).config) as { conversationBranchID?: string };
3942
+ expect(config.conversationBranchID).toBe('b1');
3943
+ });
3944
+
3945
+ it.each([
3946
+ ['the fork is not a fork of the conversation', { branchRows: [] }],
3947
+ ['the fork rows cannot be read', { branchesFail: true }],
3948
+ ])('refuses the start when %s', async (_label, opts) => {
3949
+ const { provider } = makeProvider({ historyRows: [row('User', 'never read')], ...opts });
3950
+ currentProvider = provider;
3951
+ const sessionsBefore = createSessionMock.mock.calls.length;
3952
+
3953
+ await expect(start('conv-1', 'b1')).rejects.toThrow(/fork b1/);
3954
+ expect(createSessionMock.mock.calls.length).toBe(sessionsBefore);
3955
+ });
3956
+
3957
+ it('refuses a fork without a conversation', async () => {
3958
+ const { provider } = makeProvider({});
3959
+ currentProvider = provider;
3960
+
3961
+ await expect(makeResolver().StartRealtimeClientSession(
3962
+ 'target-1', makeCtx(), undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 'b1',
3963
+ )).rejects.toThrow(/needs a conversation/);
3529
3964
  });
3530
3965
 
3531
3966
  it('skips hidden rows, error rows, and empty messages', async () => {
@@ -3636,6 +4071,195 @@ describe('RealtimeClientSessionResolver — conversation-history hydration', ()
3636
4071
  await makeResolver().StartRealtimeClientSession('target-1', makeCtx());
3637
4072
 
3638
4073
  expect(historyArg()).toEqual([]);
3639
- expect(runView.mock.calls.some((c) => (c[0] as { ExtraFilter?: string }).ExtraFilter?.startsWith('ConversationID='))).toBe(false);
4074
+ expect(historyCall(runView)).toBeUndefined();
4075
+ expect(runView.mock.calls.some((c) => (c[0] as { EntityName: string }).EntityName === 'MJ: Conversations')).toBe(false);
4076
+ });
4077
+ });
4078
+
4079
+
4080
+ describe('RealtimeClientSessionResolver — channel scoping at mint (Realtime Channels v2)', () => {
4081
+ const okConfig = { Provider: 'openai', Model: 'm', EphemeralToken: 'ek', ExpiresAt: '2099-01-01T00:00:00Z', SessionConfig: {} };
4082
+ const CANDIDATE = {
4083
+ Key: 'Whiteboard',
4084
+ DefaultAvailability: 'all-sessions',
4085
+ DisplayPolicy: 'open-on-start',
4086
+ MaxExposure: 'pixels',
4087
+ ToolNamePrefix: 'Whiteboard_',
4088
+ Tools: [{ Name: 'Whiteboard_AddNote', Description: 'Add a note', ParametersSchema: { type: 'object' } }],
4089
+ };
4090
+ const POLICY = {
4091
+ Version: 1,
4092
+ Channels: [{ Key: 'Whiteboard', DisplayPolicy: 'open-on-start', MaxExposure: 'pixels', Source: 'default' }],
4093
+ ExcludedChannels: [{ Key: 'RemoteBrowser', Reason: 'excluded-by-config' }],
4094
+ };
4095
+
4096
+ function setupStart(prepOverrides: Record<string, unknown> = {}): { created: FakeSession } {
4097
+ hasPermissionMock.mockResolvedValue(true);
4098
+ currentProvider = makeProvider(() => makeSessionEntity());
4099
+ const created = makeSessionEntity({ ID: 'session-ch' });
4100
+ createSessionMock.mockResolvedValue(created);
4101
+ prepareClientSessionMock.mockResolvedValue({ Success: true, ClientConfig: okConfig, ...prepOverrides });
4102
+ pruneSessionChannelsMock.mockClear();
4103
+ return { created };
4104
+ }
4105
+
4106
+ async function start(channelCandidatesJson?: string) {
4107
+ const resolver = makeResolver();
4108
+ return resolver.StartRealtimeClientSession(
4109
+ 'target-1', makeCtx(),
4110
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
4111
+ undefined, undefined, channelCandidatesJson,
4112
+ );
4113
+ }
4114
+
4115
+ it('threads valid candidates to PrepareClientSession and returns the resolved policy as ClientPolicyJson', async () => {
4116
+ setupStart({ ClientPolicy: POLICY });
4117
+ const result = await start(JSON.stringify([CANDIDATE]));
4118
+ const prepArg = prepareClientSessionMock.mock.calls[0][0] as { ChannelCandidates?: Array<{ Key: string }> };
4119
+ expect(prepArg.ChannelCandidates?.map((c) => c.Key)).toEqual(['Whiteboard']);
4120
+ expect(JSON.parse(result.ClientPolicyJson ?? 'null')).toEqual(POLICY);
4121
+ });
4122
+
4123
+ it('returns the avatar status as AvatarStatusJson, and none when the prepare reported none', async () => {
4124
+ setupStart({ AvatarStatus: { Requested: true, Granted: false, Reason: 'endpoint' } });
4125
+ const result = await start(undefined);
4126
+ expect(JSON.parse(result.AvatarStatusJson ?? 'null')).toEqual({ Requested: true, Granted: false, Reason: 'endpoint' });
4127
+
4128
+ setupStart();
4129
+ const none = await start(undefined);
4130
+ expect(none.AvatarStatusJson).toBeUndefined();
4131
+ });
4132
+
4133
+ it("threads an app's showsAgentVideo: false to PrepareClientSession, and nothing when the app did not say", async () => {
4134
+ const startWith = (showsAgentVideo?: boolean | null) =>
4135
+ makeResolver().StartRealtimeClientSession(
4136
+ 'target-1', makeCtx(),
4137
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
4138
+ undefined, undefined, undefined, showsAgentVideo as boolean | undefined,
4139
+ );
4140
+ setupStart({ AvatarStatus: { Requested: true, Granted: false, Reason: 'host' } });
4141
+ const result = await startWith(false);
4142
+ expect((prepareClientSessionMock.mock.calls[0][0] as { ShowsAgentVideo?: boolean }).ShowsAgentVideo).toBe(false);
4143
+ expect(JSON.parse(result.AvatarStatusJson ?? 'null')).toEqual({ Requested: true, Granted: false, Reason: 'host' });
4144
+
4145
+ for (const unstated of [undefined, null]) {
4146
+ prepareClientSessionMock.mockClear();
4147
+ setupStart();
4148
+ await startWith(unstated);
4149
+ expect((prepareClientSessionMock.mock.calls[0][0] as { ShowsAgentVideo?: boolean }).ShowsAgentVideo).toBeUndefined();
4150
+ }
4151
+ });
4152
+
4153
+ it('sends no candidates and returns no policy when the caller reported none (a client that predates scoping)', async () => {
4154
+ setupStart();
4155
+ const result = await start(undefined);
4156
+ const prepArg = prepareClientSessionMock.mock.calls[0][0] as { ChannelCandidates?: unknown };
4157
+ expect(prepArg.ChannelCandidates).toBeUndefined();
4158
+ expect(result.ClientPolicyJson).toBeUndefined();
4159
+ });
4160
+
4161
+ it('persists the in-scope channel keys on the session config, preserving what was stamped at start', async () => {
4162
+ const { created } = setupStart({ ClientPolicy: POLICY });
4163
+ await start(JSON.stringify([CANDIDATE]));
4164
+ const persisted = JSON.parse(created.Config_ as string) as { channels?: string[]; targetAgentID: string };
4165
+ expect(persisted.channels).toEqual(['Whiteboard']);
4166
+ expect(persisted.targetAgentID).toBe('target-1');
4167
+ });
4168
+
4169
+ it('carries the config stamped at session start (the media-kit override) through the run-id rewrite', async () => {
4170
+ const { created } = setupStart({ CoAgentRunID: 'run-1' });
4171
+ const resolver = makeResolver();
4172
+ // The create call receives the initial config; emulate the stored value the rewrite reads back.
4173
+ createSessionMock.mockImplementation(async (arg: { config: string }) => {
4174
+ created.Config_ = arg.config;
4175
+ return created;
4176
+ });
4177
+ await resolver.StartRealtimeClientSession(
4178
+ 'target-1', makeCtx(),
4179
+ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
4180
+ '11111111-1111-4111-8111-111111111111',
4181
+ );
4182
+ const persisted = JSON.parse(created.Config_ as string) as { mediaCollectionID?: string; coAgentRunID?: string };
4183
+ expect(persisted.mediaCollectionID).toBe('11111111-1111-4111-8111-111111111111');
4184
+ expect(persisted.coAgentRunID).toBe('run-1');
4185
+ });
4186
+
4187
+ it('prunes the server plugins of channels the scope excluded', async () => {
4188
+ setupStart({ ClientPolicy: POLICY });
4189
+ await start(JSON.stringify([CANDIDATE]));
4190
+ expect(pruneSessionChannelsMock).toHaveBeenCalledWith('session-ch', ['RemoteBrowser']);
4191
+ });
4192
+
4193
+ it('does not touch server plugins when nothing was excluded', async () => {
4194
+ setupStart({ ClientPolicy: { Version: 1, Channels: POLICY.Channels } });
4195
+ await start(JSON.stringify([CANDIDATE]));
4196
+ expect(pruneSessionChannelsMock).not.toHaveBeenCalled();
4197
+ });
4198
+
4199
+ it('tolerantly drops malformed, oversized and flooded candidate payloads (the session still starts)', async () => {
4200
+ for (const payload of ['{not json', JSON.stringify({ not: 'an array' }), JSON.stringify(Array.from({ length: 33 }, (_, i) => ({ ...CANDIDATE, Key: `C${i}` }))), 'x'.repeat(300_000)]) {
4201
+ setupStart();
4202
+ await start(payload);
4203
+ const prepArg = prepareClientSessionMock.mock.calls.at(-1)?.[0] as { ChannelCandidates?: unknown };
4204
+ expect(prepArg.ChannelCandidates).toBeUndefined();
4205
+ prepareClientSessionMock.mockClear();
4206
+ }
4207
+ });
4208
+
4209
+ it('skips an invalid candidate but keeps the valid ones', async () => {
4210
+ setupStart();
4211
+ await start(JSON.stringify([CANDIDATE, { Key: 'Broken', DefaultAvailability: 'sometimes' }]));
4212
+ const prepArg = prepareClientSessionMock.mock.calls[0][0] as { ChannelCandidates?: Array<{ Key: string }> };
4213
+ expect(prepArg.ChannelCandidates?.map((c) => c.Key)).toEqual(['Whiteboard']);
4214
+ });
4215
+ });
4216
+
4217
+ describe('RealtimeClientSessionResolver — save endpoints honor the session\'s resolved channel scope', () => {
4218
+ const CHANNEL_ROW = { ID: 'channel-wb', IsActive: true };
4219
+
4220
+ function provider(session: FakeSession) {
4221
+ engineChannelsMock.mockReturnValue([{ Name: 'Whiteboard', ...CHANNEL_ROW }]);
4222
+ const newRow = makeSessionEntity({ ID: 'sc-new' });
4223
+ return {
4224
+ newRow,
4225
+ provider: {
4226
+ GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Agent Session Channels' ? newRow : session)),
4227
+ RunView: vi.fn(async () => ({ Success: true, Results: [] })),
4228
+ },
4229
+ };
4230
+ }
4231
+
4232
+ it('refuses to save state for a channel the session\'s scope excluded', async () => {
4233
+ const session = makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 't', channels: ['Media'] }) });
4234
+ const p = provider(session);
4235
+ currentProvider = p.provider;
4236
+ const ok = await makeResolver().SaveSessionChannelState('session-1', 'Whiteboard', '{}', makeCtx());
4237
+ expect(ok).toBe(false);
4238
+ expect(p.newRow.Save).not.toHaveBeenCalled();
4239
+ });
4240
+
4241
+ it('saves state for an in-scope channel (case-insensitively)', async () => {
4242
+ const session = makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 't', channels: ['whiteboard'] }) });
4243
+ const p = provider(session);
4244
+ currentProvider = p.provider;
4245
+ const ok = await makeResolver().SaveSessionChannelState('session-1', 'Whiteboard', '{}', makeCtx());
4246
+ expect(ok).toBe(true);
4247
+ });
4248
+
4249
+ it('an unscoped session (no persisted channels) allows any channel, as before', async () => {
4250
+ const session = makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 't' }) });
4251
+ const p = provider(session);
4252
+ currentProvider = p.provider;
4253
+ const ok = await makeResolver().SaveSessionChannelState('session-1', 'Whiteboard', '{}', makeCtx());
4254
+ expect(ok).toBe(true);
4255
+ });
4256
+
4257
+ it('refuses to save an artifact for an out-of-scope channel with a structured failure', async () => {
4258
+ const session = makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 't', channels: ['Media'] }) });
4259
+ const p = provider(session);
4260
+ currentProvider = p.provider;
4261
+ const result = await makeResolver().SaveSessionChannelArtifact('session-1', 'Whiteboard', 'Board', '{}', makeCtx());
4262
+ expect(result.Success).toBe(false);
4263
+ expect(result.ErrorMessage).toContain('not in session');
3640
4264
  });
3641
4265
  });