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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/README.md +30 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/config.d.ts +2189 -71
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +123 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/context.d.ts.map +1 -1
  8. package/dist/context.js +11 -23
  9. package/dist/context.js.map +1 -1
  10. package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
  11. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  12. package/dist/generated/graphql-schemas/__mj.js +24441 -14433
  13. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  14. package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
  15. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
  16. package/dist/generic/FireAndForgetHeartbeat.js +16 -2
  17. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
  18. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
  19. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
  20. package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
  21. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
  22. package/dist/generic/PushStatusResolver.d.ts +38 -0
  23. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  24. package/dist/generic/PushStatusResolver.js +84 -1
  25. package/dist/generic/PushStatusResolver.js.map +1 -1
  26. package/dist/generic/ResolverBase.d.ts +56 -4
  27. package/dist/generic/ResolverBase.d.ts.map +1 -1
  28. package/dist/generic/ResolverBase.js +191 -74
  29. package/dist/generic/ResolverBase.js.map +1 -1
  30. package/dist/generic/RunViewResolver.d.ts +4 -0
  31. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  32. package/dist/generic/RunViewResolver.js +33 -1
  33. package/dist/generic/RunViewResolver.js.map +1 -1
  34. package/dist/index.d.ts +4 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +204 -50
  37. package/dist/index.js.map +1 -1
  38. package/dist/postgresqlCompat.d.ts +8 -0
  39. package/dist/postgresqlCompat.d.ts.map +1 -0
  40. package/dist/postgresqlCompat.js +19 -0
  41. package/dist/postgresqlCompat.js.map +1 -0
  42. package/dist/postgresqlPoolSettings.d.ts +108 -0
  43. package/dist/postgresqlPoolSettings.d.ts.map +1 -0
  44. package/dist/postgresqlPoolSettings.js +134 -0
  45. package/dist/postgresqlPoolSettings.js.map +1 -0
  46. package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
  47. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  48. package/dist/resolvers/AdhocQueryResolver.js +77 -167
  49. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  50. package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
  51. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
  52. package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
  53. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
  54. package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
  55. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
  56. package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
  57. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
  58. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
  59. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
  60. package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
  61. package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
  62. package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
  63. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  64. package/dist/resolvers/EntityRecordNameResolver.js +9 -47
  65. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  66. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  67. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
  68. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  69. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  70. package/dist/resolvers/GetDataResolver.js +9 -0
  71. package/dist/resolvers/GetDataResolver.js.map +1 -1
  72. package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
  73. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
  74. package/dist/resolvers/HumanHandoffResolver.js +324 -0
  75. package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
  76. package/dist/resolvers/MeetingResolver.d.ts +69 -0
  77. package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
  78. package/dist/resolvers/MeetingResolver.js +381 -0
  79. package/dist/resolvers/MeetingResolver.js.map +1 -0
  80. package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
  81. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  82. package/dist/resolvers/MergeRecordsResolver.js +16 -2
  83. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  84. package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
  85. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  86. package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
  87. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  88. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
  89. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  90. package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
  91. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  92. package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
  93. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  94. package/dist/resolvers/RunAIAgentResolver.js +23 -153
  95. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  96. package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
  97. package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
  98. package/dist/resolvers/RunDecisionResolver.js +396 -0
  99. package/dist/resolvers/RunDecisionResolver.js.map +1 -0
  100. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  101. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  102. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  103. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  104. package/dist/resolvers/adhocTimeout.d.ts +7 -0
  105. package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
  106. package/dist/resolvers/adhocTimeout.js +12 -0
  107. package/dist/resolvers/adhocTimeout.js.map +1 -0
  108. package/dist/resolvers/meetings/index.d.ts +5 -0
  109. package/dist/resolvers/meetings/index.d.ts.map +1 -0
  110. package/dist/resolvers/meetings/index.js +5 -0
  111. package/dist/resolvers/meetings/index.js.map +1 -0
  112. package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
  113. package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
  114. package/dist/resolvers/meetings/meetingHelpers.js +212 -0
  115. package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
  116. package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
  117. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
  118. package/dist/resolvers/meetings/meetingMutations.js +418 -0
  119. package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
  120. package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
  121. package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
  122. package/dist/resolvers/meetings/meetingQueries.js +113 -0
  123. package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
  124. package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
  125. package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
  126. package/dist/resolvers/meetings/meetingTypes.js +345 -0
  127. package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
  128. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  129. package/dist/rest/ViewOperationsHandler.js +8 -5
  130. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  131. package/dist/services/TaskGraphActionRunner.d.ts +8 -0
  132. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  133. package/dist/services/TaskGraphActionRunner.js +27 -1
  134. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  135. package/dist/services/WorkQueueHostService.d.ts +36 -0
  136. package/dist/services/WorkQueueHostService.d.ts.map +1 -0
  137. package/dist/services/WorkQueueHostService.js +97 -0
  138. package/dist/services/WorkQueueHostService.js.map +1 -0
  139. package/dist/services/workQueueConfig.d.ts +103 -0
  140. package/dist/services/workQueueConfig.d.ts.map +1 -0
  141. package/dist/services/workQueueConfig.js +48 -0
  142. package/dist/services/workQueueConfig.js.map +1 -0
  143. package/dist/types.d.ts +5 -0
  144. package/dist/types.d.ts.map +1 -1
  145. package/dist/types.js.map +1 -1
  146. package/package.json +95 -92
  147. package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
  148. package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
  149. package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
  150. package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
  151. package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
  152. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
  153. package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
  154. package/src/__tests__/MeetingResolver.test.ts +374 -0
  155. package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
  156. package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
  157. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
  158. package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
  159. package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
  160. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
  161. package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
  162. package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
  163. package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
  164. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
  165. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
  166. package/src/__tests__/RunDecisionResolver.test.ts +699 -0
  167. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
  168. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  169. package/src/__tests__/WorkQueueHostService.test.ts +133 -0
  170. package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
  171. package/src/__tests__/conversationRunEvents.test.ts +399 -0
  172. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
  173. package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
  174. package/src/__tests__/pushStatusFanOut.test.ts +206 -0
  175. package/src/__tests__/translateBracketsToPG.test.ts +20 -0
  176. package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
  177. package/src/config.ts +130 -3
  178. package/src/context.ts +11 -24
  179. package/src/generated/graphql-schemas/__mj.ts +24183 -17182
  180. package/src/generic/FireAndForgetHeartbeat.ts +16 -2
  181. package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
  182. package/src/generic/PushStatusResolver.ts +97 -1
  183. package/src/generic/ResolverBase.ts +220 -76
  184. package/src/generic/RunViewResolver.ts +33 -2
  185. package/src/index.ts +228 -51
  186. package/src/postgresqlCompat.ts +21 -0
  187. package/src/postgresqlPoolSettings.ts +213 -0
  188. package/src/resolvers/AdhocQueryResolver.ts +88 -188
  189. package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
  190. package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
  191. package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
  192. package/src/resolvers/EntityRecordNameResolver.ts +9 -51
  193. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
  194. package/src/resolvers/GetDataResolver.ts +10 -0
  195. package/src/resolvers/HumanHandoffResolver.ts +304 -0
  196. package/src/resolvers/MeetingResolver.ts +438 -0
  197. package/src/resolvers/MergeRecordsResolver.ts +14 -2
  198. package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
  199. package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
  200. package/src/resolvers/RunAIAgentResolver.ts +28 -174
  201. package/src/resolvers/RunDecisionResolver.ts +435 -0
  202. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  203. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
  204. package/src/resolvers/adhocTimeout.ts +12 -0
  205. package/src/resolvers/meetings/index.ts +4 -0
  206. package/src/resolvers/meetings/meetingHelpers.ts +307 -0
  207. package/src/resolvers/meetings/meetingMutations.ts +535 -0
  208. package/src/resolvers/meetings/meetingQueries.ts +172 -0
  209. package/src/resolvers/meetings/meetingTypes.ts +238 -0
  210. package/src/rest/ViewOperationsHandler.ts +8 -5
  211. package/src/services/TaskGraphActionRunner.ts +29 -1
  212. package/src/services/WorkQueueHostService.ts +129 -0
  213. package/src/services/workQueueConfig.ts +52 -0
  214. package/src/types.ts +5 -0
  215. package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
  216. package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
  217. package/dist/resolvers/adhoc-query-helpers.js +0 -50
  218. package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
  219. package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
  220. package/src/resolvers/adhoc-query-helpers.ts +0 -67
@@ -1,15 +1,24 @@
1
1
  import { Resolver, Mutation, Query, Arg, Ctx, ObjectType, InputType, Field } from 'type-graphql';
2
2
  import { randomUUID } from 'crypto';
3
- import { LogError, LogStatusEx, UserInfo, IMetadataProvider } from '@memberjunction/core';
4
- import { LiveKitTokenService, LiveKitAgentRoomCoordinator, LiveKitEgressService } from '@memberjunction/livekit-room-server';
3
+ import { LogError, LogStatusEx, UserInfo, IMetadataProvider, RunView } from '@memberjunction/core';
4
+ import { EscapeSQLString } from '@memberjunction/global';
5
+ import type { TurnAddressingMode } from '@memberjunction/ai-bridge-base';
6
+ import {
7
+ LiveKitTokenService,
8
+ LiveKitAgentRoomCoordinator,
9
+ LiveKitEgressService,
10
+ LiveKitUserIdentity,
11
+ RoomHandoffEngine,
12
+ } from '@memberjunction/livekit-room-server';
5
13
  import { AppContext } from '../types.js';
6
14
  import { ResolverBase } from '../generic/ResolverBase.js';
7
15
  import { GetReadWriteProvider } from '../util.js';
8
- import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink, RealtimeTurnModeratorDecision } from '@memberjunction/ai-agents';
16
+ import { CreateBridgeRealtimeSession, FinalizeBridgeCoAgentRuns, GetRealtimeModelVoices, CreateBridgeRoomTranscriptSink, RealtimeTurnModeratorDecision, GetBridgeRealtimeRuntime } from '@memberjunction/ai-agents';
9
17
  import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
10
18
  import { SessionManager } from '../agentSessions/SessionManager.js';
11
19
  import { NotificationEngine } from '@memberjunction/notifications';
12
20
  import { RegisterMeetingRecordingFile, CorrelateRecordingStart } from './meetingRecordingRegistration.js';
21
+ import { RoomAuthorizationService } from '@memberjunction/livekit-room-server';
13
22
 
14
23
  /**
15
24
  * Binds the agent realtime-session factory onto the LiveKit room coordinator's model-session creation seam.
@@ -21,6 +30,20 @@ import { RegisterMeetingRecordingFile, CorrelateRecordingStart } from './meeting
21
30
  */
22
31
  LiveKitAgentRoomCoordinator.Instance.SetSessionFactory(CreateBridgeRealtimeSession);
23
32
 
33
+ /**
34
+ * Binds the turn-taking tool handler seam (same module-load rationale as the factory above). A full-duplex model
35
+ * in a shared room calls `i_am_addressed` / `yield_turn`; the bridge engine owns what those mean, and this installs
36
+ * the engine's handler as the model session runtime's local tool handler (`@memberjunction/ai-agents`).
37
+ */
38
+ LiveKitAgentRoomCoordinator.Instance.SetTurnToolBinder((session, handler) => {
39
+ const runtime = GetBridgeRealtimeRuntime(session);
40
+ if (!runtime) {
41
+ LogError('[RealtimeBridge] the model session has no bridge runtime; i_am_addressed / yield_turn will not execute for it.');
42
+ return;
43
+ }
44
+ runtime.SetLocalToolHandler(handler);
45
+ });
46
+
24
47
  /**
25
48
  * Binds the co-agent run finalizer onto the bridge engine (same module-load rationale as the factory above).
26
49
  * Lets the engine finalize a session's dangling co-agent observability run when it reaps a bridge WITHOUT a
@@ -127,6 +150,35 @@ export class StartLiveKitAgentRoomSessionInput {
127
150
 
128
151
  @Field(() => String, { nullable: true })
129
152
  TurnMode?: string;
153
+
154
+ /**
155
+ * Start the agent able to bring someone into this room: a person (who is offered the conversation and may accept), a
156
+ * phone number, or another agent. Opt-in, and honoured only when this server has the handoff collaborators configured
157
+ * (the LiveKit SIP extension); otherwise the agent starts as it always did.
158
+ */
159
+ @Field(() => Boolean, { nullable: true })
160
+ EnableHandoff?: boolean;
161
+
162
+ /**
163
+ * How this agent decides it was addressed: `Auto` (default — the model's own judgement when it is full-duplex,
164
+ * name matching otherwise), `ModelSide`, or `Regex`.
165
+ */
166
+ @Field(() => String, { nullable: true })
167
+ TurnAddressing?: string;
168
+ }
169
+
170
+ /** The live turn-taking state of a room (floor holder, human speaking, hand-off, loop cap, recent events, seated agents). */
171
+ @ObjectType()
172
+ export class LiveKitRoomTurnStateResult {
173
+ @Field(() => Boolean)
174
+ Success: boolean;
175
+
176
+ @Field(() => String, { nullable: true })
177
+ ErrorMessage?: string;
178
+
179
+ /** JSON of the room's `RoomTurnSnapshot`; `null` when the room holds no agents. Parsed by the typed client. */
180
+ @Field(() => String, { nullable: true })
181
+ StateJSON?: string;
130
182
  }
131
183
 
132
184
  @ObjectType()
@@ -231,6 +283,11 @@ export class RealtimeBridgeResolver extends ResolverBase {
231
283
  if (!user) {
232
284
  return failure('Unable to determine current user.');
233
285
  }
286
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
287
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(input.RoomName, user, provider);
288
+ if (!auth.Authorized) {
289
+ return failure(auth.Reason ?? 'Unauthorized room access.');
290
+ }
234
291
  const tokenService = new LiveKitTokenService();
235
292
  const minted = await tokenService.MintClientToken(input.RoomName, this.participantIdentity(user), input.DisplayName ?? user.Name ?? user.Email);
236
293
  return { Success: true, ...minted };
@@ -264,9 +321,31 @@ export class RealtimeBridgeResolver extends ResolverBase {
264
321
  if (!user) {
265
322
  return failure('Unable to determine current user.');
266
323
  }
267
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
324
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
268
325
  const roomName = input.RoomName?.trim() || `mj-${randomUUID()}`;
269
326
 
327
+ if (input.RoomName?.trim()) {
328
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
329
+ if (!auth.Authorized) {
330
+ return failure(auth.Reason ?? 'Unauthorized room access.', roomName);
331
+ }
332
+ }
333
+
334
+ // Opt-in: an agent that can bring a person, a number or another agent into this room (the same room, so a visitor
335
+ // is escalated without leaving it). Falls through to the standard start when the server cannot do handoffs.
336
+ const handoffSessionBridgeID = await this.startWithHandoff(input, roomName, user, provider);
337
+ if (handoffSessionBridgeID) {
338
+ const handoffToken = await new LiveKitTokenService().MintClientToken(roomName, this.participantIdentity(user), user.Name ?? user.Email);
339
+ return {
340
+ Success: true,
341
+ SessionBridgeID: handoffSessionBridgeID,
342
+ RoomName: roomName,
343
+ ServerUrl: handoffToken.ServerUrl,
344
+ ClientToken: handoffToken.Token,
345
+ Identity: handoffToken.Identity,
346
+ };
347
+ }
348
+
270
349
  // Resolve the AIAgentSession the bridge will reference. The bridge row FK-references
271
350
  // AIAgentSession(ID), so we must use an EXISTING session — either one the caller supplied, or a
272
351
  // freshly-created one. Previously this minted a bare random UUID with no backing row, so the
@@ -293,6 +372,7 @@ export class RealtimeBridgeResolver extends ResolverBase {
293
372
  RealtimeModelID: input.RealtimeModelID,
294
373
  RealtimeVoice: input.RealtimeVoice,
295
374
  TurnMode: this.normalizeTurnMode(input.TurnMode),
375
+ TurnAddressing: this.normalizeTurnAddressing(input.TurnAddressing),
296
376
  ContextUser: user,
297
377
  MetadataProvider: provider,
298
378
  });
@@ -332,7 +412,15 @@ export class RealtimeBridgeResolver extends ResolverBase {
332
412
  if (!user) {
333
413
  return false;
334
414
  }
335
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
415
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
416
+ const roomName = LiveKitAgentRoomCoordinator.Instance.GetRoomForBridge(sessionBridgeID);
417
+ if (roomName) {
418
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
419
+ if (!auth.Authorized) {
420
+ LogError(`StopLiveKitAgentRoomSession unauthorized: user ${user.ID} cannot access room ${roomName} (${auth.Reason})`);
421
+ return false;
422
+ }
423
+ }
336
424
  return await LiveKitAgentRoomCoordinator.Instance.StopAgentRoomSession(sessionBridgeID, 'Explicit', user, provider);
337
425
  } catch (error) {
338
426
  LogError(`StopLiveKitAgentRoomSession failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -358,7 +446,12 @@ export class RealtimeBridgeResolver extends ResolverBase {
358
446
  if (!user) {
359
447
  return false;
360
448
  }
361
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
449
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
450
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
451
+ if (!auth.Authorized) {
452
+ LogError(`EndLiveKitRoom unauthorized: user ${user.ID} cannot access room ${roomName} (${auth.Reason})`);
453
+ return false;
454
+ }
362
455
  await LiveKitAgentRoomCoordinator.Instance.StopAllAgentsInRoom(roomName, 'Explicit', user, provider);
363
456
  return true;
364
457
  } catch (error) {
@@ -367,6 +460,37 @@ export class RealtimeBridgeResolver extends ResolverBase {
367
460
  }
368
461
  }
369
462
 
463
+ /**
464
+ * A room's live turn-taking state — who holds the floor, whether a person is speaking, any pending hand-off,
465
+ * the agent-to-agent loop-cap progress, the most recent floor/backchannel/yield events, and who is seated. The
466
+ * agent test bed polls it. Read-only. `StateJSON` is `null` when no agent is in the room.
467
+ *
468
+ * @param roomName The LiveKit room.
469
+ */
470
+ @Query(() => LiveKitRoomTurnStateResult)
471
+ async GetLiveKitRoomTurnState(
472
+ @Arg('roomName', () => String) roomName: string,
473
+ @Ctx() context: AppContext = {} as AppContext,
474
+ ): Promise<LiveKitRoomTurnStateResult> {
475
+ try {
476
+ const user = this.GetUserFromPayload(context.userPayload);
477
+ if (!user) {
478
+ return { Success: false, ErrorMessage: 'Unable to determine current user.' };
479
+ }
480
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
481
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
482
+ if (!auth.Authorized) {
483
+ return { Success: false, ErrorMessage: auth.Reason ?? 'Unauthorized room access.' };
484
+ }
485
+ const snapshot = LiveKitAgentRoomCoordinator.Instance.GetRoomTurnState(roomName);
486
+ return { Success: true, StateJSON: snapshot ? JSON.stringify(snapshot) : undefined };
487
+ } catch (error) {
488
+ const msg = error instanceof Error ? error.message : String(error);
489
+ LogError(`GetLiveKitRoomTurnState failed: ${msg}`);
490
+ return { Success: false, ErrorMessage: msg };
491
+ }
492
+ }
493
+
370
494
  /**
371
495
  * Lists active Realtime models with the voices each driver supports — the source for the dev model/voice
372
496
  * picker (gated client-side by the `Realtime: Advanced Session Controls` authorization). Read-only; returns
@@ -381,7 +505,7 @@ export class RealtimeBridgeResolver extends ResolverBase {
381
505
  if (!user) {
382
506
  return [];
383
507
  }
384
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
508
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
385
509
  return await GetRealtimeModelVoices(user, provider);
386
510
  } catch (error) {
387
511
  LogError(`GetRealtimeModelVoices failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -411,7 +535,12 @@ export class RealtimeBridgeResolver extends ResolverBase {
411
535
  if (!user) {
412
536
  return false;
413
537
  }
414
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
538
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
539
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
540
+ if (!auth.Authorized) {
541
+ LogError(`InviteUsersToLiveKitRoom unauthorized: user ${user.ID} cannot access room ${roomName} (${auth.Reason})`);
542
+ return false;
543
+ }
415
544
  await NotificationEngine.Instance.Config(false, user, provider);
416
545
 
417
546
  const inviter = user.Name?.trim() || user.Email || 'Someone';
@@ -453,11 +582,15 @@ export class RealtimeBridgeResolver extends ResolverBase {
453
582
  if (!user) {
454
583
  return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: '', Status: '' };
455
584
  }
585
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
586
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(input.RoomName, user, provider);
587
+ if (!auth.Authorized) {
588
+ return { Success: false, ErrorMessage: auth.Reason ?? 'Unauthorized room access.', EgressID: '', Status: '' };
589
+ }
456
590
  const info = await new LiveKitEgressService().StartRoomRecording({ RoomName: input.RoomName, Layout: input.Layout });
457
591
 
458
592
  // Best-effort: correlate the live recording with the room's Meeting-Room Conversation (if it exists
459
593
  // yet) by stamping its EgressID. Never fail the start on this — the stop-flow resolves/creates it.
460
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
461
594
  void CorrelateRecordingStart(input.RoomName, info.EgressID, user, provider);
462
595
 
463
596
  return { Success: true, EgressID: info.EgressID, Status: info.Status };
@@ -481,8 +614,43 @@ export class RealtimeBridgeResolver extends ResolverBase {
481
614
  if (!user) {
482
615
  return { Success: false, ErrorMessage: 'Unable to determine current user.', EgressID: egressID, Status: '' };
483
616
  }
484
- const info = await new LiveKitEgressService().StopRecording(egressID);
485
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
617
+ const provider = GetReadWriteProvider(context.providers) as IMetadataProvider;
618
+
619
+ // Map egress ID to room name: check LiveKit egress service first, fallback to Conversation if available.
620
+ const egressService = new LiveKitEgressService();
621
+ let roomName = await egressService.GetRoomForEgress(egressID);
622
+ if (!roomName && provider) {
623
+ try {
624
+ const rv = RunView.FromMetadataProvider(provider);
625
+ const convResult = await rv.RunView<{ ExternalID: string }>(
626
+ {
627
+ EntityName: 'MJ: Conversations',
628
+ ExtraFilter: `EgressID='${EscapeSQLString(egressID)}' AND (IsArchived IS NULL OR IsArchived=0)`,
629
+ Fields: ['ExternalID'],
630
+ OrderBy: '__mj_CreatedAt DESC',
631
+ MaxRows: 1,
632
+ ResultType: 'simple',
633
+ },
634
+ user,
635
+ );
636
+ if (convResult.Success && convResult.Results.length > 0 && convResult.Results[0].ExternalID) {
637
+ roomName = convResult.Results[0].ExternalID;
638
+ }
639
+ } catch (err) {
640
+ LogError(`StopLiveKitRecording: error looking up Conversation for egress ID '${egressID}'`, undefined, err);
641
+ }
642
+ }
643
+
644
+ if (roomName) {
645
+ const auth = await RoomAuthorizationService.Instance.AuthorizeRoomAccess(roomName, user, provider);
646
+ if (!auth.Authorized) {
647
+ return { Success: false, ErrorMessage: auth.Reason ?? 'Unauthorized room access.', EgressID: egressID, Status: '' };
648
+ }
649
+ } else {
650
+ return { Success: false, ErrorMessage: `Unable to determine room for egress ID '${egressID}'.`, EgressID: egressID, Status: '' };
651
+ }
652
+
653
+ const info = await egressService.StopRecording(egressID);
486
654
 
487
655
  // Register the completed egress MP4 as a Files row on the Meeting-Room Conversation. Best-effort:
488
656
  // any failure (e.g. storage provider not configured) leaves RecordingFileID unset but still
@@ -504,9 +672,55 @@ export class RealtimeBridgeResolver extends ResolverBase {
504
672
  }
505
673
  }
506
674
 
507
- /** Builds a stable, lowercased participant identity from the authenticated user. */
675
+ /**
676
+ * Starts the agent through the handoff-capable room starter when the caller asked for it and the server can do it.
677
+ * Returns the bridge id, or `undefined` to mean "use the standard start" (not requested, no starter, no agent named).
678
+ */
679
+ private async startWithHandoff(
680
+ input: StartLiveKitAgentRoomSessionInput,
681
+ roomName: string,
682
+ user: UserInfo,
683
+ provider: IMetadataProvider,
684
+ ): Promise<string | undefined> {
685
+ if (!input.EnableHandoff) {
686
+ return undefined;
687
+ }
688
+ const starter = RoomHandoffEngine.Instance.Deps.AgentStarter;
689
+ const agentID = input.TargetAgentID?.trim() || input.AgentID?.trim();
690
+ if (!starter || !agentID) {
691
+ LogError('StartLiveKitAgentRoomSession: handoff was requested but this server has no handoff-capable agent starter; starting the agent without it.');
692
+ return undefined;
693
+ }
694
+ const started = await starter({
695
+ RoomName: roomName,
696
+ AgentID: agentID,
697
+ AgentName: input.AgentName?.trim() ?? '',
698
+ CallerLabel: user.Name?.trim() || user.Email || 'Web visitor',
699
+ ContextUser: user,
700
+ Provider: provider,
701
+ RealtimeModelID: input.RealtimeModelID,
702
+ RealtimeVoice: input.RealtimeVoice,
703
+ });
704
+ return started.SessionBridgeID;
705
+ }
706
+
707
+ /** Builds a stable, lowercased participant identity from the authenticated user (shared with the handoff engine, which watches for it). */
508
708
  private participantIdentity(user: UserInfo): string {
509
- return `user-${user.ID}`.toLowerCase();
709
+ return LiveKitUserIdentity(user.ID);
710
+ }
711
+
712
+ /** Normalizes an addressing-mode string; unknown/absent leaves the coordinator's default (`Auto`). */
713
+ private normalizeTurnAddressing(mode?: string): TurnAddressingMode | undefined {
714
+ switch ((mode ?? '').trim().toLowerCase()) {
715
+ case 'auto':
716
+ return 'Auto';
717
+ case 'modelside':
718
+ return 'ModelSide';
719
+ case 'regex':
720
+ return 'Regex';
721
+ default:
722
+ return undefined;
723
+ }
510
724
  }
511
725
 
512
726
  /** Normalizes a turn-mode string to the bridge's accepted values. */