@memberjunction/server 6.2.0-edge.2 → 6.2.0-edge.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
- package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
- package/dist/agentSessions/BridgeHostInstance.js +30 -0
- package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
- package/dist/agentSessions/HostInstance.d.ts +75 -11
- package/dist/agentSessions/HostInstance.d.ts.map +1 -1
- package/dist/agentSessions/HostInstance.js +99 -28
- package/dist/agentSessions/HostInstance.js.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
- package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +173 -25
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +452 -54
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +43 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +99 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +2 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +10 -3
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +3 -10
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +6 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/newUsers.d.ts +10 -0
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +80 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/config.d.ts +4567 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +246 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +25 -34
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +1073 -10
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +9188 -3649
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +62 -6
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +179 -35
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +37 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -159
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
- package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
- package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
- package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
- package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
- package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +138 -0
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +98 -0
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
- package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
- package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +17 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -0
- package/dist/realtimeSessions/index.js +17 -0
- package/dist/realtimeSessions/index.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +86 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionAccess.js +90 -0
- package/dist/realtimeSessions/sessionAccess.js.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
- package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
- package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
- package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
- package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
- package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
- package/dist/realtimeSessions/verificationCore.d.ts +243 -0
- package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationCore.js +356 -0
- package/dist/realtimeSessions/verificationCore.js.map +1 -0
- package/dist/realtimeSessions/verificationPages.d.ts +66 -0
- package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPages.js +110 -0
- package/dist/realtimeSessions/verificationPages.js.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
- package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationPolicy.js +249 -0
- package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
- package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
- package/dist/realtimeSessions/verificationWorkflow.js +435 -0
- package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +35 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +92 -166
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
- package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
- package/dist/resolvers/CacheStatsResolver.js +113 -2
- package/dist/resolvers/CacheStatsResolver.js.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
- package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
- package/dist/resolvers/ClientToolRequestResolver.js +28 -10
- package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +12 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +83 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +330 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +9 -4
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +88 -3
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +352 -12
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +156 -108
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +395 -293
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
- package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
- package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +68 -11
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
- package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
- package/dist/resolvers/UserAvatarResolver.js +124 -0
- package/dist/resolvers/UserAvatarResolver.js.map +1 -0
- package/dist/resolvers/UserResolver.d.ts.map +1 -1
- package/dist/resolvers/UserResolver.js +2 -17
- package/dist/resolvers/UserResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
- package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
- package/dist/resolvers/agentVisionConsentAudit.js +63 -0
- package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
- package/dist/resolvers/avatarInputValidation.d.ts +40 -0
- package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
- package/dist/resolvers/avatarInputValidation.js +97 -0
- package/dist/resolvers/avatarInputValidation.js.map +1 -0
- package/dist/resolvers/currentUserRoles.d.ts +29 -0
- package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
- package/dist/resolvers/currentUserRoles.js +30 -0
- package/dist/resolvers/currentUserRoles.js.map +1 -1
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +18 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +439 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +4 -3
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +13 -7
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/sharedCache.d.ts +105 -0
- package/dist/sharedCache.d.ts.map +1 -0
- package/dist/sharedCache.js +276 -0
- package/dist/sharedCache.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +97 -94
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/BridgeHostInstance.test.ts +232 -0
- package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +285 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +250 -0
- package/src/__tests__/MeetingResolver.test.ts +448 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +651 -4
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
- package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
- package/src/__tests__/RealtimeRelay.test.ts +876 -0
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +169 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +4 -4
- package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
- package/src/__tests__/SessionJanitor.test.ts +845 -18
- package/src/__tests__/SessionManager.test.ts +213 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +41 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +50 -3
- package/src/__tests__/UserAvatarResolver.test.ts +255 -0
- package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
- package/src/__tests__/avatarInputValidation.test.ts +160 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLinkService.consume.test.ts +6 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/newUsers.template.test.ts +151 -0
- package/src/__tests__/newUsers.test.ts +74 -5
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
- package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
- package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
- package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
- package/src/__tests__/realtimeSessions/helpers.ts +4 -0
- package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
- package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
- package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
- package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
- package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
- package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
- package/src/__tests__/resolverBase.fls.test.ts +87 -4
- package/src/__tests__/serverExtensionReservedRoots.test.ts +2 -0
- package/src/__tests__/sharedCache.test.ts +310 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/__tests__/widgetGuestElevation.test.ts +44 -0
- package/src/agentSessions/BridgeHostInstance.ts +32 -0
- package/src/agentSessions/HostInstance.ts +124 -28
- package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
- package/src/agentSessions/SessionJanitor.ts +543 -57
- package/src/agentSessions/SessionManager.ts +125 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +10 -3
- package/src/auth/actionAuthorization.ts +116 -0
- package/src/auth/index.ts +3 -8
- package/src/auth/magicLink/MagicLinkService.ts +6 -3
- package/src/auth/newUsers.ts +99 -11
- package/src/config.ts +255 -3
- package/src/context.ts +26 -34
- package/src/generated/graphql-schemas/__mj.ts +22701 -18838
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/ResolverBase.ts +210 -36
- package/src/generic/RunViewResolver.ts +37 -2
- package/src/index.ts +236 -170
- package/src/isolatedProvider.ts +22 -0
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
- package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
- package/src/realtimeSessions/README.md +179 -0
- package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +167 -0
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +108 -0
- package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
- package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
- package/src/realtimeSessions/index.ts +16 -0
- package/src/realtimeSessions/sessionAccess.ts +141 -0
- package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
- package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
- package/src/realtimeSessions/verificationCore.ts +502 -0
- package/src/realtimeSessions/verificationPages.ts +149 -0
- package/src/realtimeSessions/verificationPolicy.ts +321 -0
- package/src/realtimeSessions/verificationWorkflow.ts +702 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +105 -187
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +312 -0
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/RealtimeBridgeResolver.ts +385 -19
- package/src/resolvers/RealtimeClientSessionResolver.ts +503 -344
- package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
- package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +78 -11
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
- package/src/resolvers/UserAvatarResolver.ts +111 -0
- package/src/resolvers/UserResolver.ts +2 -16
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +556 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/sharedCache.ts +302 -0
- package/src/types.ts +12 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* and **executes** every tool call the browser relays back. The three mutations here cover the MVP:
|
|
12
12
|
*
|
|
13
13
|
* 1. {@link RealtimeClientSessionResolver.StartRealtimeClientSession} — authorize, create the
|
|
14
|
-
* session (storing the target agent id
|
|
15
|
-
* 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
|
|
16
|
-
*
|
|
14
|
+
* session (storing the target agent id on it), mint the ephemeral config.
|
|
15
|
+
* 2. {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} — execute a relayed tool call
|
|
16
|
+
* for the target agent stored on the session, re-authorized for the caller on every call.
|
|
17
17
|
* 3. {@link RealtimeClientSessionResolver.RelayRealtimeTranscript} — persist a transcript turn as a
|
|
18
18
|
* `Conversation Detail`.
|
|
19
19
|
*
|
|
@@ -21,12 +21,19 @@
|
|
|
21
21
|
* - **Start**: the caller must have `CanRun` on the *target* agent (the Realtime Co-Agent is an internal
|
|
22
22
|
* orchestration agent, so the meaningful gate is the agent doing the real work).
|
|
23
23
|
* - **Execute / Relay**: inbound ownership — the session's `UserID` must equal `contextUser.ID`.
|
|
24
|
+
* - **Session `Config`**: the owner can edit it through the generic entity API, so nothing in it is
|
|
25
|
+
* trusted. Execute re-checks `CanRun` on its target, direct actions never come from it, and its
|
|
26
|
+
* run ids are used only when {@link SessionRunIDVerifier} finds them to be records of the session.
|
|
27
|
+
* - **Scope-limited callers** (anonymous and resource-scoped magic-link sessions, including public
|
|
28
|
+
* web-widget guests): no direct actions. Their scope exists only as row-level filters, which an
|
|
29
|
+
* action can read around, and a scoped anonymous caller's relay runs as the system user. They
|
|
30
|
+
* reach the target agent only through `invoke-target-agent`.
|
|
24
31
|
*
|
|
25
32
|
* @module @memberjunction/server
|
|
26
33
|
*/
|
|
27
34
|
import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
|
|
28
35
|
import { AppContext, UserPayload } from '../types.js';
|
|
29
|
-
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView, BaseEntity } from '@memberjunction/core';
|
|
36
|
+
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogErrorEx, LogStatus, RunView, BaseEntity } from '@memberjunction/core';
|
|
30
37
|
import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
|
|
31
38
|
import {
|
|
32
39
|
MJAIAgentEntity,
|
|
@@ -36,9 +43,10 @@ import {
|
|
|
36
43
|
MJArtifactVersionEntity,
|
|
37
44
|
MJConversationDetailArtifactEntity,
|
|
38
45
|
MJConversationDetailEntity,
|
|
46
|
+
ConversationEngine,
|
|
39
47
|
} from '@memberjunction/core-entities';
|
|
40
48
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
41
|
-
import { AIAgentPermissionHelper, AIEngineBase } from '@memberjunction/ai-engine-base';
|
|
49
|
+
import { AIAgentPermissionHelper, AIEngineBase, HasRealtimeUsage, ParseRealtimeUsageRecord, type RealtimeUsageRecord } from '@memberjunction/ai-engine-base';
|
|
42
50
|
import {
|
|
43
51
|
RealtimeClientSessionService,
|
|
44
52
|
DelegatedRunArtifact,
|
|
@@ -48,6 +56,9 @@ import {
|
|
|
48
56
|
FindIgnoredRealtimeConfigKeys,
|
|
49
57
|
ParseRealtimeTypeConfiguration,
|
|
50
58
|
ResolveEffectiveRealtimeConfig,
|
|
59
|
+
ResolveRealtimeCoAgentID,
|
|
60
|
+
FilterAllowedAgentsByCanRun,
|
|
61
|
+
INVOKE_TARGET_AGENT_TOOL_NAME,
|
|
51
62
|
RealtimeAllowedAgent,
|
|
52
63
|
RealtimeDirectActionsConfig,
|
|
53
64
|
REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION,
|
|
@@ -56,13 +67,26 @@ import {
|
|
|
56
67
|
writeRealtimeRecordingSegment,
|
|
57
68
|
deleteRealtimeRecordingSegments,
|
|
58
69
|
} from '@memberjunction/ai-agents';
|
|
59
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
AgentExecutionProgressCallback,
|
|
72
|
+
AppContextSnapshot,
|
|
73
|
+
IsValidRealtimeToolDefinition,
|
|
74
|
+
NormalizeChannelKey,
|
|
75
|
+
ParseRealtimeChannelCandidates,
|
|
76
|
+
type RealtimeChannelCandidate,
|
|
77
|
+
type RealtimeSessionClientPolicy
|
|
78
|
+
} from '@memberjunction/ai-core-plus';
|
|
60
79
|
import { ChatMessage, RealtimeToolDefinition } from '@memberjunction/ai';
|
|
61
80
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
62
81
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
63
82
|
import { GetReadWriteProvider } from '../util.js';
|
|
64
83
|
import { SessionManager } from '../agentSessions/index.js';
|
|
65
|
-
import {
|
|
84
|
+
import { SessionRunIDVerifier } from '../agentSessions/SessionRunIDVerifier.js';
|
|
85
|
+
import { IsScopeLimitedPrincipal } from '../auth/scopeLimitedPrincipal.js';
|
|
86
|
+
import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser, ResolveRecordingStoreUser } from '../realtimeWidget/widgetGuestElevation.js';
|
|
87
|
+
import { RunWithTrustedSessionConfigWrites } from '@memberjunction/core-entities-server';
|
|
88
|
+
import { BuildMintConfigStamps, LoadInheritedVerifiedIdentity } from '../realtimeSessions/sessionConfigStamps.js';
|
|
89
|
+
import { ParseConfigObject } from '../realtimeSessions/verificationCore.js';
|
|
66
90
|
|
|
67
91
|
/**
|
|
68
92
|
* Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
|
|
@@ -72,30 +96,10 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../
|
|
|
72
96
|
const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
73
97
|
|
|
74
98
|
/**
|
|
75
|
-
* The
|
|
76
|
-
*
|
|
77
|
-
* the co-agent resolution chain (see {@link RealtimeClientSessionResolver.resolveCoAgentID}).
|
|
78
|
-
* Deployments can override it per agent (`AIAgent.DefaultCoAgentID`), per agent type (an
|
|
79
|
-
* `AIAgentCoAgent` row with `TargetAgentTypeID` + `IsDefault`), or per call (the `coAgentId`
|
|
80
|
-
* mutation argument) without
|
|
81
|
-
* touching this seed.
|
|
82
|
-
*/
|
|
83
|
-
const REALTIME_CO_AGENT_NAME = 'Realtime Co-Agent';
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* DEPRECATED legacy seed name of {@link REALTIME_CO_AGENT_NAME}, from before the agent's rename
|
|
87
|
-
* from "Voice Co-Agent" to "Realtime Co-Agent". Deployments that have not re-synced the agent seed
|
|
88
|
-
* still carry this name, so {@link RealtimeClientSessionResolver.resolveGlobalCoAgentID} falls
|
|
89
|
-
* back to it (with a deprecation log) when no agent named {@link REALTIME_CO_AGENT_NAME} exists.
|
|
99
|
+
* The longest `usageDetailsJson` a usage relay may carry. Every field of both detail blocks fits in a few hundred
|
|
100
|
+
* characters, so anything longer is not a relay the browser runtime sends.
|
|
90
101
|
*/
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The seeded name of the Realtime agent TYPE. Every co-agent candidate (explicit or metadata
|
|
95
|
-
* default) must be an Active agent of this type — a co-agent drives a streaming full-duplex
|
|
96
|
-
* realtime session, which only `RealtimeAgentType`-driven agents support.
|
|
97
|
-
*/
|
|
98
|
-
const REALTIME_AGENT_TYPE_NAME = 'Realtime';
|
|
102
|
+
const MAX_USAGE_DETAILS_JSON_LENGTH = 4096;
|
|
99
103
|
|
|
100
104
|
/** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
101
105
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
@@ -124,7 +128,8 @@ const WHITEBOARD_ARTIFACT_TYPE_NAME = 'Whiteboard';
|
|
|
124
128
|
*/
|
|
125
129
|
const MAX_CLIENT_TOOLS = 64;
|
|
126
130
|
/** Maximum accepted size (chars) of the serialized client tool declarations. Raised in step with
|
|
127
|
-
* {@link MAX_CLIENT_TOOLS} — multi-channel tool sets with verbose descriptions + JSON schemas run large.
|
|
131
|
+
* {@link MAX_CLIENT_TOOLS} — multi-channel tool sets with verbose descriptions + JSON schemas run large.
|
|
132
|
+
* Also the largest serialized app-context snapshot stored on the session config. */
|
|
128
133
|
const MAX_CLIENT_TOOLS_JSON_CHARS = 256_000;
|
|
129
134
|
/** Maximum accepted size (chars) of a persisted channel state blob. */
|
|
130
135
|
const MAX_CHANNEL_STATE_CHARS = 2_000_000;
|
|
@@ -142,9 +147,9 @@ const MAX_CONVERSATION_HISTORY_TURNS = 30;
|
|
|
142
147
|
const MAX_CONVERSATION_HISTORY_CHARS = 8_000;
|
|
143
148
|
|
|
144
149
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
150
|
+
* Shape persisted in `AIAgentSession.Config_` for a client-direct voice session. The server writes
|
|
151
|
+
* it, but the session owner can also edit it through the generic entity API, so relays re-check
|
|
152
|
+
* what they read from it (see the authorization model above).
|
|
148
153
|
*/
|
|
149
154
|
interface RealtimeSessionConfig {
|
|
150
155
|
/** The top-level target agent the co-agent voices on behalf of. */
|
|
@@ -181,6 +186,12 @@ interface RealtimeSessionConfig {
|
|
|
181
186
|
* for observability / continuity; absent when the session carries no app context.
|
|
182
187
|
*/
|
|
183
188
|
applicationID?: string;
|
|
189
|
+
/**
|
|
190
|
+
* The app-context snapshot the browser sent at session start. Passed into each delegated
|
|
191
|
+
* target-agent run so its prompt sees the surface context and tools. Absent when the start carried
|
|
192
|
+
* none, or when its JSON is longer than {@link MAX_CLIENT_TOOLS_JSON_CHARS}.
|
|
193
|
+
*/
|
|
194
|
+
appContext?: AppContextSnapshot;
|
|
184
195
|
/**
|
|
185
196
|
* The session's effective allowed delegation targets (union from the config cascade, incl. the app's
|
|
186
197
|
* `RelevantAgents`). Persisted at start so each relayed `invoke-target-agent` call can validate a
|
|
@@ -199,6 +210,24 @@ interface RealtimeSessionConfig {
|
|
|
199
210
|
* agent default kit is used (the common case).
|
|
200
211
|
*/
|
|
201
212
|
mediaCollectionID?: string;
|
|
213
|
+
/**
|
|
214
|
+
* The fork (`MJ: Conversation Branches.ID`) the session was started on, fixed at start; absent means
|
|
215
|
+
* Main. Every transcript, direct-action and anchor row the session writes carries it.
|
|
216
|
+
*/
|
|
217
|
+
conversationBranchID?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Server-authoritative identity-verification state (policy snapshot taken at mint, pending/verified
|
|
220
|
+
* state written by `RealtimeSessionVerificationService`). Protected from owner writes by
|
|
221
|
+
* `MJAIAgentSessionEntityServer`; opaque to this resolver beyond preserving it across config rewrites.
|
|
222
|
+
*/
|
|
223
|
+
identityVerification?: Record<string, unknown>;
|
|
224
|
+
/**
|
|
225
|
+
* The keys of the interactive channels the session's resolved scope put IN the session (registry
|
|
226
|
+
* kill switch, agent and app configuration applied). Persisted at start so a later server-side
|
|
227
|
+
* call naming a channel — a state or artifact save — can check it is actually in scope. Absent
|
|
228
|
+
* (an unscoped session: a client that reported no channel candidates) ⇒ no channel check.
|
|
229
|
+
*/
|
|
230
|
+
channels?: string[];
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
/**
|
|
@@ -223,14 +252,32 @@ export class StartRealtimeClientSessionResult {
|
|
|
223
252
|
@Field(() => String)
|
|
224
253
|
Model: string;
|
|
225
254
|
|
|
226
|
-
/**
|
|
255
|
+
/**
|
|
256
|
+
* The short-lived client secret the browser presents to the provider to authenticate. Empty on a relay session
|
|
257
|
+
* ({@link StartRealtimeClientSessionResult.Transport} `relay`), whose {@link StartRealtimeClientSessionResult.RelayUrl}
|
|
258
|
+
* carries the relay's ticket instead.
|
|
259
|
+
*/
|
|
227
260
|
@Field(() => String)
|
|
228
261
|
EphemeralToken: string;
|
|
229
262
|
|
|
230
|
-
/** ISO-8601 timestamp at which {@link StartRealtimeClientSessionResult.EphemeralToken} expires. */
|
|
263
|
+
/** ISO-8601 timestamp at which {@link StartRealtimeClientSessionResult.EphemeralToken} (or a relay session's ticket) expires. */
|
|
231
264
|
@Field(() => String)
|
|
232
265
|
ExpiresAt: string;
|
|
233
266
|
|
|
267
|
+
/**
|
|
268
|
+
* How the browser reaches the provider: `direct` (with the ephemeral token) or `relay` (through MJAPI's realtime
|
|
269
|
+
* relay at {@link StartRealtimeClientSessionResult.RelayUrl}). Null means direct.
|
|
270
|
+
*/
|
|
271
|
+
@Field(() => String, { nullable: true })
|
|
272
|
+
Transport?: string;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Where a relay session's browser connects (MJAPI's realtime relay). Null on a direct session. The URL carries the
|
|
276
|
+
* session's ticket, so it is a credential and is never logged.
|
|
277
|
+
*/
|
|
278
|
+
@Field(() => String, { nullable: true })
|
|
279
|
+
RelayUrl?: string;
|
|
280
|
+
|
|
234
281
|
/** JSON string of the provider-native session config the browser applies verbatim. */
|
|
235
282
|
@Field(() => String)
|
|
236
283
|
SessionConfigJson: string;
|
|
@@ -287,6 +334,25 @@ export class StartRealtimeClientSessionResult {
|
|
|
287
334
|
*/
|
|
288
335
|
@Field(() => String, { nullable: true })
|
|
289
336
|
EffectiveConfigJson?: string;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* JSON of the server's resolved channel policy for this session (`RealtimeSessionClientPolicy`):
|
|
340
|
+
* which of the channel candidates the browser reported are in the session (registry kill switch,
|
|
341
|
+
* agent and app `channels` configuration applied), with each one's resolved display and config,
|
|
342
|
+
* plus the app/static client-tool tiers. Null when the caller reported no channel candidates (a
|
|
343
|
+
* client that predates channel scoping) — the browser then resolves the scope locally.
|
|
344
|
+
*/
|
|
345
|
+
@Field(() => String, { nullable: true })
|
|
346
|
+
ClientPolicyJson?: string;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* JSON of the session's live-avatar status (`RealtimeAvatarStatus` from `@memberjunction/ai`): whether the voiced
|
|
350
|
+
* agent asked for an avatar, whether the model renders it, and why not. The browser reads it after connecting and
|
|
351
|
+
* tells the user once when the call is audio only. Null when the agent asked for no avatar (its video setting is
|
|
352
|
+
* off).
|
|
353
|
+
*/
|
|
354
|
+
@Field(() => String, { nullable: true })
|
|
355
|
+
AvatarStatusJson?: string;
|
|
290
356
|
}
|
|
291
357
|
|
|
292
358
|
/**
|
|
@@ -380,6 +446,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
380
446
|
// state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
|
|
381
447
|
private readonly clientSessionService = new RealtimeClientSessionService();
|
|
382
448
|
private readonly sessionManager = new SessionManager(this.clientSessionService);
|
|
449
|
+
private readonly runIDVerifier = new SessionRunIDVerifier();
|
|
383
450
|
|
|
384
451
|
/**
|
|
385
452
|
* The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
|
|
@@ -414,9 +481,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
414
481
|
* session is created.
|
|
415
482
|
* 2. Resolve the co-agent id via the metadata-driven resolution chain (runtime `coAgentId` →
|
|
416
483
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
417
|
-
* see {@link
|
|
484
|
+
* see {@link ResolveRealtimeCoAgentID} for the full contract.
|
|
418
485
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
419
|
-
* config
|
|
486
|
+
* config — the target later relays use, re-authorized for the caller on each relay.
|
|
420
487
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
421
488
|
* On failure the just-created session is closed so no half-open session leaks.
|
|
422
489
|
*
|
|
@@ -442,6 +509,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
442
509
|
* `TypeConfiguration` ← this). **Authorization-gated**: requires the
|
|
443
510
|
* `Realtime: Advanced Session Controls` authorization — unauthorized callers receive a
|
|
444
511
|
* structured rejection (never a silent ignore). Must be a JSON object.
|
|
512
|
+
* @param showsAgentVideo `false` from an app that shows no agent video (no channel of it shows it: the embeddable
|
|
513
|
+
* widgets, the mobile app): the session asks the model for no avatar, and `AvatarStatusJson` says `host`. Absent or
|
|
514
|
+
* `true`: the app may show it. It only ever turns video off, so it needs no authorization.
|
|
515
|
+
* @param conversationBranchId Optional fork (`MJ: Conversation Branches.ID`) the session starts on;
|
|
516
|
+
* omitted is Main. It must be a fork of `conversationId`, else the start fails and no session is
|
|
517
|
+
* created. The session's history is read from that fork's path and its transcript is written there.
|
|
445
518
|
*
|
|
446
519
|
* @returns The ephemeral config + session linkage the browser needs to open its socket.
|
|
447
520
|
*/
|
|
@@ -460,10 +533,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
460
533
|
@Arg('mediaCollectionId', () => String, { nullable: true }) mediaCollectionId?: string,
|
|
461
534
|
@Arg('applicationId', () => String, { nullable: true }) applicationId?: string,
|
|
462
535
|
@Arg('appContextJson', () => String, { nullable: true }) appContextJson?: string,
|
|
536
|
+
@Arg('channelCandidatesJson', () => String, { nullable: true }) channelCandidatesJson?: string,
|
|
537
|
+
@Arg('showsAgentVideo', () => Boolean, { nullable: true }) showsAgentVideo?: boolean,
|
|
538
|
+
@Arg('conversationBranchId', () => String, { nullable: true }) conversationBranchId?: string,
|
|
463
539
|
): Promise<StartRealtimeClientSessionResult> {
|
|
464
540
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
465
541
|
|
|
466
|
-
const coAgentID = await
|
|
542
|
+
const coAgentID = await ResolveRealtimeCoAgentID(targetAgentId, coAgentId, contextUser, provider);
|
|
467
543
|
// PAIRING CONSTRAINTS: a co-agent with pairing rows is restricted to that target list
|
|
468
544
|
// (with the IsDefault row standing in when no runtime target was supplied); zero rows =
|
|
469
545
|
// universal, today's behavior untouched. Resolves the AUTHORITATIVE target id.
|
|
@@ -497,22 +573,32 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
497
573
|
);
|
|
498
574
|
}
|
|
499
575
|
}
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
576
|
+
const sessionBranchId = await this.resolveSessionBranch(conversationId, conversationBranchId, contextUser, provider);
|
|
577
|
+
if (sessionBranchId) {
|
|
578
|
+
config.conversationBranchID = sessionBranchId;
|
|
579
|
+
}
|
|
580
|
+
// The initial Config carries `maxSessionDeadlineIso` for capped sessions — a server-authoritative key
|
|
581
|
+
// that `MJAIAgentSessionEntityServer` refuses to let the session OWNER write, so this write (and only
|
|
582
|
+
// this one, built entirely from server-decided values) runs in the trusted scope.
|
|
583
|
+
const session = await RunWithTrustedSessionConfigWrites(() =>
|
|
584
|
+
this.sessionManager.CreateSession(
|
|
585
|
+
{
|
|
586
|
+
agentID: coAgentID,
|
|
587
|
+
userID: contextUser.ID,
|
|
588
|
+
conversationID: conversationId,
|
|
589
|
+
lastSessionID: lastSessionId,
|
|
590
|
+
config: JSON.stringify(config),
|
|
591
|
+
},
|
|
592
|
+
contextUser,
|
|
593
|
+
provider,
|
|
594
|
+
),
|
|
510
595
|
);
|
|
511
596
|
|
|
512
597
|
// Best-effort: stamp recording-start metadata when the browser captured (with consent) at start.
|
|
513
598
|
await this.stampRecordingStart(session, recordingConsent, recordingStartedAt);
|
|
514
599
|
|
|
515
600
|
const clientTools = this.parseClientTools(clientToolsJson);
|
|
601
|
+
const channelCandidates = this.parseChannelCandidates(channelCandidatesJson);
|
|
516
602
|
// Best-effort model-context hydration: the PRIOR session chain's transcript (ownership-
|
|
517
603
|
// checked, capped) is framed into the system prompt so the model REMEMBERS the last leg.
|
|
518
604
|
// Strictly tolerant — any problem yields no hydration, never a failed start.
|
|
@@ -522,24 +608,103 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
522
608
|
// framed as the resumed transcript, and injecting the same turns twice puts two accounts of
|
|
523
609
|
// the same exchange in one prompt.
|
|
524
610
|
const conversationMessages = await this.loadConversationHistory(
|
|
525
|
-
conversationId, prior?.LegIDs ?? [], contextUser, provider,
|
|
611
|
+
conversationId, prior?.LegIDs ?? [], contextUser, provider, sessionBranchId,
|
|
526
612
|
);
|
|
527
613
|
const result = await this.prepareClientSessionOrClose(
|
|
528
614
|
session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, prior?.Text,
|
|
529
615
|
configOverridesJson, maxSessionSeconds, applicationId, this.parseAppContext(appContextJson), conversationMessages,
|
|
616
|
+
channelCandidates, showsAgentVideo,
|
|
530
617
|
);
|
|
618
|
+
await this.stampVerificationPolicy(session, result.EffectiveConfigJson, lastSessionId, contextUser, provider);
|
|
531
619
|
// Best-effort restore of the PRIOR session's persisted channel states (e.g. the whiteboard
|
|
532
620
|
// board). Strictly tolerant — any problem yields a null field, never a failed start.
|
|
533
621
|
result.PriorChannelStatesJson = (await this.loadPriorChannelStatesJson(lastSessionId, contextUser, provider)) ?? undefined;
|
|
534
622
|
return result;
|
|
535
623
|
}
|
|
536
624
|
|
|
625
|
+
/**
|
|
626
|
+
* The fork a session starts on: null (Main) when none is named; else the named fork, which must
|
|
627
|
+
* belong to the session's conversation. Returns the fork's stored id.
|
|
628
|
+
* @throws when a fork is named without a conversation, or is not a fork of it, or the fork rows cannot be read
|
|
629
|
+
*/
|
|
630
|
+
private async resolveSessionBranch(
|
|
631
|
+
conversationId: string | undefined,
|
|
632
|
+
branchId: string | undefined,
|
|
633
|
+
contextUser: UserInfo,
|
|
634
|
+
provider: IMetadataProvider,
|
|
635
|
+
): Promise<string | null> {
|
|
636
|
+
const named = branchId?.trim();
|
|
637
|
+
if (!named) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
if (!conversationId) {
|
|
641
|
+
throw new Error(`StartRealtimeClientSession: fork ${named} needs a conversation`);
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
const scope = await ConversationEngine.LoadScope(conversationId, named, contextUser, provider);
|
|
645
|
+
return scope.BranchID;
|
|
646
|
+
} catch (error) {
|
|
647
|
+
throw new Error(`StartRealtimeClientSession: fork ${named} is not a readable fork of conversation ${conversationId}: ${(error as Error).message}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Stamps the identity-verification policy snapshot, the configured unverified duration cap and any
|
|
653
|
+
* verified identity inherited from the resumed session onto the new session's `Config`.
|
|
654
|
+
*
|
|
655
|
+
* The effective config (config cascade) is only known after prepare, so this is a second, narrowly
|
|
656
|
+
* scoped write: it runs only when there is something to stamp. It **fails closed** — a cap that could
|
|
657
|
+
* not be applied would leave the session uncapped, so on a save failure the session is closed and the
|
|
658
|
+
* start fails rather than proceeding without its limits.
|
|
659
|
+
*/
|
|
660
|
+
private async stampVerificationPolicy(
|
|
661
|
+
session: MJAIAgentSessionEntity,
|
|
662
|
+
effectiveConfigJson: string | undefined,
|
|
663
|
+
lastSessionId: string | undefined,
|
|
664
|
+
contextUser: UserInfo,
|
|
665
|
+
provider: IMetadataProvider,
|
|
666
|
+
): Promise<void> {
|
|
667
|
+
const plan = BuildMintConfigStamps({
|
|
668
|
+
EffectiveRealtime: this.readEffectiveRealtime(effectiveConfigJson),
|
|
669
|
+
ExistingConfigRaw: session.Config_,
|
|
670
|
+
InheritedVerified: await LoadInheritedVerifiedIdentity(lastSessionId, contextUser, provider),
|
|
671
|
+
NowMs: Date.now(),
|
|
672
|
+
});
|
|
673
|
+
if (!plan) {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
session.Config_ = plan.NextConfigRaw;
|
|
677
|
+
const saved = await RunWithTrustedSessionConfigWrites(() => session.Save());
|
|
678
|
+
if (!saved) {
|
|
679
|
+
LogError(
|
|
680
|
+
`RealtimeClientSessionResolver.stampVerificationPolicy save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
681
|
+
);
|
|
682
|
+
await this.sessionManager.CloseSession(session.ID, contextUser, provider, 'Error');
|
|
683
|
+
throw new Error('Failed to apply the session limits; the realtime session was not started.');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** The `realtime` section of the resolved effective config JSON, or undefined when absent/unparseable. */
|
|
688
|
+
private readEffectiveRealtime(effectiveConfigJson: string | undefined): unknown {
|
|
689
|
+
if (!effectiveConfigJson) {
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
try {
|
|
693
|
+
return (JSON.parse(effectiveConfigJson) as { realtime?: unknown }).realtime;
|
|
694
|
+
} catch (error) {
|
|
695
|
+
LogError(`RealtimeClientSessionResolver: effective config JSON was unparseable while stamping verification policy: ${(error as Error).message}`);
|
|
696
|
+
return undefined;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
537
700
|
/**
|
|
538
701
|
* Execute a single tool call the browser relayed from its provider socket and return the
|
|
539
702
|
* serialized result for the browser to relay back to the model.
|
|
540
703
|
*
|
|
541
|
-
* Ownership-gated
|
|
542
|
-
*
|
|
704
|
+
* Ownership-gated. The target agent id is read from the session config and `CanRun` on it is
|
|
705
|
+
* checked for the caller on every call, because the owner can edit the config. A direct-action
|
|
706
|
+
* call from a scope-limited caller is refused before any elevation. Heartbeats the session on
|
|
707
|
+
* success.
|
|
543
708
|
*
|
|
544
709
|
* @returns The serialized tool result JSON.
|
|
545
710
|
*/
|
|
@@ -555,13 +720,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
555
720
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
556
721
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
557
722
|
const config = this.readSessionConfig(session);
|
|
723
|
+
await this.assertCanRunTarget(config.targetAgentID, contextUser, provider);
|
|
724
|
+
if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME && IsScopeLimitedPrincipal(contextUser)) {
|
|
725
|
+
return this.refuseDirectAction(agentSessionId, toolName);
|
|
726
|
+
}
|
|
558
727
|
|
|
559
728
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
|
|
560
729
|
// run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
|
|
561
730
|
// (the caller's role deliberately holds no grants on the run entities). The lead
|
|
562
|
-
// targetAgentID
|
|
563
|
-
//
|
|
731
|
+
// targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
|
|
732
|
+
// just below, so elevation never widens agent authority.
|
|
564
733
|
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
734
|
+
const runIDs = await this.runIDVerifier.Verify(
|
|
735
|
+
session.ID,
|
|
736
|
+
{ CoAgentRunID: config.coAgentRunID, PendingFeedbackRunID: config.pendingFeedbackRunID },
|
|
737
|
+
runUser,
|
|
738
|
+
provider,
|
|
739
|
+
);
|
|
565
740
|
if (runUser !== contextUser) {
|
|
566
741
|
LogStatus(
|
|
567
742
|
`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
|
|
@@ -578,17 +753,24 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
578
753
|
TargetAgentID: config.targetAgentID,
|
|
579
754
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
580
755
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
581
|
-
AllowedAgents: await
|
|
582
|
-
DirectActions:
|
|
756
|
+
AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
757
|
+
// No DirectActions: the service takes them from the session's start-time projection
|
|
758
|
+
// or the target agent's metadata, never from the owner-editable config.
|
|
583
759
|
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
584
760
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
585
761
|
AttributionUserID: contextUser.ID,
|
|
586
762
|
// Nest the delegated target-agent run under the co-agent observability run (when present).
|
|
587
|
-
ParentRunID:
|
|
763
|
+
ParentRunID: runIDs.CoAgentRunID,
|
|
764
|
+
// The client-tool channel is keyed on the browser session id, so the delegated run can call
|
|
765
|
+
// the surface's tools; the session-start app context gives its prompt the surface context.
|
|
766
|
+
BrowserSessionID: userPayload.sessionId,
|
|
767
|
+
// The app context is a snapshot the browser sent, so a run elevated to the system user
|
|
768
|
+
// does not get it in its prompt.
|
|
769
|
+
AppContext: runUser === contextUser ? config.appContext : undefined,
|
|
588
770
|
Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
|
|
589
771
|
OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
|
|
590
772
|
// Resume a previously-paused delegated run (if any) with the user's answer.
|
|
591
|
-
ResumeRunID:
|
|
773
|
+
ResumeRunID: runIDs.PendingFeedbackRunID,
|
|
592
774
|
},
|
|
593
775
|
runUser,
|
|
594
776
|
provider,
|
|
@@ -598,7 +780,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
598
780
|
`ExecuteRealtimeSessionTool: completed relayed tool '${toolName}' (callId: ${callId}) in ${durationMs}ms`,
|
|
599
781
|
);
|
|
600
782
|
|
|
601
|
-
if (toolName !==
|
|
783
|
+
if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME) {
|
|
602
784
|
await this.persistDirectActionTurn(
|
|
603
785
|
session,
|
|
604
786
|
callId,
|
|
@@ -655,6 +837,19 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
655
837
|
};
|
|
656
838
|
}
|
|
657
839
|
|
|
840
|
+
/**
|
|
841
|
+
* The tool result for a direct-action call from a scope-limited caller (see the authorization
|
|
842
|
+
* model above). It has the broker's result shape, so the model hears that the tool is not
|
|
843
|
+
* available and can ask the target agent through `invoke-target-agent` instead.
|
|
844
|
+
*/
|
|
845
|
+
private refuseDirectAction(agentSessionId: string, toolName: string): string {
|
|
846
|
+
LogStatus(`ExecuteRealtimeSessionTool: refused direct action '${toolName}' for a scope-limited caller on session ${agentSessionId}.`);
|
|
847
|
+
return JSON.stringify({
|
|
848
|
+
success: false,
|
|
849
|
+
output: `The tool '${toolName}' is not available in this session. Ask the agent to do it with ${INVOKE_TARGET_AGENT_TOOL_NAME}.`,
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
658
853
|
/**
|
|
659
854
|
* Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
|
|
660
855
|
* consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
|
|
@@ -670,17 +865,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
670
865
|
if (!previous && !pausedRunID) {
|
|
671
866
|
return;
|
|
672
867
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
coAgentRunStepID: config.coAgentRunStepID,
|
|
678
|
-
pendingFeedbackRunID: pausedRunID,
|
|
679
|
-
// Preserve the server-authoritative voice deadline across config rewrites.
|
|
680
|
-
maxSessionDeadlineIso: config.maxSessionDeadlineIso,
|
|
681
|
-
applicationID: config.applicationID,
|
|
682
|
-
allowedAgents: config.allowedAgents,
|
|
683
|
-
};
|
|
868
|
+
// Start from the RAW stored config so every key survives the rewrite — including the
|
|
869
|
+
// server-authoritative `maxSessionDeadlineIso` and `identityVerification` (dropping either would be
|
|
870
|
+
// refused by `MJAIAgentSessionEntityServer`, and silently losing a verification would be worse).
|
|
871
|
+
const next = { ...ParseConfigObject(session.Config_), targetAgentID: config.targetAgentID, pendingFeedbackRunID: pausedRunID };
|
|
684
872
|
session.Config_ = JSON.stringify(next);
|
|
685
873
|
if (!(await session.Save())) {
|
|
686
874
|
LogError(
|
|
@@ -725,14 +913,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
725
913
|
// conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
|
|
726
914
|
// The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
|
|
727
915
|
// Conversation Detail above deliberately stays on the caller.
|
|
728
|
-
const
|
|
916
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
917
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
729
918
|
if (promptRunID) {
|
|
730
919
|
await this.clientSessionService.AppendPromptRunMessage(
|
|
731
920
|
promptRunID,
|
|
732
921
|
this.mapTranscriptRoleToChatRole(role),
|
|
733
922
|
text,
|
|
734
923
|
replacesPrevious ?? false,
|
|
735
|
-
|
|
924
|
+
runUser,
|
|
736
925
|
provider,
|
|
737
926
|
);
|
|
738
927
|
}
|
|
@@ -791,11 +980,11 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
791
980
|
return { Success: false, ErrorMessage: 'Recording consent was not granted.' };
|
|
792
981
|
}
|
|
793
982
|
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
// session link, so nothing here depends on the caller's identity.
|
|
798
|
-
const runUser =
|
|
983
|
+
// ELEVATION (issue #3371, #5195): past the ownership + consent gates, the store is server-side
|
|
984
|
+
// plumbing over entities (MJ: AI Agents read, MJ: Files, the file-session link, the session's
|
|
985
|
+
// recording stamp) that neither a scoped-anonymous relay role nor the stock UI role holds.
|
|
986
|
+
// Attribution flows through the session link, so nothing here depends on the caller's identity.
|
|
987
|
+
const runUser = ResolveRecordingStoreUser(contextUser);
|
|
799
988
|
try {
|
|
800
989
|
const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
|
|
801
990
|
if (!(await agent.Load(session.AgentID))) {
|
|
@@ -846,11 +1035,14 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
846
1035
|
|
|
847
1036
|
/**
|
|
848
1037
|
* Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
|
|
849
|
-
* folder (`realtime-recordings/<sessionId>/seg-NNNN
|
|
850
|
-
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
1038
|
+
* folder (`realtime-recordings/<sessionId>/seg-NNNN.r<rate>.pcm` for PCM with a rate in its MIME type,
|
|
1039
|
+
* else the legacy `seg-NNNN.<ext>`) as a raw storage object. Durability insurance during a live call
|
|
1040
|
+
* so a browser/tab death loses at most the last window; the shards are deleted once the canonical
|
|
1041
|
+
* consolidated file lands via {@link UploadRealtimeRecording}, or by the session janitor once it has
|
|
1042
|
+
* recovered a recording from them when that upload never arrives. Ownership-
|
|
1043
|
+
* gated; consent was already established at session start. Refused (`false`) once the session has
|
|
1044
|
+
* its consolidated recording, since nothing would delete a shard written after that. Best-effort —
|
|
1045
|
+
* returns `false` (never throws) on any problem so a failed shard never disrupts the live call.
|
|
854
1046
|
*
|
|
855
1047
|
* @param agentSessionId The in-progress session (ownership-gated).
|
|
856
1048
|
* @param segmentIndex 0-based shard index within the session.
|
|
@@ -869,6 +1061,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
869
1061
|
try {
|
|
870
1062
|
const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
|
|
871
1063
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
1064
|
+
// The consolidated recording already landed and its upload deleted the shards; a shard
|
|
1065
|
+
// arriving now (one still on the wire at hang-up) would be an orphan nothing deletes.
|
|
1066
|
+
if (session.RecordingFileID) {
|
|
1067
|
+
LogStatus(`RealtimeClientSessionResolver.UploadRealtimeRecordingSegment: shard ${segmentIndex} for session ${agentSessionId} refused — the session already has its consolidated recording`);
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
872
1070
|
// Scoped-anonymous elevation (issue #3371) — same rationale as UploadRealtimeRecording.
|
|
873
1071
|
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
874
1072
|
const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
|
|
@@ -930,7 +1128,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
930
1128
|
provider,
|
|
931
1129
|
);
|
|
932
1130
|
|
|
933
|
-
const
|
|
1131
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
1132
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
934
1133
|
if (!promptRunID) {
|
|
935
1134
|
return false;
|
|
936
1135
|
}
|
|
@@ -939,7 +1138,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
939
1138
|
'assistant',
|
|
940
1139
|
this.formatToolTurn(toolName, argsJson, resultJson),
|
|
941
1140
|
false,
|
|
942
|
-
|
|
1141
|
+
runUser,
|
|
943
1142
|
provider,
|
|
944
1143
|
);
|
|
945
1144
|
}
|
|
@@ -1011,8 +1210,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1011
1210
|
* (observability creation was skipped), a failed load, or a failed save all log and return
|
|
1012
1211
|
* `false` — usage relay must never break a live call.
|
|
1013
1212
|
*
|
|
1213
|
+
* The optional details are the per-modality usage since the last relay: input and output
|
|
1214
|
+
* blocks shaped like Core's `RealtimeUsageModalityDetail`, avatar video seconds included. They
|
|
1215
|
+
* add into the usage record on the run's `ModelSpecificResponseDetails`, which prices the avatar
|
|
1216
|
+
* video at finalize. Only the fields a block defines, with values that are finite numbers of at
|
|
1217
|
+
* least 0, are kept; a relay that carries only details (avatar seconds, no tokens) is stored.
|
|
1218
|
+
*
|
|
1014
1219
|
* @param inputTokens Input-token DELTA to add (negative/non-finite values are clamped to 0).
|
|
1015
1220
|
* @param outputTokens Output-token DELTA to add (negative/non-finite values are clamped to 0).
|
|
1221
|
+
* @param usageDetailsJson The per-modality usage since the last relay, as JSON (`{ Input, Output }`).
|
|
1016
1222
|
* @returns `true` when the accumulated usage was persisted; `false` on any tolerated failure.
|
|
1017
1223
|
*/
|
|
1018
1224
|
@Mutation(() => Boolean)
|
|
@@ -1021,25 +1227,28 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1021
1227
|
@Arg('inputTokens', () => Int) inputTokens: number,
|
|
1022
1228
|
@Arg('outputTokens', () => Int) outputTokens: number,
|
|
1023
1229
|
@Ctx() { userPayload, providers }: AppContext,
|
|
1230
|
+
@Arg('usageDetailsJson', () => String, { nullable: true }) usageDetailsJson?: string,
|
|
1024
1231
|
): Promise<boolean> {
|
|
1025
1232
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1026
1233
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
1027
1234
|
|
|
1028
1235
|
const inputDelta = this.clampTokenDelta(inputTokens);
|
|
1029
1236
|
const outputDelta = this.clampTokenDelta(outputTokens);
|
|
1030
|
-
|
|
1237
|
+
const details = this.readUsageDetails(usageDetailsJson);
|
|
1238
|
+
if (inputDelta === 0 && outputDelta === 0 && !details) {
|
|
1031
1239
|
return true; // nothing to add — a no-op flush is a success, not a failure
|
|
1032
1240
|
}
|
|
1033
1241
|
|
|
1034
|
-
|
|
1242
|
+
// Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
|
|
1243
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
1244
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
1035
1245
|
if (!promptRunID) {
|
|
1036
1246
|
return false; // no observability prompt run for this session — logged in the helper
|
|
1037
1247
|
}
|
|
1038
1248
|
// Delegate to the service so usage writes share the per-run serialization with transcript-message
|
|
1039
1249
|
// appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
|
|
1040
|
-
// Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
|
|
1041
1250
|
return this.clientSessionService.AccumulatePromptRunUsage(
|
|
1042
|
-
promptRunID, inputDelta, outputDelta,
|
|
1251
|
+
promptRunID, inputDelta, outputDelta, runUser, provider, details ?? undefined,
|
|
1043
1252
|
);
|
|
1044
1253
|
}
|
|
1045
1254
|
|
|
@@ -1048,6 +1257,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1048
1257
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1049
1258
|
}
|
|
1050
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* Reads the relayed per-modality usage, or `null` when there is none worth storing: absent,
|
|
1262
|
+
* malformed, longer than a relay ever needs (a client cannot make the server parse an
|
|
1263
|
+
* arbitrarily large string), or holding no amount above 0.
|
|
1264
|
+
*/
|
|
1265
|
+
private readUsageDetails(usageDetailsJson: string | undefined): RealtimeUsageRecord | null {
|
|
1266
|
+
if (typeof usageDetailsJson !== 'string') {
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
if (usageDetailsJson.length > MAX_USAGE_DETAILS_JSON_LENGTH) {
|
|
1270
|
+
LogError(`RelayRealtimeUsage: usage details of ${usageDetailsJson.length} characters exceed ${MAX_USAGE_DETAILS_JSON_LENGTH} — details dropped.`);
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
const details = ParseRealtimeUsageRecord(usageDetailsJson);
|
|
1274
|
+
return HasRealtimeUsage(details) ? details : null;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1051
1277
|
/**
|
|
1052
1278
|
* Sanitizes client-supplied waveform peaks before they're persisted: drops anything that isn't a
|
|
1053
1279
|
* finite number, clamps each to `[0, 1]`, and caps the array length so a hostile client can't bloat
|
|
@@ -1067,24 +1293,40 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1067
1293
|
|
|
1068
1294
|
/**
|
|
1069
1295
|
* Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
|
|
1070
|
-
* throw-on-missing-target semantics
|
|
1071
|
-
*
|
|
1296
|
+
* throw-on-missing-target semantics, and keeps it only when it is the prompt run of the
|
|
1297
|
+
* session's co-agent run (the owner can edit the config). Usage relay is best-effort, so a
|
|
1298
|
+
* missing/malformed config, an absent id, or an id of another session just logs and returns `null`.
|
|
1299
|
+
*
|
|
1300
|
+
* @param runUser The user the prompt-run write runs as; the check runs as the same user.
|
|
1072
1301
|
*/
|
|
1073
|
-
private readPromptRunID(
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1302
|
+
private async readPromptRunID(
|
|
1303
|
+
session: MJAIAgentSessionEntity,
|
|
1304
|
+
runUser: UserInfo,
|
|
1305
|
+
provider: IMetadataProvider,
|
|
1306
|
+
): Promise<string | null> {
|
|
1307
|
+
const config = this.tryReadSessionConfig(session);
|
|
1308
|
+
if (!config) {
|
|
1309
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} has no parseable config — usage delta dropped.`);
|
|
1310
|
+
return null;
|
|
1311
|
+
}
|
|
1312
|
+
if (!config.promptRunID) {
|
|
1079
1313
|
LogStatus(
|
|
1080
1314
|
`RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
|
|
1081
1315
|
'creation was skipped or failed) — usage delta dropped.',
|
|
1082
1316
|
);
|
|
1083
1317
|
return null;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1318
|
+
}
|
|
1319
|
+
const verified = await this.runIDVerifier.Verify(
|
|
1320
|
+
session.ID,
|
|
1321
|
+
{ CoAgentRunID: config.coAgentRunID, PromptRunID: config.promptRunID },
|
|
1322
|
+
runUser,
|
|
1323
|
+
provider,
|
|
1324
|
+
);
|
|
1325
|
+
if (!verified.PromptRunID) {
|
|
1326
|
+
LogError(`RelayRealtimeUsage: session ${session.ID} names a promptRunID that is not its co-agent prompt run — ignored.`);
|
|
1086
1327
|
return null;
|
|
1087
1328
|
}
|
|
1329
|
+
return verified.PromptRunID;
|
|
1088
1330
|
}
|
|
1089
1331
|
|
|
1090
1332
|
/**
|
|
@@ -1118,6 +1360,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1118
1360
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1119
1361
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
1120
1362
|
|
|
1363
|
+
if (!this.isChannelInSessionScope(session, channelName)) {
|
|
1364
|
+
LogError(`SaveSessionChannelState: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — state not saved.`);
|
|
1365
|
+
return false;
|
|
1366
|
+
}
|
|
1121
1367
|
if (stateJson.length > MAX_CHANNEL_STATE_CHARS) {
|
|
1122
1368
|
LogError(
|
|
1123
1369
|
`SaveSessionChannelState: rejected oversized state for session ${agentSessionId} / channel '${channelName}' ` +
|
|
@@ -1197,6 +1443,11 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1197
1443
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1198
1444
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
1199
1445
|
|
|
1446
|
+
if (!this.isChannelInSessionScope(session, channelName)) {
|
|
1447
|
+
const message = `SaveSessionChannelArtifact: channel '${channelName}' is not in session ${agentSessionId}'s resolved scope — nothing saved.`;
|
|
1448
|
+
LogError(message);
|
|
1449
|
+
return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
|
|
1450
|
+
}
|
|
1200
1451
|
if (contentJson.length > MAX_CHANNEL_STATE_CHARS) {
|
|
1201
1452
|
const message =
|
|
1202
1453
|
`SaveSessionChannelArtifact: rejected oversized content for session ${agentSessionId} / channel '${channelName}' ` +
|
|
@@ -1234,8 +1485,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1234
1485
|
}
|
|
1235
1486
|
|
|
1236
1487
|
/**
|
|
1237
|
-
* Authorization gate for
|
|
1238
|
-
*
|
|
1488
|
+
* Authorization gate for the **target** agent: the caller must be able to run it. Denial throws.
|
|
1489
|
+
* {@link RealtimeClientSessionResolver.StartRealtimeClientSession} runs it before any session is
|
|
1490
|
+
* created, and {@link RealtimeClientSessionResolver.ExecuteRealtimeSessionTool} runs it on every
|
|
1491
|
+
* relayed call, because the owner can edit the target in the session config.
|
|
1239
1492
|
*/
|
|
1240
1493
|
private async assertCanRunTarget(
|
|
1241
1494
|
targetAgentId: string,
|
|
@@ -1250,39 +1503,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1250
1503
|
}
|
|
1251
1504
|
}
|
|
1252
1505
|
|
|
1253
|
-
/**
|
|
1254
|
-
* Narrows a session's colleague union (`allowedAgents`) to the agents the CALLER may run.
|
|
1255
|
-
*
|
|
1256
|
-
* {@link assertCanRunTarget} gates the LEAD target at session start, but the union it travels
|
|
1257
|
-
* with was never gated at all — a model-named colleague resolves straight to a delegated run.
|
|
1258
|
-
* That was survivable while the run carried the caller's own identity, because base-agent
|
|
1259
|
-
* re-checks `CanRun` against `contextUser`. Once the run user is elevated for a scoped anonymous
|
|
1260
|
-
* caller (issue #3371) that check sees the SYSTEM user, so this is the only remaining place the
|
|
1261
|
-
* caller's own authority is applied to a colleague. It therefore runs for EVERY caller, elevated
|
|
1262
|
-
* or not — the authorization identity must never depend on the elevation decision.
|
|
1263
|
-
*
|
|
1264
|
-
* `HasPermission` reads AIEngineBase's in-memory caches (no DB round trip) and already fails
|
|
1265
|
-
* closed on error, so an unresolvable agent drops OUT of the union rather than becoming runnable.
|
|
1266
|
-
* A filtered-out colleague is not an error: the delegation layer reports it as "not available in
|
|
1267
|
-
* this session" and lists what remains, which is the same answer the model gets for a typo.
|
|
1268
|
-
*
|
|
1269
|
-
* @param allowedAgents The session's persisted colleague union (absent/empty ⇒ single-target).
|
|
1270
|
-
* @param contextUser The ORIGINAL caller — never the elevated run user.
|
|
1271
|
-
* @returns The subset the caller may run, preserving order.
|
|
1272
|
-
*/
|
|
1273
|
-
private async filterAllowedAgentsByCanRun(
|
|
1274
|
-
allowedAgents: RealtimeAllowedAgent[] | undefined,
|
|
1275
|
-
contextUser: UserInfo,
|
|
1276
|
-
): Promise<RealtimeAllowedAgent[] | undefined> {
|
|
1277
|
-
if (!allowedAgents || allowedAgents.length === 0) {
|
|
1278
|
-
return allowedAgents;
|
|
1279
|
-
}
|
|
1280
|
-
const verdicts = await Promise.all(
|
|
1281
|
-
allowedAgents.map((a) => AIAgentPermissionHelper.HasPermission(a.agentId, contextUser, 'run')),
|
|
1282
|
-
);
|
|
1283
|
-
return allowedAgents.filter((_, i) => verdicts[i]);
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
1506
|
/**
|
|
1287
1507
|
* Resolves the AUTHORITATIVE target agent id under the co-agent's PAIRING CONSTRAINTS
|
|
1288
1508
|
* (`MJ: AI Agent Co Agents`, ordered by `Sequence`):
|
|
@@ -1525,195 +1745,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1525
1745
|
}
|
|
1526
1746
|
}
|
|
1527
1747
|
|
|
1528
|
-
/**
|
|
1529
|
-
* Resolves the co-agent (the Realtime-type agent that voices the target agent) for a new
|
|
1530
|
-
* client-direct session via the metadata-driven **CO-AGENT RESOLUTION CHAIN** — first match
|
|
1531
|
-
* wins, evaluated in precedence order:
|
|
1532
|
-
*
|
|
1533
|
-
* 1. **Runtime parameter** — the mutation's explicit `coAgentId`. A per-call override; an
|
|
1534
|
-
* invalid candidate (unknown, not Active, or not of the Realtime agent type) **throws**
|
|
1535
|
-
* (fail loud — the caller asked for something specific, mirroring `preferredModelId`).
|
|
1536
|
-
* 2. **Per-agent persona** — the target agent's `AIAgent.DefaultCoAgentID`. An invalid
|
|
1537
|
-
* reference logs a warning and **falls through** to the next step (stale metadata should
|
|
1538
|
-
* degrade gracefully, never break calls).
|
|
1539
|
-
* 3. **Per-type default** — an Active `AIAgentCoAgent` row of Type `'CoAgent'` whose
|
|
1540
|
-
* `TargetAgentTypeID` is the target agent's type and `IsDefault = 1` (lowest `Sequence`
|
|
1541
|
-
* wins a tie). Same tolerant warn-and-fall-through semantics as step 2.
|
|
1542
|
-
* 4. **Global default** — the seeded {@link REALTIME_CO_AGENT_NAME} agent, looked up by name
|
|
1543
|
-
* (with a deprecated fallback to {@link LEGACY_REALTIME_CO_AGENT_NAME}). Throws when absent
|
|
1544
|
-
* entirely (the realtime feature is unconfigured in this deployment).
|
|
1545
|
-
*
|
|
1546
|
-
* Every candidate from steps 1–3 is validated by {@link findValidCoAgent}: it must exist in
|
|
1547
|
-
* {@link AIEngine}'s cached agents, have Status `Active`, and be of the
|
|
1548
|
-
* {@link REALTIME_AGENT_TYPE_NAME} agent type. Step 4 keeps its original name-lookup contract.
|
|
1549
|
-
*
|
|
1550
|
-
* @param targetAgentId The agent the co-agent will voice on behalf of (drives steps 2–3).
|
|
1551
|
-
* @param explicitCoAgentId The runtime `coAgentId` mutation argument, when supplied (step 1).
|
|
1552
|
-
* @returns The resolved co-agent's id (canonical casing from the metadata cache).
|
|
1553
|
-
*/
|
|
1554
|
-
private async resolveCoAgentID(
|
|
1555
|
-
targetAgentId: string | undefined,
|
|
1556
|
-
explicitCoAgentId: string | undefined,
|
|
1557
|
-
contextUser: UserInfo,
|
|
1558
|
-
provider: IMetadataProvider,
|
|
1559
|
-
): Promise<string> {
|
|
1560
|
-
await AIEngine.Instance.Config(false, contextUser, provider);
|
|
1561
|
-
|
|
1562
|
-
// Step 1 — explicit runtime parameter: fail LOUD on any problem.
|
|
1563
|
-
if (explicitCoAgentId) {
|
|
1564
|
-
const { agent, problem } = this.findValidCoAgent(explicitCoAgentId);
|
|
1565
|
-
if (!agent) {
|
|
1566
|
-
throw new Error(`Invalid coAgentId '${explicitCoAgentId}': ${problem}`);
|
|
1567
|
-
}
|
|
1568
|
-
return agent.ID;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
const targetAgent = targetAgentId
|
|
1572
|
-
? (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, targetAgentId))
|
|
1573
|
-
: undefined;
|
|
1574
|
-
|
|
1575
|
-
// Step 2 — the target agent's own DefaultCoAgentID (per-agent persona): warn + fall through.
|
|
1576
|
-
const fromAgent = this.resolveMetadataDefault(
|
|
1577
|
-
targetAgent?.DefaultCoAgentID,
|
|
1578
|
-
`agent '${targetAgent?.Name}' (DefaultCoAgentID)`,
|
|
1579
|
-
);
|
|
1580
|
-
if (fromAgent) {
|
|
1581
|
-
return fromAgent;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// Step 3 — the type-level AIAgentCoAgent default row (per-type default): warn + fall through.
|
|
1585
|
-
const agentType = targetAgent?.TypeID
|
|
1586
|
-
? (AIEngine.Instance.AgentTypes ?? []).find(t => UUIDsEqual(t.ID, targetAgent.TypeID))
|
|
1587
|
-
: undefined;
|
|
1588
|
-
const typeDefaultCoAgentID = agentType
|
|
1589
|
-
? this.findTypeDefaultCoAgentID(agentType.ID, contextUser, provider)
|
|
1590
|
-
: undefined;
|
|
1591
|
-
const fromType = this.resolveMetadataDefault(
|
|
1592
|
-
await typeDefaultCoAgentID,
|
|
1593
|
-
`agent type '${agentType?.Name}' (AIAgentCoAgent type-default row)`,
|
|
1594
|
-
);
|
|
1595
|
-
if (fromType) {
|
|
1596
|
-
return fromType;
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
// Step 4 — global default: the seeded Realtime Co-Agent, by name (original behavior).
|
|
1600
|
-
return this.resolveGlobalCoAgentID();
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
/**
|
|
1604
|
-
* Chain step 3 lookup: the TYPE-LEVEL default co-agent for an agent type — the Active
|
|
1605
|
-
* `AIAgentCoAgent` row of Type `'CoAgent'` whose `TargetAgentTypeID` matches, with
|
|
1606
|
-
* `IsDefault = 1` preferred and the lowest `Sequence` breaking ties (so a deployment can
|
|
1607
|
-
* stage multiple type-level candidates deterministically). Reads {@link AIEngineBase}'s
|
|
1608
|
-
* cached rows — no RunView. Tolerant: a failed cache read logs and returns `undefined`
|
|
1609
|
-
* (the chain falls through to the global default).
|
|
1610
|
-
*/
|
|
1611
|
-
private async findTypeDefaultCoAgentID(
|
|
1612
|
-
agentTypeID: string,
|
|
1613
|
-
contextUser: UserInfo,
|
|
1614
|
-
provider: IMetadataProvider,
|
|
1615
|
-
): Promise<string | undefined> {
|
|
1616
|
-
try {
|
|
1617
|
-
const engine = await this.configuredAIEngineBase(contextUser, provider);
|
|
1618
|
-
const candidates = (engine.AgentCoAgents ?? [])
|
|
1619
|
-
.filter((r) =>
|
|
1620
|
-
r.Type === 'CoAgent' &&
|
|
1621
|
-
r.Status === 'Active' &&
|
|
1622
|
-
r.TargetAgentTypeID != null &&
|
|
1623
|
-
UUIDsEqual(r.TargetAgentTypeID, agentTypeID))
|
|
1624
|
-
.sort((a, b) =>
|
|
1625
|
-
(a.IsDefault === b.IsDefault ? 0 : a.IsDefault ? -1 : 1) ||
|
|
1626
|
-
(a.Sequence ?? 0) - (b.Sequence ?? 0));
|
|
1627
|
-
return candidates[0]?.CoAgentID;
|
|
1628
|
-
} catch (error) {
|
|
1629
|
-
LogError(
|
|
1630
|
-
`StartRealtimeClientSession: ${CO_AGENT_ENTITY} cache read failed while resolving the type-level ` +
|
|
1631
|
-
`default co-agent for agent type ${agentTypeID} (${(error as Error).message}) — falling through.`,
|
|
1632
|
-
);
|
|
1633
|
-
return undefined;
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
/**
|
|
1638
|
-
* Validates one metadata-level co-agent default (chain steps 2/3). Returns the resolved
|
|
1639
|
-
* co-agent id when the reference is valid; logs a warning and returns `null` (caller falls
|
|
1640
|
-
* through to the next chain step) when the reference is set but invalid — metadata drift must
|
|
1641
|
-
* degrade, not break live calls. A `null`/absent reference returns `null` silently.
|
|
1642
|
-
*/
|
|
1643
|
-
private resolveMetadataDefault(candidateId: string | null | undefined, source: string): string | null {
|
|
1644
|
-
if (!candidateId) {
|
|
1645
|
-
return null;
|
|
1646
|
-
}
|
|
1647
|
-
const { agent, problem } = this.findValidCoAgent(candidateId);
|
|
1648
|
-
if (agent) {
|
|
1649
|
-
return agent.ID;
|
|
1650
|
-
}
|
|
1651
|
-
LogError(
|
|
1652
|
-
`StartRealtimeClientSession: ignoring co-agent default '${candidateId}' from ${source} — ${problem} ` +
|
|
1653
|
-
'Falling through to the next step of the co-agent resolution chain.',
|
|
1654
|
-
);
|
|
1655
|
-
return null;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
/**
|
|
1659
|
-
* Validates a co-agent candidate against {@link AIEngine}'s cached metadata. A valid co-agent
|
|
1660
|
-
* must (a) exist, (b) have Status `Active`, and (c) be of the {@link REALTIME_AGENT_TYPE_NAME}
|
|
1661
|
-
* agent type. Returns the cached agent on success, or a human-readable `problem` on failure —
|
|
1662
|
-
* the CALLER decides whether that's fatal (explicit runtime param) or tolerated (metadata default).
|
|
1663
|
-
*/
|
|
1664
|
-
private findValidCoAgent(candidateId: string): { agent?: MJAIAgentEntityExtended; problem?: string } {
|
|
1665
|
-
const agent = (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, candidateId));
|
|
1666
|
-
if (!agent) {
|
|
1667
|
-
return { problem: 'no agent with that ID exists.' };
|
|
1668
|
-
}
|
|
1669
|
-
if (agent.Status !== 'Active') {
|
|
1670
|
-
return { problem: `agent '${agent.Name}' is not Active (Status: ${agent.Status}).` };
|
|
1671
|
-
}
|
|
1672
|
-
const realtimeType = (AIEngine.Instance.AgentTypes ?? []).find(
|
|
1673
|
-
t => t.Name?.trim().toLowerCase() === REALTIME_AGENT_TYPE_NAME.toLowerCase(),
|
|
1674
|
-
);
|
|
1675
|
-
if (!realtimeType) {
|
|
1676
|
-
return { problem: `the '${REALTIME_AGENT_TYPE_NAME}' agent type is not configured in this deployment.` };
|
|
1677
|
-
}
|
|
1678
|
-
if (!agent.TypeID || !UUIDsEqual(agent.TypeID, realtimeType.ID)) {
|
|
1679
|
-
return { problem: `agent '${agent.Name}' is not of the '${REALTIME_AGENT_TYPE_NAME}' agent type.` };
|
|
1680
|
-
}
|
|
1681
|
-
return { agent };
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* Resolves the seeded Realtime Co-Agent's id from {@link AIEngine}'s cached agents — the GLOBAL
|
|
1686
|
-
* DEFAULT (step 4) of the co-agent resolution chain. The engine is already configured by
|
|
1687
|
-
* {@link resolveCoAgentID}. Looks up the current {@link REALTIME_CO_AGENT_NAME} first, then
|
|
1688
|
-
* falls back to the DEPRECATED {@link LEGACY_REALTIME_CO_AGENT_NAME} (pre-rename seed) with a
|
|
1689
|
-
* deprecation log so un-resynced deployments keep working. Throws a clear error when neither
|
|
1690
|
-
* name is present in metadata.
|
|
1691
|
-
*/
|
|
1692
|
-
private resolveGlobalCoAgentID(): string {
|
|
1693
|
-
const coAgent = this.findAgentByName(REALTIME_CO_AGENT_NAME);
|
|
1694
|
-
if (coAgent) {
|
|
1695
|
-
return coAgent.ID;
|
|
1696
|
-
}
|
|
1697
|
-
const legacy = this.findAgentByName(LEGACY_REALTIME_CO_AGENT_NAME);
|
|
1698
|
-
if (legacy) {
|
|
1699
|
-
LogStatus(
|
|
1700
|
-
`StartRealtimeClientSession: resolved the global co-agent via its DEPRECATED legacy name ` +
|
|
1701
|
-
`'${LEGACY_REALTIME_CO_AGENT_NAME}'. Re-sync the agent seed metadata to rename it to ` +
|
|
1702
|
-
`'${REALTIME_CO_AGENT_NAME}'.`,
|
|
1703
|
-
);
|
|
1704
|
-
return legacy.ID;
|
|
1705
|
-
}
|
|
1706
|
-
throw new Error(
|
|
1707
|
-
`The '${REALTIME_CO_AGENT_NAME}' agent is not configured; cannot start a realtime voice session.`,
|
|
1708
|
-
);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
/** Case/whitespace-insensitive agent lookup by Name in {@link AIEngine}'s cached agents. */
|
|
1712
|
-
private findAgentByName(name: string): MJAIAgentEntityExtended | undefined {
|
|
1713
|
-
const wanted = name.toLowerCase();
|
|
1714
|
-
return (AIEngine.Instance.Agents ?? []).find(a => a.Name?.trim().toLowerCase() === wanted);
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
1748
|
/**
|
|
1718
1749
|
* Mints the client session config for a just-created session. On failure, closes the session so
|
|
1719
1750
|
* no half-open record leaks, then throws the service's error message.
|
|
@@ -1725,6 +1756,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1725
1756
|
* resumed session remembers the previous leg(s).
|
|
1726
1757
|
* @param conversationMessages Optional capped history of the TEXT conversation this session is
|
|
1727
1758
|
* starting from (from {@link loadConversationHistory}) — framed as "Conversation so far".
|
|
1759
|
+
* @param showsAgentVideo `false` when the app shows no agent video: the session asks for no avatar (`host`).
|
|
1728
1760
|
*/
|
|
1729
1761
|
private async prepareClientSessionOrClose(
|
|
1730
1762
|
session: MJAIAgentSessionEntity,
|
|
@@ -1740,6 +1772,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1740
1772
|
applicationId?: string,
|
|
1741
1773
|
appContext?: AppContextSnapshot,
|
|
1742
1774
|
conversationMessages?: ChatMessage[],
|
|
1775
|
+
channelCandidates?: RealtimeChannelCandidate[],
|
|
1776
|
+
showsAgentVideo?: boolean,
|
|
1743
1777
|
): Promise<StartRealtimeClientSessionResult> {
|
|
1744
1778
|
const prep = await this.clientSessionService.PrepareClientSession(
|
|
1745
1779
|
{
|
|
@@ -1767,6 +1801,14 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1767
1801
|
// (allowed-agent union); the snapshot is injected into the system prompt at mint.
|
|
1768
1802
|
ApplicationID: applicationId,
|
|
1769
1803
|
AppContext: appContext,
|
|
1804
|
+
// Scope-limited callers get no direct actions (see the authorization model above), so
|
|
1805
|
+
// the model is not given tools that the relay refuses.
|
|
1806
|
+
ExcludeDirectActions: IsScopeLimitedPrincipal(contextUser),
|
|
1807
|
+
// The channels the browser could mount: scoped here (registry + cascade) and handed back
|
|
1808
|
+
// as the resolved policy; the declared tools are narrowed to match.
|
|
1809
|
+
ChannelCandidates: channelCandidates,
|
|
1810
|
+
// An app with no channel that shows the agent's video: no avatar is asked for (reason `host`).
|
|
1811
|
+
ShowsAgentVideo: showsAgentVideo ?? undefined,
|
|
1770
1812
|
},
|
|
1771
1813
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
|
|
1772
1814
|
// observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
|
|
@@ -1787,7 +1829,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1787
1829
|
session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID,
|
|
1788
1830
|
applicationId, prep.EffectiveConfig?.realtime?.allowedAgents,
|
|
1789
1831
|
prep.EffectiveConfig?.realtime?.directActions,
|
|
1832
|
+
prep.ClientPolicy?.Channels.map((c) => c.Key),
|
|
1833
|
+
appContext,
|
|
1790
1834
|
);
|
|
1835
|
+
this.pruneExcludedServerChannels(session.ID, prep.ClientPolicy);
|
|
1791
1836
|
|
|
1792
1837
|
const cfg = prep.ClientConfig;
|
|
1793
1838
|
return {
|
|
@@ -1797,20 +1842,43 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1797
1842
|
Model: cfg.Model,
|
|
1798
1843
|
EphemeralToken: cfg.EphemeralToken,
|
|
1799
1844
|
ExpiresAt: cfg.ExpiresAt,
|
|
1845
|
+
Transport: cfg.Transport,
|
|
1846
|
+
RelayUrl: cfg.RelayUrl,
|
|
1800
1847
|
SessionConfigJson: JSON.stringify(cfg.SessionConfig),
|
|
1801
1848
|
ModelName: prep.ModelName,
|
|
1802
1849
|
DriverClass: prep.DriverClass,
|
|
1803
1850
|
NarrationInstructionsTemplate: prep.NarrationInstructionsTemplate,
|
|
1804
1851
|
NarrationPaceMs: prep.NarrationPaceMs,
|
|
1805
1852
|
EffectiveConfigJson: prep.EffectiveConfig ? JSON.stringify(prep.EffectiveConfig) : undefined,
|
|
1853
|
+
ClientPolicyJson: prep.ClientPolicy ? JSON.stringify(prep.ClientPolicy) : undefined,
|
|
1854
|
+
AvatarStatusJson: prep.AvatarStatus ? JSON.stringify(prep.AvatarStatus) : undefined,
|
|
1806
1855
|
};
|
|
1807
1856
|
}
|
|
1808
1857
|
|
|
1858
|
+
/**
|
|
1859
|
+
* Drops the server-side channel plugins of channels the resolved scope left out. Server plugins
|
|
1860
|
+
* start when the session row is created — before the scope is known — so an excluded channel's
|
|
1861
|
+
* server half would otherwise keep running. A no-op when the caller reported no candidates.
|
|
1862
|
+
*/
|
|
1863
|
+
private pruneExcludedServerChannels(agentSessionID: string, policy: RealtimeSessionClientPolicy | undefined): void {
|
|
1864
|
+
const excluded = (policy?.ExcludedChannels ?? []).map((e) => e.Key);
|
|
1865
|
+
if (excluded.length === 0) {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const dropped = RealtimeChannelServerHost.Instance.PruneSessionChannels(agentSessionID, excluded);
|
|
1869
|
+
if (dropped.length > 0) {
|
|
1870
|
+
LogStatus(`StartRealtimeClientSession: dropped the server plugins of out-of-scope channel(s) [${dropped.join(', ')}] for session ${agentSessionID}.`);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1809
1874
|
/**
|
|
1810
1875
|
* Writes the co-agent observability run ids into the session's `Config_` alongside the
|
|
1811
|
-
*
|
|
1812
|
-
* finalize the runs (and on relay to nest delegated runs).
|
|
1813
|
-
*
|
|
1876
|
+
* `targetAgentID`, then saves the session. These ids are read back on close to
|
|
1877
|
+
* finalize the runs (and on relay to nest delegated runs). The app awareness (application id,
|
|
1878
|
+
* allowed agents, direct actions, app-context snapshot) is written with them; relays read the
|
|
1879
|
+
* allowed agents and the app context, never the direct actions.
|
|
1880
|
+
* Best-effort: a save failure is logged, not thrown — the voice session still proceeds, it just
|
|
1881
|
+
* won't carry the run ids.
|
|
1814
1882
|
*/
|
|
1815
1883
|
private async persistObservabilityRunIDs(
|
|
1816
1884
|
session: MJAIAgentSessionEntity,
|
|
@@ -1821,22 +1889,29 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1821
1889
|
applicationID?: string,
|
|
1822
1890
|
allowedAgents?: RealtimeAllowedAgent[],
|
|
1823
1891
|
directActions?: RealtimeDirectActionsConfig,
|
|
1892
|
+
channels?: string[],
|
|
1893
|
+
appContext?: AppContextSnapshot,
|
|
1824
1894
|
): Promise<void> {
|
|
1825
|
-
//
|
|
1826
|
-
//
|
|
1827
|
-
|
|
1828
|
-
const config
|
|
1895
|
+
// Start from the RAW stored config so every key set at session start survives — the
|
|
1896
|
+
// server-authoritative voice deadline (`maxSessionDeadlineIso`) and the per-session media-kit
|
|
1897
|
+
// override among them — rather than rebuilding from a whitelist that silently drops the rest.
|
|
1898
|
+
const config = {
|
|
1899
|
+
...ParseConfigObject(session.Config_),
|
|
1829
1900
|
targetAgentID,
|
|
1830
1901
|
coAgentRunID,
|
|
1831
1902
|
promptRunID,
|
|
1832
1903
|
coAgentRunStepID,
|
|
1833
|
-
maxSessionDeadlineIso: existing?.maxSessionDeadlineIso,
|
|
1834
1904
|
applicationID,
|
|
1905
|
+
appContext: this.appContextToStore(appContext, session.ID),
|
|
1835
1906
|
allowedAgents: allowedAgents && allowedAgents.length > 0 ? allowedAgents : undefined,
|
|
1836
1907
|
directActions,
|
|
1908
|
+
channels,
|
|
1837
1909
|
};
|
|
1838
1910
|
session.Config_ = JSON.stringify(config);
|
|
1839
|
-
|
|
1911
|
+
// Every value written here is server-decided (run ids, the resolved target, the in-scope channel
|
|
1912
|
+
// set), and `channels` is a server-authoritative key the owner may not write — so this save runs in
|
|
1913
|
+
// the trusted scope. Protected keys already in the stored config are carried forward unchanged.
|
|
1914
|
+
const saved = await RunWithTrustedSessionConfigWrites(() => session.Save());
|
|
1840
1915
|
if (!saved) {
|
|
1841
1916
|
LogError(
|
|
1842
1917
|
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
@@ -1844,6 +1919,32 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1844
1919
|
}
|
|
1845
1920
|
}
|
|
1846
1921
|
|
|
1922
|
+
/**
|
|
1923
|
+
* Returns the app-context snapshot to store on the session config. A snapshot whose JSON is longer
|
|
1924
|
+
* than {@link MAX_CLIENT_TOOLS_JSON_CHARS} is not stored: a warning is logged and `undefined` is
|
|
1925
|
+
* returned, so the session still starts and its delegated runs get no app context.
|
|
1926
|
+
*
|
|
1927
|
+
* @param appContext The snapshot the browser sent at session start, or undefined.
|
|
1928
|
+
* @param sessionID The session the snapshot belongs to (for the warning).
|
|
1929
|
+
* @returns The snapshot to store, or `undefined`.
|
|
1930
|
+
*/
|
|
1931
|
+
private appContextToStore(appContext: AppContextSnapshot | undefined, sessionID: string): AppContextSnapshot | undefined {
|
|
1932
|
+
if (!appContext) {
|
|
1933
|
+
return undefined;
|
|
1934
|
+
}
|
|
1935
|
+
const chars = JSON.stringify(appContext).length;
|
|
1936
|
+
if (chars > MAX_CLIENT_TOOLS_JSON_CHARS) {
|
|
1937
|
+
LogErrorEx({
|
|
1938
|
+
message:
|
|
1939
|
+
`RealtimeClientSessionResolver: app context for session ${sessionID} not stored — ${chars} chars ` +
|
|
1940
|
+
`exceeds the ${MAX_CLIENT_TOOLS_JSON_CHARS} cap; delegated runs get no app context.`,
|
|
1941
|
+
severity: 'warning',
|
|
1942
|
+
});
|
|
1943
|
+
return undefined;
|
|
1944
|
+
}
|
|
1945
|
+
return appContext;
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1847
1948
|
/**
|
|
1848
1949
|
* Loads a session, enforcing inbound ownership (`UserID === contextUser.ID`) and that it is not
|
|
1849
1950
|
* `Closed`. Throws on any violation. Returns the loaded session for the caller to read from.
|
|
@@ -2089,6 +2190,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2089
2190
|
* as the resumed transcript are removed, so the prompt never carries two accounts of one
|
|
2090
2191
|
* exchange.
|
|
2091
2192
|
*
|
|
2193
|
+
* Only the rows on the session's path that no rerun replaced are read: the fork it started on,
|
|
2194
|
+
* or Main, in `Sequence` order. When that path cannot be built — its branch rows are missing or invalid — the session
|
|
2195
|
+
* starts with no history, never with every branch's rows.
|
|
2196
|
+
*
|
|
2092
2197
|
* Strictly tolerant — no conversation, a failed read, or any throw yields `[]`, never a failed
|
|
2093
2198
|
* session start. The call still works; it just starts cold.
|
|
2094
2199
|
*
|
|
@@ -2096,6 +2201,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2096
2201
|
* @param coveredSessionIDs Session ids already framed via `PriorTranscript`.
|
|
2097
2202
|
* @param contextUser The calling user (RunView scope — the read is row-level-secured).
|
|
2098
2203
|
* @param provider The request-scoped metadata provider.
|
|
2204
|
+
* @param branchId The fork the session started on; null is Main.
|
|
2099
2205
|
* @returns The capped history, oldest first.
|
|
2100
2206
|
*/
|
|
2101
2207
|
private async loadConversationHistory(
|
|
@@ -2103,12 +2209,22 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2103
2209
|
coveredSessionIDs: string[],
|
|
2104
2210
|
contextUser: UserInfo,
|
|
2105
2211
|
provider: IMetadataProvider,
|
|
2212
|
+
branchId: string | null,
|
|
2106
2213
|
): Promise<ChatMessage[]> {
|
|
2107
2214
|
if (!conversationId) {
|
|
2108
2215
|
return [];
|
|
2109
2216
|
}
|
|
2217
|
+
const rv = RunView.FromMetadataProvider(provider);
|
|
2218
|
+
let pathFilter: string;
|
|
2219
|
+
try {
|
|
2220
|
+
pathFilter = await ConversationEngine.BranchPathFilterFresh(conversationId, branchId, contextUser, provider);
|
|
2221
|
+
} catch (error) {
|
|
2222
|
+
LogError(
|
|
2223
|
+
`StartRealtimeClientSession: could not build the path of the session's view for conversation ${conversationId}: ${(error as Error).message}`,
|
|
2224
|
+
);
|
|
2225
|
+
return [];
|
|
2226
|
+
}
|
|
2110
2227
|
try {
|
|
2111
|
-
const rv = RunView.FromMetadataProvider(provider);
|
|
2112
2228
|
const result = await rv.RunView<{
|
|
2113
2229
|
Role: string;
|
|
2114
2230
|
Message: string | null;
|
|
@@ -2117,9 +2233,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2117
2233
|
}>(
|
|
2118
2234
|
{
|
|
2119
2235
|
EntityName: CONVERSATION_DETAIL_ENTITY,
|
|
2120
|
-
ExtraFilter:
|
|
2236
|
+
ExtraFilter: ConversationEngine.LiveRowsFilter(pathFilter),
|
|
2121
2237
|
Fields: ['ID', 'Role', 'Message', 'HiddenToUser', 'AgentSessionID', '__mj_CreatedAt'],
|
|
2122
|
-
OrderBy: '
|
|
2238
|
+
OrderBy: 'Sequence ASC',
|
|
2123
2239
|
ResultType: 'simple',
|
|
2124
2240
|
},
|
|
2125
2241
|
contextUser,
|
|
@@ -2486,6 +2602,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2486
2602
|
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY, callerUser);
|
|
2487
2603
|
detail.NewRecord();
|
|
2488
2604
|
detail.ConversationID = session.ConversationID;
|
|
2605
|
+
detail.BranchID = this.sessionBranchIdOf(session);
|
|
2489
2606
|
detail.Role = 'AI';
|
|
2490
2607
|
detail.HiddenToUser = true;
|
|
2491
2608
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
@@ -2638,21 +2755,54 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2638
2755
|
|
|
2639
2756
|
/** Shape check for one client tool declaration: Name/Description non-empty strings, ParametersSchema a plain object. */
|
|
2640
2757
|
private isValidClientTool(candidate: unknown): boolean {
|
|
2641
|
-
|
|
2758
|
+
return IsValidRealtimeToolDefinition(candidate);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
/**
|
|
2762
|
+
* Tolerantly parses the channel candidates the browser reported at mint (the same caps and
|
|
2763
|
+
* tolerance as {@link parseClientTools}: capped, never throws, anything wrong is dropped and
|
|
2764
|
+
* logged). A caller that sent none yields `undefined` — an unscoped session, exactly as before.
|
|
2765
|
+
*/
|
|
2766
|
+
private parseChannelCandidates(channelCandidatesJson?: string): RealtimeChannelCandidate[] | undefined {
|
|
2767
|
+
const parsed = ParseRealtimeChannelCandidates(channelCandidatesJson);
|
|
2768
|
+
for (const reason of parsed.Rejected) {
|
|
2769
|
+
LogError(`StartRealtimeClientSession: ${reason}.`);
|
|
2770
|
+
}
|
|
2771
|
+
return parsed.Candidates.length > 0 ? parsed.Candidates : undefined;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
/**
|
|
2775
|
+
* Whether a session's persisted scope allows a server-side call to name `channelName`. A session
|
|
2776
|
+
* with no persisted scope (it reported no candidates) is unscoped and allows everything; a scoped
|
|
2777
|
+
* one allows only the channels its policy put in the session.
|
|
2778
|
+
*/
|
|
2779
|
+
private isChannelInSessionScope(session: MJAIAgentSessionEntity, channelName: string): boolean {
|
|
2780
|
+
// Read the RAW config: an absent `channels` key means an unscoped session, but anything present
|
|
2781
|
+
// and malformed fails CLOSED — a scope that cannot be read must not widen to "everything".
|
|
2782
|
+
// (`channels` is a server-authoritative key, so the owner cannot delete it to unscope a session.)
|
|
2783
|
+
const channels = ParseConfigObject(session.Config_)['channels'];
|
|
2784
|
+
if (channels === undefined) {
|
|
2785
|
+
return true;
|
|
2786
|
+
}
|
|
2787
|
+
if (!Array.isArray(channels)) {
|
|
2788
|
+
LogError(`RealtimeClientSessionResolver: session ${session.ID} has a malformed channel scope; refusing channel '${channelName}'.`);
|
|
2642
2789
|
return false;
|
|
2643
2790
|
}
|
|
2644
|
-
const
|
|
2645
|
-
return (
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2791
|
+
const wanted = NormalizeChannelKey(channelName);
|
|
2792
|
+
return channels.some((key) => typeof key === 'string' && NormalizeChannelKey(key) === wanted);
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
/** The fork the session was started on (its config's `conversationBranchID`); null is Main. */
|
|
2796
|
+
private sessionBranchIdOf(session: MJAIAgentSessionEntity): string | null {
|
|
2797
|
+
const branchId = ParseConfigObject(session.Config_)['conversationBranchID'];
|
|
2798
|
+
return typeof branchId === 'string' && branchId.length > 0 ? branchId : null;
|
|
2650
2799
|
}
|
|
2651
2800
|
|
|
2652
2801
|
/**
|
|
2653
|
-
* Parses the session's persisted config, returning
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2802
|
+
* Parses the session's persisted config, returning its `targetAgentID` plus any observability run
|
|
2803
|
+
* ids (`coAgentRunID`/`promptRunID`). Throws when the config is missing/malformed or carries no
|
|
2804
|
+
* target — a relay cannot proceed without a target. The owner can edit the config, so callers
|
|
2805
|
+
* re-check what they act on.
|
|
2656
2806
|
*/
|
|
2657
2807
|
private readSessionConfig(session: MJAIAgentSessionEntity): RealtimeSessionConfig {
|
|
2658
2808
|
const raw = session.Config_;
|
|
@@ -2670,11 +2820,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2670
2820
|
maxSessionDeadlineIso:
|
|
2671
2821
|
typeof parsed.maxSessionDeadlineIso === 'string' ? parsed.maxSessionDeadlineIso : undefined,
|
|
2672
2822
|
applicationID: typeof parsed.applicationID === 'string' ? parsed.applicationID : undefined,
|
|
2823
|
+
appContext:
|
|
2824
|
+
parsed.appContext && typeof parsed.appContext === 'object' && !Array.isArray(parsed.appContext)
|
|
2825
|
+
? parsed.appContext
|
|
2826
|
+
: undefined,
|
|
2673
2827
|
allowedAgents: Array.isArray(parsed.allowedAgents) ? parsed.allowedAgents : undefined,
|
|
2674
2828
|
directActions:
|
|
2675
2829
|
parsed.directActions && typeof parsed.directActions === 'object' && typeof parsed.directActions.enabled === 'boolean'
|
|
2676
2830
|
? (parsed.directActions as RealtimeDirectActionsConfig)
|
|
2677
2831
|
: undefined,
|
|
2832
|
+
conversationBranchID: typeof parsed.conversationBranchID === 'string' ? parsed.conversationBranchID : undefined,
|
|
2833
|
+
mediaCollectionID: typeof parsed.mediaCollectionID === 'string' ? parsed.mediaCollectionID : undefined,
|
|
2834
|
+
channels: Array.isArray(parsed.channels) ? parsed.channels.filter((k): k is string => typeof k === 'string') : undefined,
|
|
2678
2835
|
};
|
|
2679
2836
|
}
|
|
2680
2837
|
} catch {
|
|
@@ -2762,6 +2919,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2762
2919
|
);
|
|
2763
2920
|
detail.NewRecord();
|
|
2764
2921
|
detail.ConversationID = session.ConversationID;
|
|
2922
|
+
detail.BranchID = this.sessionBranchIdOf(session);
|
|
2765
2923
|
detail.Role = this.mapTranscriptRole(role);
|
|
2766
2924
|
detail.Message = text;
|
|
2767
2925
|
detail.AgentSessionID = session.ID;
|
|
@@ -2935,6 +3093,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2935
3093
|
);
|
|
2936
3094
|
detail.NewRecord();
|
|
2937
3095
|
detail.ConversationID = session.ConversationID;
|
|
3096
|
+
detail.BranchID = this.sessionBranchIdOf(session);
|
|
2938
3097
|
detail.Role = 'AI';
|
|
2939
3098
|
detail.HiddenToUser = true;
|
|
2940
3099
|
detail.AgentSessionID = session.ID;
|