@memberjunction/server 6.2.0-edge.3 → 6.2.0-edge.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +2378 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -0
- 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 +14 -11
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +135 -0
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +706 -0
- 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 +20 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +39 -10
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +4 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -131
- 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/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 +14 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +21 -5
- 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/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/HumanHandoffResolver.d.ts.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.js +7 -1
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
- 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/RealtimeBridgeResolver.d.ts +54 -2
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +155 -25
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +394 -90
- 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/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/meetingMutations.d.ts +6 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
- package/dist/resolvers/meetings/meetingMutations.js +21 -0
- package/dist/resolvers/meetings/meetingMutations.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/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 +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +97 -95
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- 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__/FetchEntityVectorsResolver.test.ts +285 -0
- package/src/__tests__/HostInstance.test.ts +84 -0
- package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
- package/src/__tests__/MeetingResolver.test.ts +75 -1
- package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
- 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.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
- 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__/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__/actionAuthorization.test.ts +130 -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__/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__/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 +125 -0
- package/src/context.ts +15 -10
- package/src/generated/graphql-schemas/__mj.ts +496 -1
- package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
- package/src/generic/ResolverBase.ts +39 -9
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +132 -143
- package/src/isolatedProvider.ts +22 -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 +24 -6
- package/src/resolvers/CacheStatsResolver.ts +89 -2
- package/src/resolvers/ClientToolRequestResolver.ts +25 -7
- package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
- package/src/resolvers/HumanHandoffResolver.ts +8 -0
- package/src/resolvers/MCPResolver.ts +8 -5
- package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
- package/src/resolvers/RealtimeClientSessionResolver.ts +501 -96
- 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/agentVisionConsentAudit.ts +82 -0
- package/src/resolvers/avatarInputValidation.ts +123 -0
- package/src/resolvers/currentUserRoles.ts +32 -1
- package/src/resolvers/meetings/meetingMutations.ts +21 -0
- package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
- package/src/sharedCache.ts +302 -0
- package/src/types.ts +7 -0
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* and **executes** every tool call the browser relays back. The three mutations here cover the MVP:
|
|
12
12
|
*
|
|
13
13
|
* 1. {@link RealtimeClientSessionResolver.StartRealtimeClientSession} — authorize, create the
|
|
14
|
-
* session (storing the target agent id
|
|
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,
|
|
@@ -50,6 +58,7 @@ import {
|
|
|
50
58
|
ResolveEffectiveRealtimeConfig,
|
|
51
59
|
ResolveRealtimeCoAgentID,
|
|
52
60
|
FilterAllowedAgentsByCanRun,
|
|
61
|
+
INVOKE_TARGET_AGENT_TOOL_NAME,
|
|
53
62
|
RealtimeAllowedAgent,
|
|
54
63
|
RealtimeDirectActionsConfig,
|
|
55
64
|
REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION,
|
|
@@ -58,13 +67,26 @@ import {
|
|
|
58
67
|
writeRealtimeRecordingSegment,
|
|
59
68
|
deleteRealtimeRecordingSegments,
|
|
60
69
|
} from '@memberjunction/ai-agents';
|
|
61
|
-
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';
|
|
62
79
|
import { ChatMessage, RealtimeToolDefinition } from '@memberjunction/ai';
|
|
63
80
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
64
81
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
65
82
|
import { GetReadWriteProvider } from '../util.js';
|
|
66
83
|
import { SessionManager } from '../agentSessions/index.js';
|
|
67
|
-
import {
|
|
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';
|
|
68
90
|
|
|
69
91
|
/**
|
|
70
92
|
* Progress steps worth narrating to the realtime model — mirrors the normal agent-run path's filter
|
|
@@ -73,6 +95,12 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../
|
|
|
73
95
|
*/
|
|
74
96
|
const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
75
97
|
|
|
98
|
+
/**
|
|
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.
|
|
101
|
+
*/
|
|
102
|
+
const MAX_USAGE_DETAILS_JSON_LENGTH = 4096;
|
|
103
|
+
|
|
76
104
|
/** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
77
105
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
78
106
|
const CO_AGENT_ENTITY = 'MJ: AI Agent Co Agents';
|
|
@@ -100,7 +128,8 @@ const WHITEBOARD_ARTIFACT_TYPE_NAME = 'Whiteboard';
|
|
|
100
128
|
*/
|
|
101
129
|
const MAX_CLIENT_TOOLS = 64;
|
|
102
130
|
/** Maximum accepted size (chars) of the serialized client tool declarations. Raised in step with
|
|
103
|
-
* {@link MAX_CLIENT_TOOLS} — multi-channel tool sets with verbose descriptions + JSON schemas run large.
|
|
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. */
|
|
104
133
|
const MAX_CLIENT_TOOLS_JSON_CHARS = 256_000;
|
|
105
134
|
/** Maximum accepted size (chars) of a persisted channel state blob. */
|
|
106
135
|
const MAX_CHANNEL_STATE_CHARS = 2_000_000;
|
|
@@ -118,9 +147,9 @@ const MAX_CONVERSATION_HISTORY_TURNS = 30;
|
|
|
118
147
|
const MAX_CONVERSATION_HISTORY_CHARS = 8_000;
|
|
119
148
|
|
|
120
149
|
/**
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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).
|
|
124
153
|
*/
|
|
125
154
|
interface RealtimeSessionConfig {
|
|
126
155
|
/** The top-level target agent the co-agent voices on behalf of. */
|
|
@@ -157,6 +186,12 @@ interface RealtimeSessionConfig {
|
|
|
157
186
|
* for observability / continuity; absent when the session carries no app context.
|
|
158
187
|
*/
|
|
159
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;
|
|
160
195
|
/**
|
|
161
196
|
* The session's effective allowed delegation targets (union from the config cascade, incl. the app's
|
|
162
197
|
* `RelevantAgents`). Persisted at start so each relayed `invoke-target-agent` call can validate a
|
|
@@ -175,6 +210,24 @@ interface RealtimeSessionConfig {
|
|
|
175
210
|
* agent default kit is used (the common case).
|
|
176
211
|
*/
|
|
177
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[];
|
|
178
231
|
}
|
|
179
232
|
|
|
180
233
|
/**
|
|
@@ -199,14 +252,32 @@ export class StartRealtimeClientSessionResult {
|
|
|
199
252
|
@Field(() => String)
|
|
200
253
|
Model: string;
|
|
201
254
|
|
|
202
|
-
/**
|
|
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
|
+
*/
|
|
203
260
|
@Field(() => String)
|
|
204
261
|
EphemeralToken: string;
|
|
205
262
|
|
|
206
|
-
/** 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. */
|
|
207
264
|
@Field(() => String)
|
|
208
265
|
ExpiresAt: string;
|
|
209
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
|
+
|
|
210
281
|
/** JSON string of the provider-native session config the browser applies verbatim. */
|
|
211
282
|
@Field(() => String)
|
|
212
283
|
SessionConfigJson: string;
|
|
@@ -263,6 +334,25 @@ export class StartRealtimeClientSessionResult {
|
|
|
263
334
|
*/
|
|
264
335
|
@Field(() => String, { nullable: true })
|
|
265
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;
|
|
266
356
|
}
|
|
267
357
|
|
|
268
358
|
/**
|
|
@@ -356,6 +446,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
356
446
|
// state on — otherwise that cleanup silently no-ops on a freshly-constructed instance.
|
|
357
447
|
private readonly clientSessionService = new RealtimeClientSessionService();
|
|
358
448
|
private readonly sessionManager = new SessionManager(this.clientSessionService);
|
|
449
|
+
private readonly runIDVerifier = new SessionRunIDVerifier();
|
|
359
450
|
|
|
360
451
|
/**
|
|
361
452
|
* The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
|
|
@@ -392,7 +483,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
392
483
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
393
484
|
* see {@link ResolveRealtimeCoAgentID} for the full contract.
|
|
394
485
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
395
|
-
* config
|
|
486
|
+
* config — the target later relays use, re-authorized for the caller on each relay.
|
|
396
487
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
397
488
|
* On failure the just-created session is closed so no half-open session leaks.
|
|
398
489
|
*
|
|
@@ -418,6 +509,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
418
509
|
* `TypeConfiguration` ← this). **Authorization-gated**: requires the
|
|
419
510
|
* `Realtime: Advanced Session Controls` authorization — unauthorized callers receive a
|
|
420
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.
|
|
421
518
|
*
|
|
422
519
|
* @returns The ephemeral config + session linkage the browser needs to open its socket.
|
|
423
520
|
*/
|
|
@@ -436,6 +533,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
436
533
|
@Arg('mediaCollectionId', () => String, { nullable: true }) mediaCollectionId?: string,
|
|
437
534
|
@Arg('applicationId', () => String, { nullable: true }) applicationId?: string,
|
|
438
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,
|
|
439
539
|
): Promise<StartRealtimeClientSessionResult> {
|
|
440
540
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
441
541
|
|
|
@@ -473,22 +573,32 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
473
573
|
);
|
|
474
574
|
}
|
|
475
575
|
}
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
+
),
|
|
486
595
|
);
|
|
487
596
|
|
|
488
597
|
// Best-effort: stamp recording-start metadata when the browser captured (with consent) at start.
|
|
489
598
|
await this.stampRecordingStart(session, recordingConsent, recordingStartedAt);
|
|
490
599
|
|
|
491
600
|
const clientTools = this.parseClientTools(clientToolsJson);
|
|
601
|
+
const channelCandidates = this.parseChannelCandidates(channelCandidatesJson);
|
|
492
602
|
// Best-effort model-context hydration: the PRIOR session chain's transcript (ownership-
|
|
493
603
|
// checked, capped) is framed into the system prompt so the model REMEMBERS the last leg.
|
|
494
604
|
// Strictly tolerant — any problem yields no hydration, never a failed start.
|
|
@@ -498,24 +608,103 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
498
608
|
// framed as the resumed transcript, and injecting the same turns twice puts two accounts of
|
|
499
609
|
// the same exchange in one prompt.
|
|
500
610
|
const conversationMessages = await this.loadConversationHistory(
|
|
501
|
-
conversationId, prior?.LegIDs ?? [], contextUser, provider,
|
|
611
|
+
conversationId, prior?.LegIDs ?? [], contextUser, provider, sessionBranchId,
|
|
502
612
|
);
|
|
503
613
|
const result = await this.prepareClientSessionOrClose(
|
|
504
614
|
session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, prior?.Text,
|
|
505
615
|
configOverridesJson, maxSessionSeconds, applicationId, this.parseAppContext(appContextJson), conversationMessages,
|
|
616
|
+
channelCandidates, showsAgentVideo,
|
|
506
617
|
);
|
|
618
|
+
await this.stampVerificationPolicy(session, result.EffectiveConfigJson, lastSessionId, contextUser, provider);
|
|
507
619
|
// Best-effort restore of the PRIOR session's persisted channel states (e.g. the whiteboard
|
|
508
620
|
// board). Strictly tolerant — any problem yields a null field, never a failed start.
|
|
509
621
|
result.PriorChannelStatesJson = (await this.loadPriorChannelStatesJson(lastSessionId, contextUser, provider)) ?? undefined;
|
|
510
622
|
return result;
|
|
511
623
|
}
|
|
512
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
|
+
|
|
513
700
|
/**
|
|
514
701
|
* Execute a single tool call the browser relayed from its provider socket and return the
|
|
515
702
|
* serialized result for the browser to relay back to the model.
|
|
516
703
|
*
|
|
517
|
-
* Ownership-gated
|
|
518
|
-
*
|
|
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.
|
|
519
708
|
*
|
|
520
709
|
* @returns The serialized tool result JSON.
|
|
521
710
|
*/
|
|
@@ -531,13 +720,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
531
720
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
532
721
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
533
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
|
+
}
|
|
534
727
|
|
|
535
728
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): once ownership is proven above, the delegated
|
|
536
729
|
// run + its AI-run-entity writes execute as the system user for a scoped anonymous caller
|
|
537
730
|
// (the caller's role deliberately holds no grants on the run entities). The lead
|
|
538
|
-
// targetAgentID
|
|
539
|
-
//
|
|
731
|
+
// targetAgentID is CanRun-gated against the CALLER just above, and the colleague union
|
|
732
|
+
// just below, so elevation never widens agent authority.
|
|
540
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
|
+
);
|
|
541
740
|
if (runUser !== contextUser) {
|
|
542
741
|
LogStatus(
|
|
543
742
|
`ExecuteRealtimeSessionTool: dispatching relayed tool '${toolName}' for session ${agentSessionId} ` +
|
|
@@ -555,16 +754,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
555
754
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
556
755
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
557
756
|
AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
558
|
-
DirectActions:
|
|
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.
|
|
559
759
|
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
560
760
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
561
761
|
AttributionUserID: contextUser.ID,
|
|
562
762
|
// Nest the delegated target-agent run under the co-agent observability run (when present).
|
|
563
|
-
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,
|
|
564
770
|
Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
|
|
565
771
|
OnProgress: this.buildDelegationProgressCallback(pubSub, userPayload, agentSessionId, callId),
|
|
566
772
|
// Resume a previously-paused delegated run (if any) with the user's answer.
|
|
567
|
-
ResumeRunID:
|
|
773
|
+
ResumeRunID: runIDs.PendingFeedbackRunID,
|
|
568
774
|
},
|
|
569
775
|
runUser,
|
|
570
776
|
provider,
|
|
@@ -574,7 +780,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
574
780
|
`ExecuteRealtimeSessionTool: completed relayed tool '${toolName}' (callId: ${callId}) in ${durationMs}ms`,
|
|
575
781
|
);
|
|
576
782
|
|
|
577
|
-
if (toolName !==
|
|
783
|
+
if (toolName !== INVOKE_TARGET_AGENT_TOOL_NAME) {
|
|
578
784
|
await this.persistDirectActionTurn(
|
|
579
785
|
session,
|
|
580
786
|
callId,
|
|
@@ -631,6 +837,19 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
631
837
|
};
|
|
632
838
|
}
|
|
633
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
|
+
|
|
634
853
|
/**
|
|
635
854
|
* Rolls the session's `pendingFeedbackRunID` forward after a relayed tool call. The id we just
|
|
636
855
|
* consumed (resumed or none) is dropped; a new paused run id is stored only when the run paused
|
|
@@ -646,17 +865,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
646
865
|
if (!previous && !pausedRunID) {
|
|
647
866
|
return;
|
|
648
867
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
coAgentRunStepID: config.coAgentRunStepID,
|
|
654
|
-
pendingFeedbackRunID: pausedRunID,
|
|
655
|
-
// Preserve the server-authoritative voice deadline across config rewrites.
|
|
656
|
-
maxSessionDeadlineIso: config.maxSessionDeadlineIso,
|
|
657
|
-
applicationID: config.applicationID,
|
|
658
|
-
allowedAgents: config.allowedAgents,
|
|
659
|
-
};
|
|
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 };
|
|
660
872
|
session.Config_ = JSON.stringify(next);
|
|
661
873
|
if (!(await session.Save())) {
|
|
662
874
|
LogError(
|
|
@@ -701,14 +913,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
701
913
|
// conversation (run-viewer observability parity). Best-effort — never fails the transcript relay.
|
|
702
914
|
// The prompt-run write runs as the scoped-anonymous elevated user (issue #3371) — the visible
|
|
703
915
|
// Conversation Detail above deliberately stays on the caller.
|
|
704
|
-
const
|
|
916
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
917
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
705
918
|
if (promptRunID) {
|
|
706
919
|
await this.clientSessionService.AppendPromptRunMessage(
|
|
707
920
|
promptRunID,
|
|
708
921
|
this.mapTranscriptRoleToChatRole(role),
|
|
709
922
|
text,
|
|
710
923
|
replacesPrevious ?? false,
|
|
711
|
-
|
|
924
|
+
runUser,
|
|
712
925
|
provider,
|
|
713
926
|
);
|
|
714
927
|
}
|
|
@@ -767,11 +980,11 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
767
980
|
return { Success: false, ErrorMessage: 'Recording consent was not granted.' };
|
|
768
981
|
}
|
|
769
982
|
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
// session link, so nothing here depends on the caller's identity.
|
|
774
|
-
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);
|
|
775
988
|
try {
|
|
776
989
|
const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
|
|
777
990
|
if (!(await agent.Load(session.AgentID))) {
|
|
@@ -822,11 +1035,14 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
822
1035
|
|
|
823
1036
|
/**
|
|
824
1037
|
* Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
|
|
825
|
-
* folder (`realtime-recordings/<sessionId>/seg-NNNN
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
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.
|
|
830
1046
|
*
|
|
831
1047
|
* @param agentSessionId The in-progress session (ownership-gated).
|
|
832
1048
|
* @param segmentIndex 0-based shard index within the session.
|
|
@@ -845,6 +1061,12 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
845
1061
|
try {
|
|
846
1062
|
const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
|
|
847
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
|
+
}
|
|
848
1070
|
// Scoped-anonymous elevation (issue #3371) — same rationale as UploadRealtimeRecording.
|
|
849
1071
|
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
850
1072
|
const agent = await provider.GetEntityObject<MJAIAgentEntity>('MJ: AI Agents', runUser);
|
|
@@ -906,7 +1128,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
906
1128
|
provider,
|
|
907
1129
|
);
|
|
908
1130
|
|
|
909
|
-
const
|
|
1131
|
+
const runUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
1132
|
+
const promptRunID = await this.readPromptRunID(session, runUser, provider);
|
|
910
1133
|
if (!promptRunID) {
|
|
911
1134
|
return false;
|
|
912
1135
|
}
|
|
@@ -915,7 +1138,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
915
1138
|
'assistant',
|
|
916
1139
|
this.formatToolTurn(toolName, argsJson, resultJson),
|
|
917
1140
|
false,
|
|
918
|
-
|
|
1141
|
+
runUser,
|
|
919
1142
|
provider,
|
|
920
1143
|
);
|
|
921
1144
|
}
|
|
@@ -987,8 +1210,15 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
987
1210
|
* (observability creation was skipped), a failed load, or a failed save all log and return
|
|
988
1211
|
* `false` — usage relay must never break a live call.
|
|
989
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
|
+
*
|
|
990
1219
|
* @param inputTokens Input-token DELTA to add (negative/non-finite values are clamped to 0).
|
|
991
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 }`).
|
|
992
1222
|
* @returns `true` when the accumulated usage was persisted; `false` on any tolerated failure.
|
|
993
1223
|
*/
|
|
994
1224
|
@Mutation(() => Boolean)
|
|
@@ -997,25 +1227,28 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
997
1227
|
@Arg('inputTokens', () => Int) inputTokens: number,
|
|
998
1228
|
@Arg('outputTokens', () => Int) outputTokens: number,
|
|
999
1229
|
@Ctx() { userPayload, providers }: AppContext,
|
|
1230
|
+
@Arg('usageDetailsJson', () => String, { nullable: true }) usageDetailsJson?: string,
|
|
1000
1231
|
): Promise<boolean> {
|
|
1001
1232
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1002
1233
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
1003
1234
|
|
|
1004
1235
|
const inputDelta = this.clampTokenDelta(inputTokens);
|
|
1005
1236
|
const outputDelta = this.clampTokenDelta(outputTokens);
|
|
1006
|
-
|
|
1237
|
+
const details = this.readUsageDetails(usageDetailsJson);
|
|
1238
|
+
if (inputDelta === 0 && outputDelta === 0 && !details) {
|
|
1007
1239
|
return true; // nothing to add — a no-op flush is a success, not a failure
|
|
1008
1240
|
}
|
|
1009
1241
|
|
|
1010
|
-
|
|
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);
|
|
1011
1245
|
if (!promptRunID) {
|
|
1012
1246
|
return false; // no observability prompt run for this session — logged in the helper
|
|
1013
1247
|
}
|
|
1014
1248
|
// Delegate to the service so usage writes share the per-run serialization with transcript-message
|
|
1015
1249
|
// appends — otherwise the frequent usage save clobbers freshly-appended Messages (and vice-versa).
|
|
1016
|
-
// Runs as the scoped-anonymous elevated user (issue #3371) — the caller's role holds no prompt-run grants.
|
|
1017
1250
|
return this.clientSessionService.AccumulatePromptRunUsage(
|
|
1018
|
-
promptRunID, inputDelta, outputDelta,
|
|
1251
|
+
promptRunID, inputDelta, outputDelta, runUser, provider, details ?? undefined,
|
|
1019
1252
|
);
|
|
1020
1253
|
}
|
|
1021
1254
|
|
|
@@ -1024,6 +1257,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1024
1257
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
1025
1258
|
}
|
|
1026
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
|
+
|
|
1027
1277
|
/**
|
|
1028
1278
|
* Sanitizes client-supplied waveform peaks before they're persisted: drops anything that isn't a
|
|
1029
1279
|
* finite number, clamps each to `[0, 1]`, and caps the array length so a hostile client can't bloat
|
|
@@ -1043,24 +1293,40 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1043
1293
|
|
|
1044
1294
|
/**
|
|
1045
1295
|
* Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
|
|
1046
|
-
* throw-on-missing-target semantics
|
|
1047
|
-
*
|
|
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.
|
|
1048
1301
|
*/
|
|
1049
|
-
private readPromptRunID(
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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) {
|
|
1055
1313
|
LogStatus(
|
|
1056
1314
|
`RelayRealtimeUsage: session ${session.ID} has no co-agent promptRunID (observability run ` +
|
|
1057
1315
|
'creation was skipped or failed) — usage delta dropped.',
|
|
1058
1316
|
);
|
|
1059
1317
|
return null;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
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.`);
|
|
1062
1327
|
return null;
|
|
1063
1328
|
}
|
|
1329
|
+
return verified.PromptRunID;
|
|
1064
1330
|
}
|
|
1065
1331
|
|
|
1066
1332
|
/**
|
|
@@ -1094,6 +1360,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1094
1360
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1095
1361
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
1096
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
|
+
}
|
|
1097
1367
|
if (stateJson.length > MAX_CHANNEL_STATE_CHARS) {
|
|
1098
1368
|
LogError(
|
|
1099
1369
|
`SaveSessionChannelState: rejected oversized state for session ${agentSessionId} / channel '${channelName}' ` +
|
|
@@ -1173,6 +1443,11 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1173
1443
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
1174
1444
|
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
1175
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
|
+
}
|
|
1176
1451
|
if (contentJson.length > MAX_CHANNEL_STATE_CHARS) {
|
|
1177
1452
|
const message =
|
|
1178
1453
|
`SaveSessionChannelArtifact: rejected oversized content for session ${agentSessionId} / channel '${channelName}' ` +
|
|
@@ -1210,8 +1485,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1210
1485
|
}
|
|
1211
1486
|
|
|
1212
1487
|
/**
|
|
1213
|
-
* Authorization gate for
|
|
1214
|
-
*
|
|
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.
|
|
1215
1492
|
*/
|
|
1216
1493
|
private async assertCanRunTarget(
|
|
1217
1494
|
targetAgentId: string,
|
|
@@ -1479,6 +1756,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1479
1756
|
* resumed session remembers the previous leg(s).
|
|
1480
1757
|
* @param conversationMessages Optional capped history of the TEXT conversation this session is
|
|
1481
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`).
|
|
1482
1760
|
*/
|
|
1483
1761
|
private async prepareClientSessionOrClose(
|
|
1484
1762
|
session: MJAIAgentSessionEntity,
|
|
@@ -1494,6 +1772,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1494
1772
|
applicationId?: string,
|
|
1495
1773
|
appContext?: AppContextSnapshot,
|
|
1496
1774
|
conversationMessages?: ChatMessage[],
|
|
1775
|
+
channelCandidates?: RealtimeChannelCandidate[],
|
|
1776
|
+
showsAgentVideo?: boolean,
|
|
1497
1777
|
): Promise<StartRealtimeClientSessionResult> {
|
|
1498
1778
|
const prep = await this.clientSessionService.PrepareClientSession(
|
|
1499
1779
|
{
|
|
@@ -1521,6 +1801,14 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1521
1801
|
// (allowed-agent union); the snapshot is injected into the system prompt at mint.
|
|
1522
1802
|
ApplicationID: applicationId,
|
|
1523
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,
|
|
1524
1812
|
},
|
|
1525
1813
|
// SCOPED-ANONYMOUS ELEVATION (issue #3371): the prepare creates the co-agent
|
|
1526
1814
|
// observability AIAgentRun/AIPromptRun/run-step, which a scoped anonymous caller's role
|
|
@@ -1541,7 +1829,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1541
1829
|
session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID,
|
|
1542
1830
|
applicationId, prep.EffectiveConfig?.realtime?.allowedAgents,
|
|
1543
1831
|
prep.EffectiveConfig?.realtime?.directActions,
|
|
1832
|
+
prep.ClientPolicy?.Channels.map((c) => c.Key),
|
|
1833
|
+
appContext,
|
|
1544
1834
|
);
|
|
1835
|
+
this.pruneExcludedServerChannels(session.ID, prep.ClientPolicy);
|
|
1545
1836
|
|
|
1546
1837
|
const cfg = prep.ClientConfig;
|
|
1547
1838
|
return {
|
|
@@ -1551,20 +1842,43 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1551
1842
|
Model: cfg.Model,
|
|
1552
1843
|
EphemeralToken: cfg.EphemeralToken,
|
|
1553
1844
|
ExpiresAt: cfg.ExpiresAt,
|
|
1845
|
+
Transport: cfg.Transport,
|
|
1846
|
+
RelayUrl: cfg.RelayUrl,
|
|
1554
1847
|
SessionConfigJson: JSON.stringify(cfg.SessionConfig),
|
|
1555
1848
|
ModelName: prep.ModelName,
|
|
1556
1849
|
DriverClass: prep.DriverClass,
|
|
1557
1850
|
NarrationInstructionsTemplate: prep.NarrationInstructionsTemplate,
|
|
1558
1851
|
NarrationPaceMs: prep.NarrationPaceMs,
|
|
1559
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,
|
|
1560
1855
|
};
|
|
1561
1856
|
}
|
|
1562
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
|
+
|
|
1563
1874
|
/**
|
|
1564
1875
|
* Writes the co-agent observability run ids into the session's `Config_` alongside the
|
|
1565
|
-
*
|
|
1566
|
-
* finalize the runs (and on relay to nest delegated runs).
|
|
1567
|
-
*
|
|
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.
|
|
1568
1882
|
*/
|
|
1569
1883
|
private async persistObservabilityRunIDs(
|
|
1570
1884
|
session: MJAIAgentSessionEntity,
|
|
@@ -1575,22 +1889,29 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1575
1889
|
applicationID?: string,
|
|
1576
1890
|
allowedAgents?: RealtimeAllowedAgent[],
|
|
1577
1891
|
directActions?: RealtimeDirectActionsConfig,
|
|
1892
|
+
channels?: string[],
|
|
1893
|
+
appContext?: AppContextSnapshot,
|
|
1578
1894
|
): Promise<void> {
|
|
1579
|
-
//
|
|
1580
|
-
//
|
|
1581
|
-
|
|
1582
|
-
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_),
|
|
1583
1900
|
targetAgentID,
|
|
1584
1901
|
coAgentRunID,
|
|
1585
1902
|
promptRunID,
|
|
1586
1903
|
coAgentRunStepID,
|
|
1587
|
-
maxSessionDeadlineIso: existing?.maxSessionDeadlineIso,
|
|
1588
1904
|
applicationID,
|
|
1905
|
+
appContext: this.appContextToStore(appContext, session.ID),
|
|
1589
1906
|
allowedAgents: allowedAgents && allowedAgents.length > 0 ? allowedAgents : undefined,
|
|
1590
1907
|
directActions,
|
|
1908
|
+
channels,
|
|
1591
1909
|
};
|
|
1592
1910
|
session.Config_ = JSON.stringify(config);
|
|
1593
|
-
|
|
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());
|
|
1594
1915
|
if (!saved) {
|
|
1595
1916
|
LogError(
|
|
1596
1917
|
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
@@ -1598,6 +1919,32 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1598
1919
|
}
|
|
1599
1920
|
}
|
|
1600
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
|
+
|
|
1601
1948
|
/**
|
|
1602
1949
|
* Loads a session, enforcing inbound ownership (`UserID === contextUser.ID`) and that it is not
|
|
1603
1950
|
* `Closed`. Throws on any violation. Returns the loaded session for the caller to read from.
|
|
@@ -1843,6 +2190,10 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1843
2190
|
* as the resumed transcript are removed, so the prompt never carries two accounts of one
|
|
1844
2191
|
* exchange.
|
|
1845
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
|
+
*
|
|
1846
2197
|
* Strictly tolerant — no conversation, a failed read, or any throw yields `[]`, never a failed
|
|
1847
2198
|
* session start. The call still works; it just starts cold.
|
|
1848
2199
|
*
|
|
@@ -1850,6 +2201,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1850
2201
|
* @param coveredSessionIDs Session ids already framed via `PriorTranscript`.
|
|
1851
2202
|
* @param contextUser The calling user (RunView scope — the read is row-level-secured).
|
|
1852
2203
|
* @param provider The request-scoped metadata provider.
|
|
2204
|
+
* @param branchId The fork the session started on; null is Main.
|
|
1853
2205
|
* @returns The capped history, oldest first.
|
|
1854
2206
|
*/
|
|
1855
2207
|
private async loadConversationHistory(
|
|
@@ -1857,12 +2209,22 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1857
2209
|
coveredSessionIDs: string[],
|
|
1858
2210
|
contextUser: UserInfo,
|
|
1859
2211
|
provider: IMetadataProvider,
|
|
2212
|
+
branchId: string | null,
|
|
1860
2213
|
): Promise<ChatMessage[]> {
|
|
1861
2214
|
if (!conversationId) {
|
|
1862
2215
|
return [];
|
|
1863
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
|
+
}
|
|
1864
2227
|
try {
|
|
1865
|
-
const rv = RunView.FromMetadataProvider(provider);
|
|
1866
2228
|
const result = await rv.RunView<{
|
|
1867
2229
|
Role: string;
|
|
1868
2230
|
Message: string | null;
|
|
@@ -1871,9 +2233,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1871
2233
|
}>(
|
|
1872
2234
|
{
|
|
1873
2235
|
EntityName: CONVERSATION_DETAIL_ENTITY,
|
|
1874
|
-
ExtraFilter:
|
|
2236
|
+
ExtraFilter: ConversationEngine.LiveRowsFilter(pathFilter),
|
|
1875
2237
|
Fields: ['ID', 'Role', 'Message', 'HiddenToUser', 'AgentSessionID', '__mj_CreatedAt'],
|
|
1876
|
-
OrderBy: '
|
|
2238
|
+
OrderBy: 'Sequence ASC',
|
|
1877
2239
|
ResultType: 'simple',
|
|
1878
2240
|
},
|
|
1879
2241
|
contextUser,
|
|
@@ -2240,6 +2602,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2240
2602
|
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY, callerUser);
|
|
2241
2603
|
detail.NewRecord();
|
|
2242
2604
|
detail.ConversationID = session.ConversationID;
|
|
2605
|
+
detail.BranchID = this.sessionBranchIdOf(session);
|
|
2243
2606
|
detail.Role = 'AI';
|
|
2244
2607
|
detail.HiddenToUser = true;
|
|
2245
2608
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
@@ -2392,21 +2755,54 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2392
2755
|
|
|
2393
2756
|
/** Shape check for one client tool declaration: Name/Description non-empty strings, ParametersSchema a plain object. */
|
|
2394
2757
|
private isValidClientTool(candidate: unknown): boolean {
|
|
2395
|
-
|
|
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}'.`);
|
|
2396
2789
|
return false;
|
|
2397
2790
|
}
|
|
2398
|
-
const
|
|
2399
|
-
return (
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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;
|
|
2404
2799
|
}
|
|
2405
2800
|
|
|
2406
2801
|
/**
|
|
2407
|
-
* Parses the session's persisted config, returning
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
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.
|
|
2410
2806
|
*/
|
|
2411
2807
|
private readSessionConfig(session: MJAIAgentSessionEntity): RealtimeSessionConfig {
|
|
2412
2808
|
const raw = session.Config_;
|
|
@@ -2424,11 +2820,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2424
2820
|
maxSessionDeadlineIso:
|
|
2425
2821
|
typeof parsed.maxSessionDeadlineIso === 'string' ? parsed.maxSessionDeadlineIso : undefined,
|
|
2426
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,
|
|
2427
2827
|
allowedAgents: Array.isArray(parsed.allowedAgents) ? parsed.allowedAgents : undefined,
|
|
2428
2828
|
directActions:
|
|
2429
2829
|
parsed.directActions && typeof parsed.directActions === 'object' && typeof parsed.directActions.enabled === 'boolean'
|
|
2430
2830
|
? (parsed.directActions as RealtimeDirectActionsConfig)
|
|
2431
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,
|
|
2432
2835
|
};
|
|
2433
2836
|
}
|
|
2434
2837
|
} catch {
|
|
@@ -2516,6 +2919,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2516
2919
|
);
|
|
2517
2920
|
detail.NewRecord();
|
|
2518
2921
|
detail.ConversationID = session.ConversationID;
|
|
2922
|
+
detail.BranchID = this.sessionBranchIdOf(session);
|
|
2519
2923
|
detail.Role = this.mapTranscriptRole(role);
|
|
2520
2924
|
detail.Message = text;
|
|
2521
2925
|
detail.AgentSessionID = session.ID;
|
|
@@ -2689,6 +3093,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2689
3093
|
);
|
|
2690
3094
|
detail.NewRecord();
|
|
2691
3095
|
detail.ConversationID = session.ConversationID;
|
|
3096
|
+
detail.BranchID = this.sessionBranchIdOf(session);
|
|
2692
3097
|
detail.Role = 'AI';
|
|
2693
3098
|
detail.HiddenToUser = true;
|
|
2694
3099
|
detail.AgentSessionID = session.ID;
|